From 70128896d12dcf8859b990bf02eebba50c4742a7 Mon Sep 17 00:00:00 2001 From: Anthony Lawn Date: Sat, 11 Nov 2023 15:18:39 -0600 Subject: [PATCH 01/18] WIP External Auth --- crates/api/Cargo.toml | 1 + crates/api/src/local_user/mod.rs | 1 + crates/api/src/local_user/oauth_callback.rs | 122 ++++++++++++++++++ crates/api/src/site/leave_admin.rs | 5 +- crates/api_common/src/external_auth.rs | 96 ++++++++++++++ crates/api_common/src/lib.rs | 1 + crates/api_common/src/site.rs | 3 + crates/api_crud/src/external_auth/create.rs | 42 ++++++ crates/api_crud/src/external_auth/delete.rs | 25 ++++ crates/api_crud/src/external_auth/mod.rs | 3 + crates/api_crud/src/external_auth/update.rs | 46 +++++++ crates/api_crud/src/lib.rs | 1 + crates/api_crud/src/site/read.rs | 5 +- crates/db_schema/src/impls/external_auth.rs | 40 ++++++ crates/db_schema/src/impls/mod.rs | 1 + crates/db_schema/src/newtypes.rs | 8 +- crates/db_schema/src/schema.rs | 22 ++++ crates/db_schema/src/source/external_auth.rs | 70 ++++++++++ crates/db_schema/src/source/mod.rs | 1 + crates/db_views/Cargo.toml | 1 + crates/db_views/src/external_auth_view.rs | 85 ++++++++++++ crates/db_views/src/lib.rs | 2 + crates/db_views/src/structs.rs | 9 ++ docker/docker-compose.yml | 8 +- .../down.sql | 1 + .../up.sql | 17 +++ src/api_routes_http.rs | 18 +++ 27 files changed, 627 insertions(+), 7 deletions(-) create mode 100644 crates/api/src/local_user/oauth_callback.rs create mode 100644 crates/api_common/src/external_auth.rs create mode 100644 crates/api_crud/src/external_auth/create.rs create mode 100644 crates/api_crud/src/external_auth/delete.rs create mode 100644 crates/api_crud/src/external_auth/mod.rs create mode 100644 crates/api_crud/src/external_auth/update.rs create mode 100644 crates/db_schema/src/impls/external_auth.rs create mode 100644 crates/db_schema/src/source/external_auth.rs create mode 100644 crates/db_views/src/external_auth_view.rs create mode 100644 migrations/2023-11-03-131721_create_external_auth/down.sql create mode 100644 migrations/2023-11-03-131721_create_external_auth/up.sql diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index d9c4c10513..a9c08a70d1 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -23,6 +23,7 @@ lemmy_api_common = { workspace = true, features = ["full"] } activitypub_federation = { workspace = true } bcrypt = { workspace = true } serde = { workspace = true } +serde_json = { workspace = true } actix-web = { workspace = true } base64 = { workspace = true } uuid = { workspace = true } diff --git a/crates/api/src/local_user/mod.rs b/crates/api/src/local_user/mod.rs index 1b58713f15..9d3b40b92a 100644 --- a/crates/api/src/local_user/mod.rs +++ b/crates/api/src/local_user/mod.rs @@ -10,6 +10,7 @@ pub mod list_logins; pub mod login; pub mod logout; pub mod notifications; +pub mod oauth_callback; pub mod report_count; pub mod reset_password; pub mod save_settings; diff --git a/crates/api/src/local_user/oauth_callback.rs b/crates/api/src/local_user/oauth_callback.rs new file mode 100644 index 0000000000..4596ee6fdb --- /dev/null +++ b/crates/api/src/local_user/oauth_callback.rs @@ -0,0 +1,122 @@ +use actix_web::{ + http::StatusCode, + web::{Data, Query}, + HttpRequest, + HttpResponse, +}; +use lemmy_api_common::{ + claims::Claims, + context::LemmyContext, + external_auth::{OAuth, TokenResponse}, + utils::{create_login_cookie}, +}; +use lemmy_db_schema::{ + source::{local_site::LocalSite, registration_application::RegistrationApplication}, + utils::DbPool, + RegistrationMode, +}; +use lemmy_db_views::structs::{ExternalAuthView, LocalUserView, SiteView}; +use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType}; + +#[tracing::instrument(skip(context))] +pub async fn oauth_callback( + data: Query, + req: HttpRequest, + context: Data, +) -> Result { + let site_view = SiteView::read_local(&mut context.pool()).await?; + + if !data.state.contains("|") { + Err(LemmyErrorType::IncorrectLogin)? + } + + let stateParts = data.state.split("|"); + let client_id = stateParts.next(); + let client_redirect_uri = stateParts.next(); + + // Fetch the auth method + let external_auth = ExternalAuthView::get(&mut context.pool(), ExternalAuthId(client_id.into())) + .await + .with_lemmy_type(LemmyErrorType::IncorrectLogin)? + .external_auth; + let client_secret = ExternalAuthView::get_client_secret(&mut context.pool(), client_id) + .await + .with_lemmy_type(LemmyErrorType::IncorrectLogin)?; + + // Send token request + let response = context.client() + .post(external_auth.token_endpoint) + .form(&[ + ("grant_type", "authorization_code"), + ("code", data.code), + ("redirect_uri", req.uri), + ("client_id", client_id), + ("client_secret", client_secret), + ]) + .send() + .await?; + + // Check token response + if req.status != StatusCode::OK { + Err(LemmyErrorType::IncorrectLogin)? + } + + // Obtain access token + let access_token = response.json::().await?.access_token; + + // Make user info request + let response = context.client() + .post(external_auth.user_endpoint) + .bearer_auth(access_token) + .send() + .await?; + + // Find or create user + let email = response.json::().await?; + let local_user_view = + LocalUserView::find_by_email_or_name(&mut context.pool(), &email) + .await; + if local_user_view.is_ok() { + // Found user + check_registration_application(&local_user_view, &site_view.local_site, &mut context.pool()) + .await?; + // Check email is verified regardless of site setting, to prevent potential account theft + if !local_user_view.local_user.admin && !local_user_view.local_user.email_verified { + Err(LemmyErrorType::EmailNotVerified)? + } + } else { + // TODO register user - how to handle registration applications? show_nsfw? overriding username? + Err(LemmyErrorType::IncorrectLogin)? + } + + let jwt = Claims::generate(local_user_view.local_user.id, req, &context).await?; + + let mut res = HttpResponse::build(StatusCode::FOUND) + .insert_header(("Location", client_redirect_uri)) + .finish(); + res.add_cookie(&create_login_cookie(jwt))?; + Ok(res) +} + +async fn check_registration_application( + local_user_view: &LocalUserView, + local_site: &LocalSite, + pool: &mut DbPool<'_>, +) -> Result<(), LemmyError> { + 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?; + if registration.admin_id.is_some() { + Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))? + } else { + Err(LemmyErrorType::RegistrationApplicationIsPending)? + } + } + Ok(()) +} diff --git a/crates/api/src/site/leave_admin.rs b/crates/api/src/site/leave_admin.rs index f25747ef31..f260652ccb 100644 --- a/crates/api/src/site/leave_admin.rs +++ b/crates/api/src/site/leave_admin.rs @@ -10,7 +10,7 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_db_views::structs::{CustomEmojiView, LocalUserView, SiteView}; +use lemmy_db_views::structs::{CustomEmojiView, ExternalAuthView, LocalUserView, SiteView}; use lemmy_db_views_actor::structs::PersonView; use lemmy_utils::{ error::{LemmyError, LemmyErrorType}, @@ -59,6 +59,8 @@ 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 external_auths = + ExternalAuthView::get_all(&mut context.pool(), site_view.local_site.id).await?; Ok(Json(GetSiteResponse { site_view, @@ -69,5 +71,6 @@ pub async fn leave_admin( discussion_languages, taglines, custom_emojis, + external_auths, })) } diff --git a/crates/api_common/src/external_auth.rs b/crates/api_common/src/external_auth.rs new file mode 100644 index 0000000000..4c97dadf5a --- /dev/null +++ b/crates/api_common/src/external_auth.rs @@ -0,0 +1,96 @@ +use lemmy_db_schema::newtypes::ExternalAuthId; +use lemmy_db_views::structs::ExternalAuthView; +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 CreateExternalAuth { + pub display_name: String, + pub auth_type: String, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub auth_endpoint: Url, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub token_endpoint: Url, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub user_endpoint: Url, + pub id_attribute: String, + pub issuer: String, + pub client_id: String, + pub client_secret: String, + pub scopes: String, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// Edit an external auth method. +pub struct EditExternalAuth { + pub id: ExternalAuthId, + pub display_name: String, + pub auth_type: String, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub auth_endpoint: Url, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub token_endpoint: Url, + #[cfg_attr(feature = "full", ts(type = "string"))] + pub user_endpoint: Url, + pub id_attribute: String, + pub issuer: String, + pub client_id: String, + pub client_secret: String, + pub scopes: String, +} + +#[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 DeleteExternalAuth { + pub id: ExternalAuthId, +} + +#[derive(Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The response for deleting an external auth method. +pub struct DeleteExternalAuthResponse { + pub id: ExternalAuthId, + pub success: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// A response for an external auth method. +pub struct ExternalAuthResponse { + pub external_auth: ExternalAuthView, +} + +#[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 token +pub struct OAuth { + pub code: String, + pub state: String, +} + +#[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/lib.rs b/crates/api_common/src/lib.rs index 6f7da52eef..898656dc80 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 external_auth; pub mod person; pub mod post; pub mod private_message; diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index b047c6dd03..283d05aa0e 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -10,6 +10,7 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::{ CommentView, CustomEmojiView, + ExternalAuthView, LocalUserView, PostView, RegistrationApplicationView, @@ -285,6 +286,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 external_auths: Vec, } #[skip_serializing_none] diff --git a/crates/api_crud/src/external_auth/create.rs b/crates/api_crud/src/external_auth/create.rs new file mode 100644 index 0000000000..21cf881b09 --- /dev/null +++ b/crates/api_crud/src/external_auth/create.rs @@ -0,0 +1,42 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + external_auth::{CreateExternalAuth, ExternalAuthResponse}, + utils::is_admin, +}; +use lemmy_db_schema::source::{ + external_auth::{ExternalAuth, ExternalAuthInsertForm}, + local_site::LocalSite, +}; +use lemmy_db_views::structs::{ExternalAuthView, LocalUserView}; +use lemmy_utils::error::LemmyError; + +#[tracing::instrument(skip(context))] +pub async fn create_external_auth( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + let local_site = LocalSite::read(&mut context.pool()).await?; + // Make sure user is an admin + is_admin(&local_user_view)?; + + let clonedData = data.clone(); + let external_auth_form = ExternalAuthInsertForm::builder() + .local_site_id(local_site.id) + .display_name(clonedData.display_name.into()) + .auth_type(data.auth_type.to_string()) + .auth_endpoint(clonedData.auth_endpoint.into()) + .token_endpoint(clonedData.token_endpoint.into()) + .user_endpoint(clonedData.user_endpoint.into()) + .id_attribute(clonedData.id_attribute.into()) + .issuer(data.issuer.to_string()) + .client_id(data.client_id.to_string()) + .client_secret(data.client_secret.to_string()) + .scopes(data.scopes.to_string()) + .build(); + let external_auth = ExternalAuth::create(&mut context.pool(), &external_auth_form).await?; + let view = ExternalAuthView::get(&mut context.pool(), external_auth.id).await?; + Ok(Json(ExternalAuthResponse { external_auth: view })) +} diff --git a/crates/api_crud/src/external_auth/delete.rs b/crates/api_crud/src/external_auth/delete.rs new file mode 100644 index 0000000000..d72bf75e6f --- /dev/null +++ b/crates/api_crud/src/external_auth/delete.rs @@ -0,0 +1,25 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + external_auth::{DeleteExternalAuth, DeleteExternalAuthResponse}, + utils::is_admin, +}; +use lemmy_db_schema::source::external_auth::ExternalAuth; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyError; + +#[tracing::instrument(skip(context))] +pub async fn delete_external_auth( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + // Make sure user is an admin + is_admin(&local_user_view)?; + ExternalAuth::delete(&mut context.pool(), data.id).await?; + Ok(Json(DeleteExternalAuthResponse { + id: data.id, + success: true, + })) +} diff --git a/crates/api_crud/src/external_auth/mod.rs b/crates/api_crud/src/external_auth/mod.rs new file mode 100644 index 0000000000..fdb2f55613 --- /dev/null +++ b/crates/api_crud/src/external_auth/mod.rs @@ -0,0 +1,3 @@ +pub mod create; +pub mod delete; +pub mod update; diff --git a/crates/api_crud/src/external_auth/update.rs b/crates/api_crud/src/external_auth/update.rs new file mode 100644 index 0000000000..669e87618f --- /dev/null +++ b/crates/api_crud/src/external_auth/update.rs @@ -0,0 +1,46 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + external_auth::{ExternalAuthResponse, EditExternalAuth}, + utils::is_admin, +}; +use lemmy_db_schema::source::{ + external_auth::{ExternalAuth, ExternalAuthUpdateForm}, + local_site::LocalSite, +}; +use lemmy_db_views::structs::{ExternalAuthView, LocalUserView}; +use lemmy_utils::error::LemmyError; + +#[tracing::instrument(skip(context))] +pub async fn update_external_auth( + data: Json, + context: Data, + local_user_view: LocalUserView, +) -> Result, LemmyError> { + let local_site = LocalSite::read(&mut context.pool()).await?; + // Make sure user is an admin + is_admin(&local_user_view)?; + + let clonedData = data.clone(); + let mut external_auth_form = ExternalAuthUpdateForm::builder() + .local_site_id(local_site.id) + .display_name(clonedData.display_name.into()) + .auth_type(data.auth_type.to_string()) + .auth_endpoint(clonedData.auth_endpoint.into()) + .token_endpoint(clonedData.token_endpoint.into()) + .user_endpoint(clonedData.user_endpoint.into()) + .id_attribute(data.id_attribute.to_string()) + .issuer(data.issuer.to_string()) + .client_id(data.client_id.to_string()) + .scopes(data.scopes.to_string()); + + if data.client_secret != "" { + external_auth_form = external_auth_form.client_secret(Some(data.client_secret.to_string())); + } else { + external_auth_form = external_auth_form.client_secret(None); + } + let external_auth = ExternalAuth::update(&mut context.pool(), data.id, &external_auth_form.build()).await?; + let view = ExternalAuthView::get(&mut context.pool(), external_auth.id).await?; + Ok(Json(ExternalAuthResponse { external_auth: view })) +} diff --git a/crates/api_crud/src/lib.rs b/crates/api_crud/src/lib.rs index aee3e81345..e9529ad412 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 external_auth; pub mod post; pub mod private_message; pub mod site; diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index aceee29d49..31459c67a8 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -8,7 +8,7 @@ use lemmy_db_schema::source::{ language::Language, tagline::Tagline, }; -use lemmy_db_views::structs::{CustomEmojiView, LocalUserView, SiteView}; +use lemmy_db_views::structs::{CustomEmojiView, ExternalAuthView, LocalUserView, SiteView}; use lemmy_db_views_actor::structs::{ CommunityBlockView, CommunityFollowerView, @@ -80,6 +80,8 @@ pub async fn get_site( 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 external_auths = + ExternalAuthView::get_all(&mut context.pool(), site_view.local_site.id).await?; Ok(Json(GetSiteResponse { site_view, @@ -90,5 +92,6 @@ pub async fn get_site( discussion_languages, taglines, custom_emojis, + external_auths, })) } diff --git a/crates/db_schema/src/impls/external_auth.rs b/crates/db_schema/src/impls/external_auth.rs new file mode 100644 index 0000000000..9eb8ee582e --- /dev/null +++ b/crates/db_schema/src/impls/external_auth.rs @@ -0,0 +1,40 @@ +use crate::{ + newtypes::ExternalAuthId, + schema::{ + external_auth::dsl::external_auth, + }, + source::{ + external_auth::{ExternalAuth, ExternalAuthInsertForm, ExternalAuthUpdateForm}, + }, + utils::{get_conn, DbPool}, +}; +use diesel::{dsl::insert_into, result::Error, QueryDsl}; +use diesel_async::RunQueryDsl; + +impl ExternalAuth { + pub async fn create(pool: &mut DbPool<'_>, form: &ExternalAuthInsertForm) -> Result { + let conn = &mut get_conn(pool).await?; + insert_into(external_auth) + .values(form) + .get_result::(conn) + .await + } + pub async fn update( + pool: &mut DbPool<'_>, + external_auth_id: ExternalAuthId, + form: &ExternalAuthUpdateForm, + ) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::update(external_auth.find(external_auth_id)) + .set(form) + .get_result::(conn) + .await + } + pub async fn delete(pool: &mut DbPool<'_>, external_auth_id: ExternalAuthId) -> Result { + let conn = &mut get_conn(pool).await?; + diesel::delete(external_auth.find(external_auth_id)) + .execute(conn) + .await + } +} + \ No newline at end of file diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index 3cf0f1066c..b6b2b5783c 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -8,6 +8,7 @@ pub mod community; pub mod community_block; pub mod custom_emoji; pub mod email_verification; +pub mod external_auth; pub mod federation_allowlist; pub mod federation_blocklist; pub mod image_upload; diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs index 555b982568..3af04c8554 100644 --- a/crates/db_schema/src/newtypes.rs +++ b/crates/db_schema/src/newtypes.rs @@ -153,7 +153,7 @@ pub struct InstanceId(i32); #[cfg_attr(feature = "full", derive(DieselNewType, TS))] #[cfg_attr(feature = "full", ts(export))] /// The local site id. -pub struct LocalSiteId(i32); +pub struct LocalSiteId(pub i32); #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] #[cfg_attr(feature = "full", derive(DieselNewType, TS))] @@ -161,6 +161,12 @@ pub struct LocalSiteId(i32); /// The custom emoji id. pub struct CustomEmojiId(i32); +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "full", derive(DieselNewType, TS))] +#[cfg_attr(feature = "full", ts(export))] +/// The external auth id. +pub struct ExternalAuthId(pub i32); + #[cfg(feature = "full")] #[derive(Serialize, Deserialize)] #[serde(remote = "Ltree")] diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 6942fdccd9..a6730f223b 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -285,6 +285,26 @@ diesel::table! { } } +diesel::table! { + external_auth (id) { + id -> Int4, + local_site_id -> Int4, + display_name -> Text, + #[max_length = 128] + auth_type -> Varchar, + auth_endpoint -> Text, + token_endpoint -> Text, + user_endpoint -> Text, + id_attribute -> Text, + issuer -> Text, + client_id -> Text, + client_secret -> Text, + scopes -> Text, + published -> Timestamptz, + updated -> Nullable, + } +} + diesel::table! { federation_allowlist (id) { id -> Int4, @@ -946,6 +966,7 @@ diesel::joinable!(community_person_ban -> person (person_id)); diesel::joinable!(custom_emoji -> local_site (local_site_id)); diesel::joinable!(custom_emoji_keyword -> custom_emoji (custom_emoji_id)); diesel::joinable!(email_verification -> local_user (local_user_id)); +diesel::joinable!(external_auth -> local_site (local_site_id)); diesel::joinable!(federation_allowlist -> instance (instance_id)); diesel::joinable!(federation_blocklist -> instance (instance_id)); diesel::joinable!(federation_queue_state -> instance (instance_id)); @@ -1026,6 +1047,7 @@ diesel::allow_tables_to_appear_in_same_query!( custom_emoji, custom_emoji_keyword, email_verification, + external_auth, federation_allowlist, federation_blocklist, federation_queue_state, diff --git a/crates/db_schema/src/source/external_auth.rs b/crates/db_schema/src/source/external_auth.rs new file mode 100644 index 0000000000..1cf54fad78 --- /dev/null +++ b/crates/db_schema/src/source/external_auth.rs @@ -0,0 +1,70 @@ +use crate::newtypes::{ExternalAuthId, DbUrl, LocalSiteId}; +#[cfg(feature = "full")] +use crate::schema::external_auth; +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(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Associations, Identifiable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = external_auth))] +#[cfg_attr( + feature = "full", + diesel(belongs_to(crate::source::local_site::LocalSite)) +)] +#[cfg_attr(feature = "full", ts(export))] +/// An external auth method. +pub struct ExternalAuth { + pub id: ExternalAuthId, + pub local_site_id: LocalSiteId, + pub display_name: String, + pub auth_type: String, + pub auth_endpoint: DbUrl, + pub token_endpoint: DbUrl, + pub user_endpoint: DbUrl, + pub id_attribute: String, + pub issuer: String, + pub client_id: String, + pub client_secret: String, + pub scopes: 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 = external_auth))] +pub struct ExternalAuthInsertForm { + pub local_site_id: LocalSiteId, + pub display_name: String, + pub auth_type: String, + pub auth_endpoint: DbUrl, + pub token_endpoint: DbUrl, + pub user_endpoint: DbUrl, + pub id_attribute: String, + pub issuer: String, + pub client_id: String, + pub client_secret: String, + pub scopes: String, +} + +#[derive(Debug, Clone, TypedBuilder)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = external_auth))] +pub struct ExternalAuthUpdateForm { + pub local_site_id: LocalSiteId, + pub display_name: String, + pub auth_type: String, + pub auth_endpoint: DbUrl, + pub token_endpoint: DbUrl, + pub user_endpoint: DbUrl, + pub id_attribute: String, + pub issuer: String, + pub client_id: String, + pub client_secret: Option, + pub scopes: String, +} diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index 9879ef35f9..0972c1e179 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -13,6 +13,7 @@ pub mod community_block; pub mod custom_emoji; pub mod custom_emoji_keyword; pub mod email_verification; +pub mod external_auth; pub mod federation_allowlist; pub mod federation_blocklist; pub mod image_upload; diff --git a/crates/db_views/Cargo.toml b/crates/db_views/Cargo.toml index 69fa24403b..e3e9c29013 100644 --- a/crates/db_views/Cargo.toml +++ b/crates/db_views/Cargo.toml @@ -32,6 +32,7 @@ serde = { workspace = true } serde_with = { workspace = true } tracing = { workspace = true, optional = true } ts-rs = { workspace = true, optional = true } +url = { workspace = true } actix-web = { workspace = true, optional = true } [dev-dependencies] diff --git a/crates/db_views/src/external_auth_view.rs b/crates/db_views/src/external_auth_view.rs new file mode 100644 index 0000000000..27344ca553 --- /dev/null +++ b/crates/db_views/src/external_auth_view.rs @@ -0,0 +1,85 @@ +use crate::structs::ExternalAuthView; +use diesel::{result::Error, ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + newtypes::{ExternalAuthId, LocalSiteId}, + schema::{external_auth}, + source::external_auth::ExternalAuth, + utils::{get_conn, DbPool}, +}; + +impl ExternalAuthView { + pub async fn get(pool: &mut DbPool<'_>, external_auth_id: ExternalAuthId) -> Result { + let conn = &mut get_conn(pool).await?; + let external_auths = external_auth::table + .find(external_auth_id) + .select(external_auth::all_columns) + .load::(conn) + .await?; + if let Some(external_auth) = ExternalAuthView::from_tuple_to_vec(external_auths) + .into_iter() + .next() + { + Ok(external_auth) + } else { + Err(diesel::result::Error::NotFound) + } + } + + // client_secret is in its own function because it should never be sent to any frontends, + // and will only be needed when performing an oauth request by the server + pub async fn get_client_secret(pool: &mut DbPool<'_>, external_auth_id: ExternalAuthId) -> Result { + let conn = &mut get_conn(pool).await?; + let external_auths = external_auth::table + .find(external_auth_id) + .select(external_auth::client_secret) + .load::(conn) + .await?; + if let Some(external_auth) = external_auths.into_iter().next() { + Ok(external_auth) + } else { + Err(diesel::result::Error::NotFound) + } + } + + pub async fn get_all( + pool: &mut DbPool<'_>, + for_local_site_id: LocalSiteId, + ) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + let external_auths = external_auth::table + .filter(external_auth::local_site_id.eq(for_local_site_id)) + .order(external_auth::id) + .select(external_auth::all_columns) + .load::(conn) + .await?; + + Ok(ExternalAuthView::from_tuple_to_vec(external_auths)) + } + + fn from_tuple_to_vec(items: Vec) -> Vec { + let mut result = Vec::new(); + for item in &items { + result.push(ExternalAuthView { + // Can't just clone entire object because client_secret must be stripped + external_auth: ExternalAuth { + id: item.id.clone(), + local_site_id: item.local_site_id.clone(), + display_name: item.display_name.clone(), + auth_type: item.auth_type.clone(), + auth_endpoint: item.auth_endpoint.clone(), + token_endpoint: item.token_endpoint.clone(), + user_endpoint: item.user_endpoint.clone(), + id_attribute: item.id_attribute.clone(), + issuer: item.issuer.clone(), + client_id: item.client_id.clone(), + client_secret: String::new(), + scopes: item.scopes.clone(), + published: item.published.clone(), + updated: item.updated.clone(), + }, + }); + } + result + } +} diff --git a/crates/db_views/src/lib.rs b/crates/db_views/src/lib.rs index 8abf776ba5..4b23544f59 100644 --- a/crates/db_views/src/lib.rs +++ b/crates/db_views/src/lib.rs @@ -8,6 +8,8 @@ pub mod comment_view; #[cfg(feature = "full")] pub mod custom_emoji_view; #[cfg(feature = "full")] +pub mod external_auth_view; +#[cfg(feature = "full")] pub mod local_user_view; #[cfg(feature = "full")] pub mod post_report_view; diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 10847b0538..3e17713d66 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -8,6 +8,7 @@ use lemmy_db_schema::{ community::Community, custom_emoji::CustomEmoji, custom_emoji_keyword::CustomEmojiKeyword, + external_auth::ExternalAuth, local_site::LocalSite, local_site_rate_limit::LocalSiteRateLimit, local_user::LocalUser, @@ -170,3 +171,11 @@ pub struct CustomEmojiView { pub custom_emoji: CustomEmoji, pub keywords: Vec, } + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// An external auth view. +pub struct ExternalAuthView { + pub external_auth: ExternalAuth, +} diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index a61f259730..32819dfde4 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -55,14 +55,14 @@ services: lemmy-ui: # use "image" to pull down an already compiled lemmy-ui. make sure to comment out "build". - image: dessalines/lemmy-ui:0.18.4 + #image: dessalines/lemmy-ui:0.18.4 # platform: linux/x86_64 # no arm64 support. uncomment platform if using m1. # use "build" to build your local lemmy ui image for development. make sure to comment out "image". # run: docker compose up --build - # build: - # context: ../../lemmy-ui # assuming lemmy-ui is cloned besides lemmy directory - # dockerfile: dev.dockerfile + build: + context: ../../lemmy-ui # assuming lemmy-ui is cloned besides lemmy directory + dockerfile: dev.dockerfile environment: # this needs to match the hostname defined in the lemmy service - LEMMY_UI_LEMMY_INTERNAL_HOST=lemmy:8536 diff --git a/migrations/2023-11-03-131721_create_external_auth/down.sql b/migrations/2023-11-03-131721_create_external_auth/down.sql new file mode 100644 index 0000000000..d619666427 --- /dev/null +++ b/migrations/2023-11-03-131721_create_external_auth/down.sql @@ -0,0 +1 @@ +DROP TABLE external_auth; diff --git a/migrations/2023-11-03-131721_create_external_auth/up.sql b/migrations/2023-11-03-131721_create_external_auth/up.sql new file mode 100644 index 0000000000..fdf4ac3f87 --- /dev/null +++ b/migrations/2023-11-03-131721_create_external_auth/up.sql @@ -0,0 +1,17 @@ +CREATE TABLE external_auth ( + id serial PRIMARY KEY, + local_site_id int REFERENCES local_site ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, + display_name text NOT NULL, + auth_type varchar(128) NOT NULL UNIQUE, + auth_endpoint text NOT NULL, + token_endpoint text NOT NULL, + user_endpoint text NOT NULL, + id_attribute text NOT NULL, + issuer text NOT NULL, + client_id text NOT NULL UNIQUE, + client_secret text NOT NULL, + scopes text NOT NULL, + published timestamp without time zone DEFAULT now() NOT NULL, + updated timestamp without time zone +); + diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 3546b34000..bc52754e6f 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -34,6 +34,7 @@ use lemmy_api::{ mark_reply_read::mark_reply_as_read, unread_count::unread_count, }, + oauth_callback::oauth_callback, report_count::report_count, reset_password::reset_password, save_settings::save_user_settings, @@ -98,6 +99,11 @@ use lemmy_api_crud::{ delete::delete_custom_emoji, update::update_custom_emoji, }, + external_auth::{ + create::create_external_auth, + delete::delete_external_auth, + update::update_external_auth, + }, post::{ create::create_post, delete::delete_post, @@ -335,6 +341,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("/external_auth") + .wrap(rate_limit.message()) + .route("", web::post().to(create_external_auth)) + .route("", web::put().to(update_external_auth)) + .route("/delete", web::post().to(delete_external_auth)), + ) + .service( + web::scope("/oauth") + .wrap(rate_limit.message()) + .route("/callback", web::get().to(oauth_callback)), ), ); cfg.service( From 6980430ca0085de860bf7b6c58c8af9945022d95 Mon Sep 17 00:00:00 2001 From: Anthony Lawn Date: Thu, 16 Nov 2023 13:20:00 -0600 Subject: [PATCH 02/18] Implemented logging in via OAuth --- crates/api/Cargo.toml | 1 + crates/api/src/local_user/change_password.rs | 2 +- crates/api/src/local_user/login.rs | 2 +- crates/api/src/local_user/oauth_callback.rs | 183 +++++++++++------- crates/api_common/src/external_auth.rs | 29 +-- crates/api_crud/src/external_auth/create.rs | 12 +- crates/api_crud/src/external_auth/update.rs | 22 +-- crates/api_crud/src/user/create.rs | 56 +++++- crates/api_crud/src/user/delete.rs | 2 +- crates/db_schema/src/source/external_auth.rs | 20 +- .../up.sql | 2 +- 11 files changed, 214 insertions(+), 117 deletions(-) diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index a9c08a70d1..08dd040a64 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -20,6 +20,7 @@ lemmy_db_views = { workspace = true, features = ["full"] } lemmy_db_views_moderator = { workspace = true, features = ["full"] } lemmy_db_views_actor = { workspace = true, features = ["full"] } lemmy_api_common = { workspace = true, features = ["full"] } +lemmy_api_crud = { workspace = true } activitypub_federation = { workspace = true } bcrypt = { workspace = true } serde = { workspace = true } diff --git a/crates/api/src/local_user/change_password.rs b/crates/api/src/local_user/change_password.rs index ab5b32dd95..1391a96bf2 100644 --- a/crates/api/src/local_user/change_password.rs +++ b/crates/api/src/local_user/change_password.rs @@ -28,7 +28,7 @@ pub async fn change_password( } // Check the old password - let valid: bool = verify( + let valid: bool = local_user_view.local_user.password_encrypted == "" || verify( &data.old_password, &local_user_view.local_user.password_encrypted, ) diff --git a/crates/api/src/local_user/login.rs b/crates/api/src/local_user/login.rs index f57fd0a70c..f8ff134a1f 100644 --- a/crates/api/src/local_user/login.rs +++ b/crates/api/src/local_user/login.rs @@ -36,7 +36,7 @@ pub async fn login( .with_lemmy_type(LemmyErrorType::IncorrectLogin)?; // Verify the password - let valid: bool = verify( + let valid: bool = local_user_view.local_user.password_encrypted != "" && verify( &data.password, &local_user_view.local_user.password_encrypted, ) diff --git a/crates/api/src/local_user/oauth_callback.rs b/crates/api/src/local_user/oauth_callback.rs index 4596ee6fdb..a858b83c1c 100644 --- a/crates/api/src/local_user/oauth_callback.rs +++ b/crates/api/src/local_user/oauth_callback.rs @@ -1,122 +1,161 @@ +use activitypub_federation::{config::Data}; use actix_web::{ http::StatusCode, - web::{Data, Query}, + web::Query, HttpRequest, HttpResponse, }; use lemmy_api_common::{ claims::Claims, context::LemmyContext, - external_auth::{OAuth, TokenResponse}, + external_auth::{OAuth, OAuthResponse, TokenResponse}, utils::{create_login_cookie}, }; +use lemmy_api_crud::user::create::register_from_oauth; use lemmy_db_schema::{ - source::{local_site::LocalSite, registration_application::RegistrationApplication}, - utils::DbPool, + newtypes::ExternalAuthId, RegistrationMode, + source::local_user::LocalUser, }; use lemmy_db_views::structs::{ExternalAuthView, LocalUserView, SiteView}; -use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType}; +use url::Url; #[tracing::instrument(skip(context))] pub async fn oauth_callback( data: Query, req: HttpRequest, context: Data, -) -> Result { - let site_view = SiteView::read_local(&mut context.pool()).await?; +) -> HttpResponse { + let site_view = SiteView::read_local(&mut context.pool()).await; - if !data.state.contains("|") { - Err(LemmyErrorType::IncorrectLogin)? + if !site_view.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=internal")).finish(); } - let stateParts = data.state.split("|"); - let client_id = stateParts.next(); - let client_redirect_uri = stateParts.next(); + let state = serde_json::from_str::(&data.state); + if !state.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=oauth_response")).finish(); + } + let oauth_state = state.unwrap(); // Fetch the auth method - let external_auth = ExternalAuthView::get(&mut context.pool(), ExternalAuthId(client_id.into())) - .await - .with_lemmy_type(LemmyErrorType::IncorrectLogin)? - .external_auth; - let client_secret = ExternalAuthView::get_client_secret(&mut context.pool(), client_id) - .await - .with_lemmy_type(LemmyErrorType::IncorrectLogin)?; + let external_auth_id = ExternalAuthId(oauth_state.external_auth); + let external_auth_view = ExternalAuthView::get(&mut context.pool(), external_auth_id).await; + if !external_auth_view.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); + } + let external_auth = external_auth_view.unwrap().external_auth; + let client_secret = ExternalAuthView::get_client_secret(&mut context.pool(), external_auth_id) + .await; + if !client_secret.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); + } // Send token request - let response = context.client() - .post(external_auth.token_endpoint) + let token_endpoint = Url::parse(&external_auth.token_endpoint); + if !token_endpoint.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); + } + let mut response = context.client() + .post(token_endpoint.unwrap()) .form(&[ ("grant_type", "authorization_code"), - ("code", data.code), - ("redirect_uri", req.uri), - ("client_id", client_id), - ("client_secret", client_secret), + ("code", &data.code), + ("redirect_uri", &req.uri().to_string()), + ("client_id", &external_auth.client_id), + ("client_secret", &client_secret.unwrap()), ]) .send() - .await?; - - // Check token response - if req.status != StatusCode::OK { - Err(LemmyErrorType::IncorrectLogin)? + .await; + if !response.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=token")).finish(); + } + let mut res = response.unwrap(); + if res.status() != StatusCode::OK { + return HttpResponse::Found().append_header(("Location", "/login?err=token")).finish(); } // Obtain access token - let access_token = response.json::().await?.access_token; + let token_response = res.json::().await; + if !token_response.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=token")).finish(); + } + let access_token = token_response.unwrap().access_token; // Make user info request - let response = context.client() - .post(external_auth.user_endpoint) + let user_endpoint = Url::parse(&external_auth.user_endpoint); + if !user_endpoint.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); + } + response = context.client() + .post(user_endpoint.unwrap()) .bearer_auth(access_token) .send() - .await?; + .await; + if !response.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=userinfo")).finish(); + } + res = response.unwrap(); + if res.status() != StatusCode::OK { + return HttpResponse::Found().append_header(("Location", "/login?err=userinfo")).finish(); + } // Find or create user - let email = response.json::().await?; + let userinfo = res.json::().await; + if !userinfo.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); + } + let user_info = userinfo.unwrap(); + let user_id = serde_json::from_value::(user_info["email"].clone()); + if !user_id.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=user")).finish(); + } + let email = user_id.unwrap(); + let local_user_view = - LocalUserView::find_by_email_or_name(&mut context.pool(), &email) - .await; + LocalUserView::find_by_email(&mut context.pool(), &email).await; + let local_site = site_view.unwrap().local_site; + let local_user: LocalUser; if local_user_view.is_ok() { - // Found user - check_registration_application(&local_user_view, &site_view.local_site, &mut context.pool()) - .await?; - // Check email is verified regardless of site setting, to prevent potential account theft - if !local_user_view.local_user.admin && !local_user_view.local_user.email_verified { - Err(LemmyErrorType::EmailNotVerified)? - } + local_user = local_user_view.unwrap().local_user; } else { - // TODO register user - how to handle registration applications? show_nsfw? overriding username? - Err(LemmyErrorType::IncorrectLogin)? + let username = serde_json::from_value::(user_info[external_auth.id_attribute] + .clone()); + if !username.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); + } + let registered_user = register_from_oauth(username.unwrap(), email, &context).await; + if !registered_user.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=user")).finish(); + } + local_user = registered_user.unwrap(); + + // if registration is not allowed + // return HttpResponse::Found().append_header(("Location", "/signup")).finish(); + } + + if (local_site.registration_mode == RegistrationMode::RequireApplication + || local_site.registration_mode == RegistrationMode::Closed) + && !local_user.accepted_application + && !local_user.admin { + return HttpResponse::Found().append_header(("Location", "/login?err=application")).finish(); + } + + // Check email is verified regardless of site setting, to prevent potential account theft + if !local_user.admin && !local_user.email_verified { + return HttpResponse::Found().append_header(("Location", "/login?err=email")).finish(); } - let jwt = Claims::generate(local_user_view.local_user.id, req, &context).await?; + let jwt = Claims::generate(local_user.id, req, &context).await; + if !jwt.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=jwt")).finish(); + } let mut res = HttpResponse::build(StatusCode::FOUND) - .insert_header(("Location", client_redirect_uri)) + .insert_header(("Location", oauth_state.client_redirect_uri)) .finish(); - res.add_cookie(&create_login_cookie(jwt))?; - Ok(res) -} - -async fn check_registration_application( - local_user_view: &LocalUserView, - local_site: &LocalSite, - pool: &mut DbPool<'_>, -) -> Result<(), LemmyError> { - 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?; - if registration.admin_id.is_some() { - Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))? - } else { - Err(LemmyErrorType::RegistrationApplicationIsPending)? - } + if !res.add_cookie(&create_login_cookie(jwt.unwrap())).is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=jwt")).finish(); } - Ok(()) + return res; } diff --git a/crates/api_common/src/external_auth.rs b/crates/api_common/src/external_auth.rs index 4c97dadf5a..c0c5d06146 100644 --- a/crates/api_common/src/external_auth.rs +++ b/crates/api_common/src/external_auth.rs @@ -4,7 +4,6 @@ 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))] @@ -13,12 +12,9 @@ use url::Url; pub struct CreateExternalAuth { pub display_name: String, pub auth_type: String, - #[cfg_attr(feature = "full", ts(type = "string"))] - pub auth_endpoint: Url, - #[cfg_attr(feature = "full", ts(type = "string"))] - pub token_endpoint: Url, - #[cfg_attr(feature = "full", ts(type = "string"))] - pub user_endpoint: Url, + pub auth_endpoint: String, + pub token_endpoint: String, + pub user_endpoint: String, pub id_attribute: String, pub issuer: String, pub client_id: String, @@ -34,12 +30,9 @@ pub struct EditExternalAuth { pub id: ExternalAuthId, pub display_name: String, pub auth_type: String, - #[cfg_attr(feature = "full", ts(type = "string"))] - pub auth_endpoint: Url, - #[cfg_attr(feature = "full", ts(type = "string"))] - pub token_endpoint: Url, - #[cfg_attr(feature = "full", ts(type = "string"))] - pub user_endpoint: Url, + pub auth_endpoint: String, + pub token_endpoint: String, + pub user_endpoint: String, pub id_attribute: String, pub issuer: String, pub client_id: String, @@ -94,3 +87,13 @@ pub struct TokenResponse { pub refresh_token: Option, pub scope: Option, } + +#[skip_serializing_none] +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +/// State parameter from the auth endpoint response +pub struct OAuthResponse { + pub external_auth: i32, + pub client_redirect_uri: String, +} diff --git a/crates/api_crud/src/external_auth/create.rs b/crates/api_crud/src/external_auth/create.rs index 21cf881b09..fc10435864 100644 --- a/crates/api_crud/src/external_auth/create.rs +++ b/crates/api_crud/src/external_auth/create.rs @@ -22,15 +22,15 @@ pub async fn create_external_auth( // Make sure user is an admin is_admin(&local_user_view)?; - let clonedData = data.clone(); + let cloned_data = data.clone(); let external_auth_form = ExternalAuthInsertForm::builder() .local_site_id(local_site.id) - .display_name(clonedData.display_name.into()) + .display_name(cloned_data.display_name.into()) .auth_type(data.auth_type.to_string()) - .auth_endpoint(clonedData.auth_endpoint.into()) - .token_endpoint(clonedData.token_endpoint.into()) - .user_endpoint(clonedData.user_endpoint.into()) - .id_attribute(clonedData.id_attribute.into()) + .auth_endpoint(cloned_data.auth_endpoint.into()) + .token_endpoint(cloned_data.token_endpoint.into()) + .user_endpoint(cloned_data.user_endpoint.into()) + .id_attribute(cloned_data.id_attribute.into()) .issuer(data.issuer.to_string()) .client_id(data.client_id.to_string()) .client_secret(data.client_secret.to_string()) diff --git a/crates/api_crud/src/external_auth/update.rs b/crates/api_crud/src/external_auth/update.rs index 669e87618f..56d423251d 100644 --- a/crates/api_crud/src/external_auth/update.rs +++ b/crates/api_crud/src/external_auth/update.rs @@ -22,24 +22,24 @@ pub async fn update_external_auth( // Make sure user is an admin is_admin(&local_user_view)?; - let clonedData = data.clone(); - let mut external_auth_form = ExternalAuthUpdateForm::builder() + let cloned_data = data.clone(); + let external_auth_form = ExternalAuthUpdateForm::builder() .local_site_id(local_site.id) - .display_name(clonedData.display_name.into()) + .display_name(cloned_data.display_name.into()) .auth_type(data.auth_type.to_string()) - .auth_endpoint(clonedData.auth_endpoint.into()) - .token_endpoint(clonedData.token_endpoint.into()) - .user_endpoint(clonedData.user_endpoint.into()) + .auth_endpoint(cloned_data.auth_endpoint.into()) + .token_endpoint(cloned_data.token_endpoint.into()) + .user_endpoint(cloned_data.user_endpoint.into()) .id_attribute(data.id_attribute.to_string()) .issuer(data.issuer.to_string()) .client_id(data.client_id.to_string()) + .client_secret(if data.client_secret != "" { + Some(data.client_secret.to_string()) + } else { + None + }) .scopes(data.scopes.to_string()); - if data.client_secret != "" { - external_auth_form = external_auth_form.client_secret(Some(data.client_secret.to_string())); - } else { - external_auth_form = external_auth_form.client_secret(None); - } let external_auth = ExternalAuth::update(&mut context.pool(), data.id, &external_auth_form.build()).await?; let view = ExternalAuthView::get(&mut context.pool(), external_auth.id).await?; Ok(Json(ExternalAuthResponse { external_auth: view })) diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index 4a326a3acb..80e7db2172 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -37,7 +37,6 @@ use lemmy_utils::{ }, }; -#[tracing::instrument(skip(context))] pub async fn register( data: Json, req: HttpRequest, @@ -203,3 +202,58 @@ pub async fn register( Ok(res.json(login_response)) } + +#[tracing::instrument(skip(context))] +pub async fn register_from_oauth( + username: String, + email: String, + context: &Data, +) -> Result { + let site_view = SiteView::read_local(&mut context.pool()).await?; + let local_site = site_view.local_site; + + let slur_regex = local_site_to_slur_regex(&local_site); + check_slurs(&username, &slur_regex)?; + + 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(), + )?; + + // We have to create both a person, and local_user + + // Register the new person + let person_form = PersonInsertForm::builder() + .name(username.clone()) + .actor_id(Some(actor_id.clone())) + .private_key(Some(actor_keypair.private_key)) + .public_key(actor_keypair.public_key) + .inbox_url(Some(generate_inbox_url(&actor_id)?)) + .shared_inbox_url(Some(generate_shared_inbox_url(&actor_id)?)) + .instance_id(site_view.site.instance_id) + .build(); + + // insert the person + let inserted_person = Person::create(&mut context.pool(), &person_form) + .await + .with_lemmy_type(LemmyErrorType::UserAlreadyExists)?; + + // Create the local user + let local_user_form = LocalUserInsertForm::builder() + .person_id(inserted_person.id) + .email(Some(str::to_lowercase(&email))) + .password_encrypted("".to_string()) + .show_nsfw(Some(false)) + .accepted_application(Some(true)) + .email_verified(Some(true)) + .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(); + + let inserted_local_user = LocalUser::create(&mut context.pool(), &local_user_form).await?; + Ok(inserted_local_user) +} diff --git a/crates/api_crud/src/user/delete.rs b/crates/api_crud/src/user/delete.rs index 363230d836..342b035b2b 100644 --- a/crates/api_crud/src/user/delete.rs +++ b/crates/api_crud/src/user/delete.rs @@ -19,7 +19,7 @@ pub async fn delete_account( local_user_view: LocalUserView, ) -> LemmyResult> { // Verify the password - let valid: bool = verify( + let valid: bool = local_user_view.local_user.password_encrypted == "" || verify( &data.password, &local_user_view.local_user.password_encrypted, ) diff --git a/crates/db_schema/src/source/external_auth.rs b/crates/db_schema/src/source/external_auth.rs index 1cf54fad78..7886d0cbca 100644 --- a/crates/db_schema/src/source/external_auth.rs +++ b/crates/db_schema/src/source/external_auth.rs @@ -1,4 +1,4 @@ -use crate::newtypes::{ExternalAuthId, DbUrl, LocalSiteId}; +use crate::newtypes::{ExternalAuthId, LocalSiteId}; #[cfg(feature = "full")] use crate::schema::external_auth; use chrono::{DateTime, Utc}; @@ -23,9 +23,9 @@ pub struct ExternalAuth { pub local_site_id: LocalSiteId, pub display_name: String, pub auth_type: String, - pub auth_endpoint: DbUrl, - pub token_endpoint: DbUrl, - pub user_endpoint: DbUrl, + pub auth_endpoint: String, + pub token_endpoint: String, + pub user_endpoint: String, pub id_attribute: String, pub issuer: String, pub client_id: String, @@ -42,9 +42,9 @@ pub struct ExternalAuthInsertForm { pub local_site_id: LocalSiteId, pub display_name: String, pub auth_type: String, - pub auth_endpoint: DbUrl, - pub token_endpoint: DbUrl, - pub user_endpoint: DbUrl, + pub auth_endpoint: String, + pub token_endpoint: String, + pub user_endpoint: String, pub id_attribute: String, pub issuer: String, pub client_id: String, @@ -59,9 +59,9 @@ pub struct ExternalAuthUpdateForm { pub local_site_id: LocalSiteId, pub display_name: String, pub auth_type: String, - pub auth_endpoint: DbUrl, - pub token_endpoint: DbUrl, - pub user_endpoint: DbUrl, + pub auth_endpoint: String, + pub token_endpoint: String, + pub user_endpoint: String, pub id_attribute: String, pub issuer: String, pub client_id: String, diff --git a/migrations/2023-11-03-131721_create_external_auth/up.sql b/migrations/2023-11-03-131721_create_external_auth/up.sql index fdf4ac3f87..0cf1140829 100644 --- a/migrations/2023-11-03-131721_create_external_auth/up.sql +++ b/migrations/2023-11-03-131721_create_external_auth/up.sql @@ -2,7 +2,7 @@ CREATE TABLE external_auth ( id serial PRIMARY KEY, local_site_id int REFERENCES local_site ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, display_name text NOT NULL, - auth_type varchar(128) NOT NULL UNIQUE, + auth_type varchar(128) NOT NULL, auth_endpoint text NOT NULL, token_endpoint text NOT NULL, user_endpoint text NOT NULL, From 70b3b36bc345777a5bfcc2883fd53f7ae5340b55 Mon Sep 17 00:00:00 2001 From: Anthony Lawn Date: Thu, 7 Dec 2023 02:57:32 -0600 Subject: [PATCH 03/18] Implemented OIDC auth type --- crates/api/src/local_user/oauth_callback.rs | 47 +++++++++++++++++---- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/crates/api/src/local_user/oauth_callback.rs b/crates/api/src/local_user/oauth_callback.rs index a858b83c1c..c776b947bc 100644 --- a/crates/api/src/local_user/oauth_callback.rs +++ b/crates/api/src/local_user/oauth_callback.rs @@ -51,11 +51,46 @@ pub async fn oauth_callback( return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); } - // Send token request - let token_endpoint = Url::parse(&external_auth.token_endpoint); - if !token_endpoint.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); + // Get endpoints + let token_endpoint; + let user_endpoint; + if external_auth.auth_type == "oidc" { + let discovery_endpoint = + if external_auth.issuer.ends_with("/.well-known/openid-configuration") { + external_auth.issuer.to_string() + } else { + format!("{}/.well-known/openid-configuration", external_auth.issuer) + }; + let res = context.client().get(discovery_endpoint).send().await; + if !res.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=external_auth1")).finish(); + } + let oidc_response = res.unwrap().json::().await; + if !oidc_response.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=external_auth2")).finish(); + } + let oidc_information = oidc_response.unwrap(); + let token_endpoint_string = + serde_json::from_value::(oidc_information["token_endpoint"].clone()); + if !token_endpoint_string.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=external_auth3")).finish(); + } + token_endpoint = Url::parse(&token_endpoint_string.unwrap()); + let user_endpoint_string = + serde_json::from_value::(oidc_information["userinfo_endpoint"].clone()); + if !user_endpoint_string.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=external_auth4")).finish(); + } + user_endpoint = Url::parse(&user_endpoint_string.unwrap()); + } else { + token_endpoint = Url::parse(&external_auth.token_endpoint); + user_endpoint = Url::parse(&external_auth.user_endpoint); + }; + if !token_endpoint.is_ok() || !user_endpoint.is_ok() { + return HttpResponse::Found().append_header(("Location", "/login?err=external_auth5")).finish(); } + + // Send token request let mut response = context.client() .post(token_endpoint.unwrap()) .form(&[ @@ -83,10 +118,6 @@ pub async fn oauth_callback( let access_token = token_response.unwrap().access_token; // Make user info request - let user_endpoint = Url::parse(&external_auth.user_endpoint); - if !user_endpoint.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); - } response = context.client() .post(user_endpoint.unwrap()) .bearer_auth(access_token) From 1b991a4f9622bd3d80caf2ed92be58c2fbe656bc Mon Sep 17 00:00:00 2001 From: Anthony Lawn Date: Fri, 8 Dec 2023 10:26:24 -0600 Subject: [PATCH 04/18] Added oauth registration setting and fixed oauth issues --- crates/api/src/local_user/oauth_callback.rs | 24 +++++++++++-------- crates/api_common/src/site.rs | 3 +++ crates/api_crud/src/site/create.rs | 3 +++ crates/api_crud/src/site/update.rs | 3 +++ crates/db_schema/src/schema.rs | 1 + crates/db_schema/src/source/local_site.rs | 4 ++++ .../down.sql | 3 +++ .../up.sql | 3 +++ 8 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 migrations/2023-12-07-200127_add_oauth_registration/down.sql create mode 100644 migrations/2023-12-07-200127_add_oauth_registration/up.sql diff --git a/crates/api/src/local_user/oauth_callback.rs b/crates/api/src/local_user/oauth_callback.rs index c776b947bc..6b04bf2984 100644 --- a/crates/api/src/local_user/oauth_callback.rs +++ b/crates/api/src/local_user/oauth_callback.rs @@ -63,23 +63,23 @@ pub async fn oauth_callback( }; let res = context.client().get(discovery_endpoint).send().await; if !res.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=external_auth1")).finish(); + return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); } let oidc_response = res.unwrap().json::().await; if !oidc_response.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=external_auth2")).finish(); + return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); } let oidc_information = oidc_response.unwrap(); let token_endpoint_string = serde_json::from_value::(oidc_information["token_endpoint"].clone()); if !token_endpoint_string.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=external_auth3")).finish(); + return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); } token_endpoint = Url::parse(&token_endpoint_string.unwrap()); let user_endpoint_string = serde_json::from_value::(oidc_information["userinfo_endpoint"].clone()); if !user_endpoint_string.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=external_auth4")).finish(); + return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); } user_endpoint = Url::parse(&user_endpoint_string.unwrap()); } else { @@ -149,20 +149,21 @@ pub async fn oauth_callback( let local_user: LocalUser; if local_user_view.is_ok() { local_user = local_user_view.unwrap().local_user; - } else { + } else if local_site.oauth_registration { let username = serde_json::from_value::(user_info[external_auth.id_attribute] .clone()); if !username.is_ok() { return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); } - let registered_user = register_from_oauth(username.unwrap(), email, &context).await; + let user = str::replace(&username.unwrap(), " ", "_"); + let registered_user = register_from_oauth(user, email, &context).await; if !registered_user.is_ok() { + tracing::error!("Failed to create user: {}", registered_user.err().unwrap()); return HttpResponse::Found().append_header(("Location", "/login?err=user")).finish(); } local_user = registered_user.unwrap(); - - // if registration is not allowed - // return HttpResponse::Found().append_header(("Location", "/signup")).finish(); + } else { + return HttpResponse::Found().append_header(("Location", "/signup")).finish(); } if (local_site.registration_mode == RegistrationMode::RequireApplication @@ -185,7 +186,10 @@ pub async fn oauth_callback( let mut res = HttpResponse::build(StatusCode::FOUND) .insert_header(("Location", oauth_state.client_redirect_uri)) .finish(); - if !res.add_cookie(&create_login_cookie(jwt.unwrap())).is_ok() { + let mut cookie = create_login_cookie(jwt.unwrap()); + cookie.set_path("/"); + cookie.set_http_only(false); // We'll need to access the cookie via document.cookie for this req + if !res.add_cookie(&cookie).is_ok() { return HttpResponse::Found().append_header(("Location", "/login?err=jwt")).finish(); } return res; diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 283d05aa0e..e3ce6a12e8 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -181,6 +181,7 @@ pub struct CreateSite { pub blocked_instances: Option>, pub taglines: Option>, pub registration_mode: Option, + pub oauth_registration: Option, } #[skip_serializing_none] @@ -257,6 +258,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, } diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs index 61dfd7c77b..807cc9d1ca 100644 --- a/crates/api_crud/src/site/create.rs +++ b/crates/api_crud/src/site/create.rs @@ -89,6 +89,7 @@ pub async fn create_site( federation_enabled: data.federation_enabled, captcha_enabled: data.captcha_enabled, captcha_difficulty: data.captcha_difficulty.clone(), + oauth_registration: data.oauth_registration, ..Default::default() }; @@ -517,6 +518,7 @@ mod tests { published: Default::default(), updated: None, registration_mode: site_registration_mode, + oauth_registration: false, reports_email_admins: false, } } @@ -575,6 +577,7 @@ mod tests { blocked_instances: None, taglines: None, registration_mode: site_registration_mode, + oauth_registration: None, } } } diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index 3afc795599..30a7ad6d59 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -89,6 +89,7 @@ pub async fn update_site( federation_enabled: data.federation_enabled, captcha_enabled: data.captcha_enabled, captcha_difficulty: data.captcha_difficulty.clone(), + oauth_registration: data.oauth_registration, reports_email_admins: data.reports_email_admins, ..Default::default() }; @@ -516,6 +517,7 @@ mod tests { published: Default::default(), updated: None, registration_mode: site_registration_mode, + oauth_registration: false, reports_email_admins: false, } } @@ -574,6 +576,7 @@ mod tests { blocked_instances: None, taglines: None, registration_mode: site_registration_mode, + oauth_registration: None, reports_email_admins: None, } } diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index a6730f223b..8b67a0a6ce 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -404,6 +404,7 @@ diesel::table! { published -> Timestamptz, updated -> Nullable, registration_mode -> RegistrationModeEnum, + oauth_registration -> Bool, reports_email_admins -> Bool, } } diff --git a/crates/db_schema/src/source/local_site.rs b/crates/db_schema/src/source/local_site.rs index e5945e86fd..ac363bb4e2 100644 --- a/crates/db_schema/src/source/local_site.rs +++ b/crates/db_schema/src/source/local_site.rs @@ -58,6 +58,8 @@ pub struct LocalSite { pub published: DateTime, pub updated: Option>, pub registration_mode: RegistrationMode, + /// Whether or not external auth methods can auto-register users. + pub oauth_registration: bool, /// Whether to email admins on new reports. pub reports_email_admins: bool, } @@ -87,6 +89,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, } @@ -112,6 +115,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>>, } diff --git a/migrations/2023-12-07-200127_add_oauth_registration/down.sql b/migrations/2023-12-07-200127_add_oauth_registration/down.sql new file mode 100644 index 0000000000..8caef3c015 --- /dev/null +++ b/migrations/2023-12-07-200127_add_oauth_registration/down.sql @@ -0,0 +1,3 @@ +ALTER TABLE local_site + DROP COLUMN oauth_registration; + diff --git a/migrations/2023-12-07-200127_add_oauth_registration/up.sql b/migrations/2023-12-07-200127_add_oauth_registration/up.sql new file mode 100644 index 0000000000..e8681638a7 --- /dev/null +++ b/migrations/2023-12-07-200127_add_oauth_registration/up.sql @@ -0,0 +1,3 @@ +ALTER TABLE local_site + ADD COLUMN oauth_registration boolean DEFAULT false NOT NULL; + From 2e1ef5c976a5d79c94994569a82aa48f31f42483 Mon Sep 17 00:00:00 2001 From: Anthony Lawn Date: Fri, 8 Dec 2023 11:20:14 -0600 Subject: [PATCH 05/18] Remove DeleteExternalAuthResponse --- crates/api_common/src/external_auth.rs | 9 --------- crates/api_crud/src/external_auth/delete.rs | 10 ++++------ 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/crates/api_common/src/external_auth.rs b/crates/api_common/src/external_auth.rs index c0c5d06146..1dfe76cc80 100644 --- a/crates/api_common/src/external_auth.rs +++ b/crates/api_common/src/external_auth.rs @@ -48,15 +48,6 @@ pub struct DeleteExternalAuth { pub id: ExternalAuthId, } -#[derive(Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// The response for deleting an external auth method. -pub struct DeleteExternalAuthResponse { - pub id: ExternalAuthId, - pub success: bool, -} - #[derive(Debug, Serialize, Deserialize, Clone)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] diff --git a/crates/api_crud/src/external_auth/delete.rs b/crates/api_crud/src/external_auth/delete.rs index d72bf75e6f..f6c77b7f98 100644 --- a/crates/api_crud/src/external_auth/delete.rs +++ b/crates/api_crud/src/external_auth/delete.rs @@ -2,8 +2,9 @@ use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ context::LemmyContext, - external_auth::{DeleteExternalAuth, DeleteExternalAuthResponse}, + external_auth::DeleteExternalAuth, utils::is_admin, + SuccessResponse, }; use lemmy_db_schema::source::external_auth::ExternalAuth; use lemmy_db_views::structs::LocalUserView; @@ -14,12 +15,9 @@ pub async fn delete_external_auth( data: Json, context: Data, local_user_view: LocalUserView, -) -> Result, LemmyError> { +) -> Result, LemmyError> { // Make sure user is an admin is_admin(&local_user_view)?; ExternalAuth::delete(&mut context.pool(), data.id).await?; - Ok(Json(DeleteExternalAuthResponse { - id: data.id, - success: true, - })) + Ok(Json(SuccessResponse::default())) } From 19b33c746aba22904424cefddbd26542fcd3d0c5 Mon Sep 17 00:00:00 2001 From: Anthony Lawn Date: Fri, 8 Dec 2023 13:26:41 -0600 Subject: [PATCH 06/18] Linted --- crates/api/Cargo.toml | 1 - crates/api/src/lib.rs | 171 ---- crates/api/src/local_user/change_password.rs | 11 +- crates/api/src/local_user/login.rs | 11 +- crates/api/src/local_user/oauth_callback.rs | 154 +-- crates/api_common/src/utils.rs | 883 ------------------ crates/api_crud/src/external_auth/create.rs | 4 +- crates/api_crud/src/external_auth/update.rs | 9 +- crates/api_crud/src/user/delete.rs | 11 +- crates/db_schema/src/impls/external_auth.rs | 14 +- crates/db_views/Cargo.toml | 1 - crates/db_views/src/external_auth_view.rs | 7 +- docker/docker-compose.yml | 6 +- .../up.sql | 2 +- 14 files changed, 139 insertions(+), 1146 deletions(-) delete mode 100644 crates/api/src/lib.rs delete mode 100644 crates/api_common/src/utils.rs diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml index 9986731b61..075c2e25d7 100644 --- a/crates/api/Cargo.toml +++ b/crates/api/Cargo.toml @@ -26,7 +26,6 @@ lemmy_api_common = { workspace = true, features = ["full"] } lemmy_api_crud = { workspace = true } activitypub_federation = { workspace = true } bcrypt = { workspace = true } -serde = { workspace = true } serde_json = { workspace = true } actix-web = { workspace = true } base64 = { workspace = true } diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs deleted file mode 100644 index faa74824ec..0000000000 --- a/crates/api/src/lib.rs +++ /dev/null @@ -1,171 +0,0 @@ -use actix_web::{http::header::Header, HttpRequest}; -use actix_web_httpauth::headers::authorization::{Authorization, Bearer}; -use base64::{engine::general_purpose::STANDARD_NO_PAD as base64, Engine}; -use captcha::Captcha; -use lemmy_api_common::{ - claims::Claims, - context::LemmyContext, - utils::{check_user_valid, local_site_to_slur_regex, AUTH_COOKIE_NAME}, -}; -use lemmy_db_schema::source::local_site::LocalSite; -use lemmy_db_views::structs::LocalUserView; -use lemmy_utils::{ - error::{LemmyError, LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult}, - utils::slurs::check_slurs, -}; -use std::io::Cursor; -use totp_rs::{Secret, TOTP}; - -pub mod comment; -pub mod comment_report; -pub mod community; -pub mod local_user; -pub mod post; -pub mod post_report; -pub mod private_message; -pub mod private_message_report; -pub mod site; -pub mod sitemap; - -/// Converts the captcha to a base64 encoded wav audio file -pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> Result { - let letters = captcha.as_wav(); - - // Decode each wav file, concatenate the samples - let mut concat_samples: Vec = Vec::new(); - let mut any_header: Option = None; - for letter in letters { - let mut cursor = Cursor::new(letter.unwrap_or_default()); - let (header, samples) = wav::read(&mut cursor)?; - any_header = Some(header); - if let Some(samples16) = samples.as_sixteen() { - concat_samples.extend(samples16); - } else { - Err(LemmyErrorType::CouldntCreateAudioCaptcha)? - } - } - - // Encode the concatenated result as a wav file - let mut output_buffer = Cursor::new(vec![]); - if let Some(header) = any_header { - wav::write( - header, - &wav::BitDepth::Sixteen(concat_samples), - &mut output_buffer, - ) - .with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?; - - Ok(base64.encode(output_buffer.into_inner())) - } else { - Err(LemmyErrorType::CouldntCreateAudioCaptcha)? - } -} - -/// Check size of report -pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> Result<(), LemmyError> { - let slur_regex = &local_site_to_slur_regex(local_site); - - check_slurs(reason, slur_regex)?; - if reason.is_empty() { - Err(LemmyErrorType::ReportReasonRequired)? - } else if reason.chars().count() > 1000 { - Err(LemmyErrorType::ReportTooLong)? - } else { - Ok(()) - } -} - -pub fn read_auth_token(req: &HttpRequest) -> Result, LemmyError> { - // Try reading jwt from auth header - if let Ok(header) = Authorization::::parse(req) { - Ok(Some(header.as_ref().token().to_string())) - } - // If that fails, try to read from cookie - else if let Some(cookie) = &req.cookie(AUTH_COOKIE_NAME) { - Ok(Some(cookie.value().to_string())) - } - // Otherwise, there's no auth - else { - Ok(None) - } -} - -pub(crate) fn check_totp_2fa_valid( - local_user_view: &LocalUserView, - totp_token: &Option, - site_name: &str, -) -> LemmyResult<()> { - // Throw an error if their token is missing - let token = totp_token - .as_deref() - .ok_or(LemmyErrorType::MissingTotpToken)?; - let secret = local_user_view - .local_user - .totp_2fa_secret - .as_deref() - .ok_or(LemmyErrorType::MissingTotpSecret)?; - - let totp = build_totp_2fa(site_name, &local_user_view.person.name, secret)?; - - let check_passed = totp.check_current(token)?; - if !check_passed { - return Err(LemmyErrorType::IncorrectTotpToken.into()); - } - - Ok(()) -} - -pub(crate) fn generate_totp_2fa_secret() -> String { - Secret::generate_secret().to_string() -} - -pub(crate) fn build_totp_2fa( - site_name: &str, - username: &str, - secret: &str, -) -> Result { - let sec = Secret::Raw(secret.as_bytes().to_vec()); - let sec_bytes = sec - .to_bytes() - .map_err(|_| LemmyErrorType::CouldntParseTotpSecret)?; - - TOTP::new( - totp_rs::Algorithm::SHA1, - 6, - 1, - 30, - sec_bytes, - Some(site_name.to_string()), - username.to_string(), - ) - .with_lemmy_type(LemmyErrorType::CouldntGenerateTotp) -} - -#[tracing::instrument(skip_all)] -pub async fn local_user_view_from_jwt( - jwt: &str, - context: &LemmyContext, -) -> Result { - let local_user_id = Claims::validate(jwt, context) - .await - .with_lemmy_type(LemmyErrorType::NotLoggedIn)?; - let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?; - check_user_valid(&local_user_view.person)?; - - Ok(local_user_view) -} - -#[cfg(test)] -mod tests { - #![allow(clippy::unwrap_used)] - #![allow(clippy::indexing_slicing)] - - use super::*; - - #[test] - fn test_build_totp() { - let generated_secret = generate_totp_2fa_secret(); - let totp = build_totp_2fa("lemmy", "my_name", &generated_secret); - assert!(totp.is_ok()); - } -} diff --git a/crates/api/src/local_user/change_password.rs b/crates/api/src/local_user/change_password.rs index 1391a96bf2..53a267d9a7 100644 --- a/crates/api/src/local_user/change_password.rs +++ b/crates/api/src/local_user/change_password.rs @@ -28,11 +28,12 @@ pub async fn change_password( } // Check the old password - let valid: bool = local_user_view.local_user.password_encrypted == "" || verify( - &data.old_password, - &local_user_view.local_user.password_encrypted, - ) - .unwrap_or(false); + let valid: bool = local_user_view.local_user.password_encrypted == "" + || verify( + &data.old_password, + &local_user_view.local_user.password_encrypted, + ) + .unwrap_or(false); if !valid { Err(LemmyErrorType::IncorrectLogin)? } diff --git a/crates/api/src/local_user/login.rs b/crates/api/src/local_user/login.rs index 28bb80ed2c..7f55b7239f 100644 --- a/crates/api/src/local_user/login.rs +++ b/crates/api/src/local_user/login.rs @@ -34,11 +34,12 @@ pub async fn login( .with_lemmy_type(LemmyErrorType::IncorrectLogin)?; // Verify the password - let valid: bool = local_user_view.local_user.password_encrypted != "" && verify( - &data.password, - &local_user_view.local_user.password_encrypted, - ) - .unwrap_or(false); + let valid: bool = local_user_view.local_user.password_encrypted != "" + && verify( + &data.password, + &local_user_view.local_user.password_encrypted, + ) + .unwrap_or(false); if !valid { Err(LemmyErrorType::IncorrectLogin)? } diff --git a/crates/api/src/local_user/oauth_callback.rs b/crates/api/src/local_user/oauth_callback.rs index 6b04bf2984..fb37502755 100644 --- a/crates/api/src/local_user/oauth_callback.rs +++ b/crates/api/src/local_user/oauth_callback.rs @@ -1,22 +1,13 @@ -use activitypub_federation::{config::Data}; -use actix_web::{ - http::StatusCode, - web::Query, - HttpRequest, - HttpResponse, -}; +use activitypub_federation::config::Data; +use actix_web::{http::StatusCode, web::Query, HttpRequest, HttpResponse}; use lemmy_api_common::{ claims::Claims, context::LemmyContext, external_auth::{OAuth, OAuthResponse, TokenResponse}, - utils::{create_login_cookie}, + utils::create_login_cookie, }; use lemmy_api_crud::user::create::register_from_oauth; -use lemmy_db_schema::{ - newtypes::ExternalAuthId, - RegistrationMode, - source::local_user::LocalUser, -}; +use lemmy_db_schema::{newtypes::ExternalAuthId, source::local_user::LocalUser, RegistrationMode}; use lemmy_db_views::structs::{ExternalAuthView, LocalUserView, SiteView}; use url::Url; @@ -29,12 +20,16 @@ pub async fn oauth_callback( let site_view = SiteView::read_local(&mut context.pool()).await; if !site_view.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=internal")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=internal")) + .finish(); } let state = serde_json::from_str::(&data.state); if !state.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=oauth_response")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=oauth_response")) + .finish(); } let oauth_state = state.unwrap(); @@ -42,44 +37,58 @@ pub async fn oauth_callback( let external_auth_id = ExternalAuthId(oauth_state.external_auth); let external_auth_view = ExternalAuthView::get(&mut context.pool(), external_auth_id).await; if !external_auth_view.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=external_auth")) + .finish(); } let external_auth = external_auth_view.unwrap().external_auth; - let client_secret = ExternalAuthView::get_client_secret(&mut context.pool(), external_auth_id) - .await; + let client_secret = + ExternalAuthView::get_client_secret(&mut context.pool(), external_auth_id).await; if !client_secret.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=external_auth")) + .finish(); } // Get endpoints let token_endpoint; let user_endpoint; if external_auth.auth_type == "oidc" { - let discovery_endpoint = - if external_auth.issuer.ends_with("/.well-known/openid-configuration") { - external_auth.issuer.to_string() - } else { - format!("{}/.well-known/openid-configuration", external_auth.issuer) - }; + let discovery_endpoint = if external_auth + .issuer + .ends_with("/.well-known/openid-configuration") + { + external_auth.issuer.to_string() + } else { + format!("{}/.well-known/openid-configuration", external_auth.issuer) + }; let res = context.client().get(discovery_endpoint).send().await; if !res.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=external_auth")) + .finish(); } let oidc_response = res.unwrap().json::().await; if !oidc_response.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=external_auth")) + .finish(); } let oidc_information = oidc_response.unwrap(); let token_endpoint_string = serde_json::from_value::(oidc_information["token_endpoint"].clone()); if !token_endpoint_string.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=external_auth")) + .finish(); } token_endpoint = Url::parse(&token_endpoint_string.unwrap()); let user_endpoint_string = serde_json::from_value::(oidc_information["userinfo_endpoint"].clone()); if !user_endpoint_string.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=external_auth")) + .finish(); } user_endpoint = Url::parse(&user_endpoint_string.unwrap()); } else { @@ -87,100 +96,129 @@ pub async fn oauth_callback( user_endpoint = Url::parse(&external_auth.user_endpoint); }; if !token_endpoint.is_ok() || !user_endpoint.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=external_auth5")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=external_auth5")) + .finish(); } // Send token request - let mut response = context.client() + let mut response = context + .client() .post(token_endpoint.unwrap()) .form(&[ - ("grant_type", "authorization_code"), - ("code", &data.code), - ("redirect_uri", &req.uri().to_string()), - ("client_id", &external_auth.client_id), - ("client_secret", &client_secret.unwrap()), + ("grant_type", "authorization_code"), + ("code", &data.code), + ("redirect_uri", &req.uri().to_string()), + ("client_id", &external_auth.client_id), + ("client_secret", &client_secret.unwrap()), ]) .send() .await; if !response.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=token")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=token")) + .finish(); } let mut res = response.unwrap(); if res.status() != StatusCode::OK { - return HttpResponse::Found().append_header(("Location", "/login?err=token")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=token")) + .finish(); } // Obtain access token let token_response = res.json::().await; if !token_response.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=token")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=token")) + .finish(); } let access_token = token_response.unwrap().access_token; // Make user info request - response = context.client() + response = context + .client() .post(user_endpoint.unwrap()) .bearer_auth(access_token) .send() .await; if !response.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=userinfo")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=userinfo")) + .finish(); } res = response.unwrap(); if res.status() != StatusCode::OK { - return HttpResponse::Found().append_header(("Location", "/login?err=userinfo")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=userinfo")) + .finish(); } // Find or create user let userinfo = res.json::().await; if !userinfo.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=external_auth")) + .finish(); } let user_info = userinfo.unwrap(); let user_id = serde_json::from_value::(user_info["email"].clone()); if !user_id.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=user")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=user")) + .finish(); } let email = user_id.unwrap(); - let local_user_view = - LocalUserView::find_by_email(&mut context.pool(), &email).await; + let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email).await; let local_site = site_view.unwrap().local_site; let local_user: LocalUser; if local_user_view.is_ok() { local_user = local_user_view.unwrap().local_user; } else if local_site.oauth_registration { - let username = serde_json::from_value::(user_info[external_auth.id_attribute] - .clone()); + let username = serde_json::from_value::(user_info[external_auth.id_attribute].clone()); if !username.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=external_auth")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=external_auth")) + .finish(); } let user = str::replace(&username.unwrap(), " ", "_"); let registered_user = register_from_oauth(user, email, &context).await; if !registered_user.is_ok() { tracing::error!("Failed to create user: {}", registered_user.err().unwrap()); - return HttpResponse::Found().append_header(("Location", "/login?err=user")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=user")) + .finish(); } local_user = registered_user.unwrap(); } else { - return HttpResponse::Found().append_header(("Location", "/signup")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/signup")) + .finish(); } if (local_site.registration_mode == RegistrationMode::RequireApplication || local_site.registration_mode == RegistrationMode::Closed) && !local_user.accepted_application - && !local_user.admin { - return HttpResponse::Found().append_header(("Location", "/login?err=application")).finish(); + && !local_user.admin + { + return HttpResponse::Found() + .append_header(("Location", "/login?err=application")) + .finish(); } // Check email is verified regardless of site setting, to prevent potential account theft if !local_user.admin && !local_user.email_verified { - return HttpResponse::Found().append_header(("Location", "/login?err=email")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=email")) + .finish(); } let jwt = Claims::generate(local_user.id, req, &context).await; if !jwt.is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=jwt")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=jwt")) + .finish(); } let mut res = HttpResponse::build(StatusCode::FOUND) @@ -190,7 +228,9 @@ pub async fn oauth_callback( cookie.set_path("/"); cookie.set_http_only(false); // We'll need to access the cookie via document.cookie for this req if !res.add_cookie(&cookie).is_ok() { - return HttpResponse::Found().append_header(("Location", "/login?err=jwt")).finish(); + return HttpResponse::Found() + .append_header(("Location", "/login?err=jwt")) + .finish(); } - return res; + return res; } diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs deleted file mode 100644 index 0ea27f794c..0000000000 --- a/crates/api_common/src/utils.rs +++ /dev/null @@ -1,883 +0,0 @@ -use crate::{ - context::LemmyContext, - request::purge_image_from_pictrs, - site::{FederatedInstances, InstanceWithFederationState}, -}; -use chrono::{DateTime, Days, Local, TimeZone, Utc}; -use enum_map::{enum_map, EnumMap}; -use lemmy_db_schema::{ - newtypes::{CommunityId, DbUrl, InstanceId, PersonId, PostId}, - source::{ - comment::{Comment, CommentUpdateForm}, - community::{Community, CommunityModerator, CommunityUpdateForm}, - community_block::CommunityBlock, - email_verification::{EmailVerification, EmailVerificationForm}, - instance::Instance, - instance_block::InstanceBlock, - local_site::LocalSite, - local_site_rate_limit::LocalSiteRateLimit, - password_reset_request::PasswordResetRequest, - person::{Person, PersonUpdateForm}, - person_block::PersonBlock, - post::{Post, PostRead}, - }, - traits::Crud, - utils::DbPool, -}; -use lemmy_db_views::{comment_view::CommentQuery, structs::LocalUserView}; -use lemmy_db_views_actor::structs::{ - CommunityModeratorView, - CommunityPersonBanView, - CommunityView, -}; -use lemmy_utils::{ - email::{send_email, translations::Lang}, - error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, - rate_limit::{ActionType, BucketConfig}, - settings::structs::Settings, - utils::slurs::build_slur_regex, -}; -use regex::Regex; -use rosetta_i18n::{Language, LanguageId}; -use std::collections::HashSet; -use tracing::warn; -use url::{ParseError, Url}; - -pub static AUTH_COOKIE_NAME: &str = "jwt"; - -#[tracing::instrument(skip_all)] -pub async fn is_mod_or_admin( - pool: &mut DbPool<'_>, - person: &Person, - community_id: CommunityId, -) -> Result<(), LemmyError> { - check_user_valid(person)?; - - let is_mod_or_admin = CommunityView::is_mod_or_admin(pool, person.id, community_id).await?; - if !is_mod_or_admin { - Err(LemmyErrorType::NotAModOrAdmin)? - } else { - Ok(()) - } -} - -#[tracing::instrument(skip_all)] -pub async fn is_mod_or_admin_opt( - pool: &mut DbPool<'_>, - local_user_view: Option<&LocalUserView>, - community_id: Option, -) -> Result<(), LemmyError> { - if let Some(local_user_view) = local_user_view { - if let Some(community_id) = community_id { - is_mod_or_admin(pool, &local_user_view.person, community_id).await - } else { - is_admin(local_user_view) - } - } else { - Err(LemmyErrorType::NotAModOrAdmin)? - } -} - -/// Check that a person is either a mod of any community, or an admin -/// -/// Should only be used for read operations -#[tracing::instrument(skip_all)] -pub async fn check_community_mod_of_any_or_admin_action( - local_user_view: &LocalUserView, - pool: &mut DbPool<'_>, -) -> LemmyResult<()> { - let person = &local_user_view.person; - - check_user_valid(person)?; - - let is_mod_of_any_or_admin = CommunityView::is_mod_of_any_or_admin(pool, person.id).await?; - if !is_mod_of_any_or_admin { - Err(LemmyErrorType::NotAModOrAdmin)? - } else { - Ok(()) - } -} - -pub fn is_admin(local_user_view: &LocalUserView) -> Result<(), LemmyError> { - check_user_valid(&local_user_view.person)?; - if !local_user_view.local_user.admin { - Err(LemmyErrorType::NotAnAdmin)? - } else if local_user_view.person.banned { - Err(LemmyErrorType::Banned)? - } else { - Ok(()) - } -} - -pub fn is_top_mod( - local_user_view: &LocalUserView, - community_mods: &[CommunityModeratorView], -) -> Result<(), LemmyError> { - check_user_valid(&local_user_view.person)?; - if local_user_view.person.id - != community_mods - .first() - .map(|cm| cm.moderator.id) - .unwrap_or(PersonId(0)) - { - Err(LemmyErrorType::NotTopMod)? - } else { - Ok(()) - } -} - -#[tracing::instrument(skip_all)] -pub async fn get_post(post_id: PostId, pool: &mut DbPool<'_>) -> Result { - Post::read(pool, post_id) - .await - .with_lemmy_type(LemmyErrorType::CouldntFindPost) -} - -#[tracing::instrument(skip_all)] -pub async fn mark_post_as_read( - person_id: PersonId, - post_id: PostId, - pool: &mut DbPool<'_>, -) -> Result<(), LemmyError> { - PostRead::mark_as_read(pool, HashSet::from([post_id]), person_id) - .await - .with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?; - Ok(()) -} - -pub fn check_user_valid(person: &Person) -> Result<(), LemmyError> { - // Check for a site ban - if person.banned { - Err(LemmyErrorType::SiteBan)? - } - // check for account deletion - else if person.deleted { - Err(LemmyErrorType::Deleted)? - } else { - 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 -/// the user isn't banned. -pub async fn check_community_user_action( - person: &Person, - community_id: CommunityId, - pool: &mut DbPool<'_>, -) -> LemmyResult<()> { - check_user_valid(person)?; - check_community_deleted_removed(community_id, pool).await?; - check_community_ban(person, community_id, pool).await?; - Ok(()) -} - -async fn check_community_deleted_removed( - community_id: CommunityId, - pool: &mut DbPool<'_>, -) -> LemmyResult<()> { - let community = Community::read(pool, community_id) - .await - .with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?; - if community.deleted || community.removed { - Err(LemmyErrorType::Deleted)? - } - Ok(()) -} - -async fn check_community_ban( - person: &Person, - community_id: CommunityId, - pool: &mut DbPool<'_>, -) -> LemmyResult<()> { - // check if user was banned from site or community - let is_banned = CommunityPersonBanView::get(pool, person.id, community_id).await?; - if is_banned { - Err(LemmyErrorType::BannedFromCommunity)? - } - Ok(()) -} - -/// Check that the given user can perform a mod action in the community. -/// -/// In particular it checks that he is an admin or mod, wasn't banned and the community isn't -/// removed/deleted. -pub async fn check_community_mod_action( - person: &Person, - community_id: CommunityId, - allow_deleted: bool, - pool: &mut DbPool<'_>, -) -> LemmyResult<()> { - is_mod_or_admin(pool, person, community_id).await?; - check_community_ban(person, community_id, pool).await?; - - // it must be possible to restore deleted community - if !allow_deleted { - check_community_deleted_removed(community_id, pool).await?; - } - Ok(()) -} - -pub fn check_post_deleted_or_removed(post: &Post) -> Result<(), LemmyError> { - if post.deleted || post.removed { - Err(LemmyErrorType::Deleted)? - } else { - Ok(()) - } -} - -/// Throws an error if a recipient has blocked a person. -#[tracing::instrument(skip_all)] -pub async fn check_person_block( - my_id: PersonId, - potential_blocker_id: PersonId, - pool: &mut DbPool<'_>, -) -> Result<(), LemmyError> { - let is_blocked = PersonBlock::read(pool, potential_blocker_id, my_id).await?; - if is_blocked { - Err(LemmyErrorType::PersonIsBlocked)? - } else { - Ok(()) - } -} - -/// Throws an error if a recipient has blocked a community. -#[tracing::instrument(skip_all)] -async fn check_community_block( - community_id: CommunityId, - person_id: PersonId, - pool: &mut DbPool<'_>, -) -> Result<(), LemmyError> { - let is_blocked = CommunityBlock::read(pool, person_id, community_id).await?; - if is_blocked { - Err(LemmyErrorType::CommunityIsBlocked)? - } else { - Ok(()) - } -} - -/// Throws an error if a recipient has blocked an instance. -#[tracing::instrument(skip_all)] -async fn check_instance_block( - instance_id: InstanceId, - person_id: PersonId, - pool: &mut DbPool<'_>, -) -> Result<(), LemmyError> { - let is_blocked = InstanceBlock::read(pool, person_id, instance_id).await?; - if is_blocked { - Err(LemmyErrorType::InstanceIsBlocked)? - } else { - Ok(()) - } -} - -#[tracing::instrument(skip_all)] -pub async fn check_person_instance_community_block( - my_id: PersonId, - potential_blocker_id: PersonId, - instance_id: InstanceId, - community_id: CommunityId, - pool: &mut DbPool<'_>, -) -> Result<(), LemmyError> { - check_person_block(my_id, potential_blocker_id, pool).await?; - check_instance_block(instance_id, potential_blocker_id, pool).await?; - check_community_block(community_id, potential_blocker_id, pool).await?; - Ok(()) -} - -#[tracing::instrument(skip_all)] -pub fn check_downvotes_enabled(score: i16, local_site: &LocalSite) -> Result<(), LemmyError> { - if score == -1 && !local_site.enable_downvotes { - Err(LemmyErrorType::DownvotesAreDisabled)? - } else { - Ok(()) - } -} - -/// Dont allow bots to do certain actions, like voting -#[tracing::instrument(skip_all)] -pub fn check_bot_account(person: &Person) -> Result<(), LemmyError> { - if person.bot_account { - Err(LemmyErrorType::InvalidBotAction)? - } else { - Ok(()) - } -} - -#[tracing::instrument(skip_all)] -pub fn check_private_instance( - local_user_view: &Option, - local_site: &LocalSite, -) -> Result<(), LemmyError> { - if local_user_view.is_none() && local_site.private_instance { - Err(LemmyErrorType::InstanceIsPrivate)? - } else { - Ok(()) - } -} - -#[tracing::instrument(skip_all)] -pub async fn build_federated_instances( - local_site: &LocalSite, - pool: &mut DbPool<'_>, -) -> Result, LemmyError> { - if local_site.federation_enabled { - let mut linked = Vec::new(); - let mut allowed = Vec::new(); - let mut blocked = Vec::new(); - - let all = Instance::read_all_with_fed_state(pool).await?; - for (instance, federation_state, is_blocked, is_allowed) in all { - let i = InstanceWithFederationState { - instance, - federation_state: federation_state.map(std::convert::Into::into), - }; - if is_blocked { - // blocked instances will only have an entry here if they had been federated with in the past. - blocked.push(i); - } else if is_allowed { - allowed.push(i.clone()); - linked.push(i); - } else { - // not explicitly allowed but implicitly linked - linked.push(i); - } - } - - Ok(Some(FederatedInstances { - linked, - allowed, - blocked, - })) - } else { - Ok(None) - } -} - -/// Checks the password length -pub fn password_length_check(pass: &str) -> Result<(), LemmyError> { - if !(10..=60).contains(&pass.chars().count()) { - Err(LemmyErrorType::InvalidPassword)? - } else { - Ok(()) - } -} - -/// Checks for a honeypot. If this field is filled, fail the rest of the function -pub fn honeypot_check(honeypot: &Option) -> Result<(), LemmyError> { - if honeypot.is_some() && honeypot != &Some(String::new()) { - Err(LemmyErrorType::HoneypotFailed)? - } else { - Ok(()) - } -} - -pub async fn send_email_to_user( - local_user_view: &LocalUserView, - subject: &str, - body: &str, - settings: &Settings, -) { - if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email { - return; - } - - if let Some(user_email) = &local_user_view.local_user.email { - match send_email( - subject, - user_email, - &local_user_view.person.name, - body, - settings, - ) - .await - { - Ok(_o) => _o, - Err(e) => warn!("{}", e), - }; - } -} - -pub async fn send_password_reset_email( - user: &LocalUserView, - pool: &mut DbPool<'_>, - settings: &Settings, -) -> Result<(), LemmyError> { - // Generate a random token - let token = uuid::Uuid::new_v4().to_string(); - - // Insert the row - let local_user_id = user.local_user.id; - PasswordResetRequest::create_token(pool, local_user_id, token.clone()).await?; - - let email = &user.local_user.email.clone().expect("email"); - let lang = get_interface_language(user); - let subject = &lang.password_reset_subject(&user.person.name); - let protocol_and_hostname = settings.get_protocol_and_hostname(); - let reset_link = format!("{}/password_change/{}", protocol_and_hostname, &token); - let body = &lang.password_reset_body(reset_link, &user.person.name); - send_email(subject, email, &user.person.name, body, settings).await -} - -/// Send a verification email -pub async fn send_verification_email( - user: &LocalUserView, - new_email: &str, - pool: &mut DbPool<'_>, - settings: &Settings, -) -> Result<(), LemmyError> { - let form = EmailVerificationForm { - local_user_id: user.local_user.id, - email: new_email.to_string(), - verification_token: uuid::Uuid::new_v4().to_string(), - }; - let verify_link = format!( - "{}/verify_email/{}", - settings.get_protocol_and_hostname(), - &form.verification_token - ); - EmailVerification::create(pool, &form).await?; - - let lang = get_interface_language(user); - let subject = lang.verify_email_subject(&settings.hostname); - let body = lang.verify_email_body(&settings.hostname, &user.person.name, verify_link); - send_email(&subject, new_email, &user.person.name, &body, settings).await?; - - Ok(()) -} - -pub fn get_interface_language(user: &LocalUserView) -> Lang { - lang_str_to_lang(&user.local_user.interface_language) -} - -pub fn get_interface_language_from_settings(user: &LocalUserView) -> Lang { - lang_str_to_lang(&user.local_user.interface_language) -} - -fn lang_str_to_lang(lang: &str) -> Lang { - let lang_id = LanguageId::new(lang); - Lang::from_language_id(&lang_id).unwrap_or_else(|| { - let en = LanguageId::new("en"); - Lang::from_language_id(&en).expect("default language") - }) -} - -pub fn local_site_rate_limit_to_rate_limit_config( - l: &LocalSiteRateLimit, -) -> EnumMap { - enum_map! { - ActionType::Message => (l.message, l.message_per_second), - ActionType::Post => (l.post, l.post_per_second), - ActionType::Register => (l.register, l.register_per_second), - ActionType::Image => (l.image, l.image_per_second), - ActionType::Comment => (l.comment, l.comment_per_second), - ActionType::Search => (l.search, l.search_per_second), - ActionType::ImportUserSettings => (l.import_user_settings, l.import_user_settings_per_second), - } - .map(|_key, (capacity, secs_to_refill)| BucketConfig { - capacity: u32::try_from(capacity).unwrap_or(0), - secs_to_refill: u32::try_from(secs_to_refill).unwrap_or(0), - }) -} - -pub fn local_site_to_slur_regex(local_site: &LocalSite) -> Option { - build_slur_regex(local_site.slur_filter_regex.as_deref()) -} - -pub fn local_site_opt_to_slur_regex(local_site: &Option) -> Option { - local_site - .as_ref() - .map(local_site_to_slur_regex) - .unwrap_or(None) -} - -pub fn local_site_opt_to_sensitive(local_site: &Option) -> bool { - local_site - .as_ref() - .map(|site| site.enable_nsfw) - .unwrap_or(false) -} - -pub async fn send_application_approved_email( - user: &LocalUserView, - settings: &Settings, -) -> Result<(), LemmyError> { - let email = &user.local_user.email.clone().expect("email"); - let lang = get_interface_language(user); - let subject = lang.registration_approved_subject(&user.person.actor_id); - let body = lang.registration_approved_body(&settings.hostname); - send_email(&subject, email, &user.person.name, &body, settings).await -} - -/// Send a new applicant email notification to all admins -pub async fn send_new_applicant_email_to_admins( - applicant_username: &str, - pool: &mut DbPool<'_>, - settings: &Settings, -) -> Result<(), LemmyError> { - // Collect the admins with emails - let admins = LocalUserView::list_admins_with_emails(pool).await?; - - let applications_link = &format!( - "{}/registration_applications", - settings.get_protocol_and_hostname(), - ); - - for admin in &admins { - let email = &admin.local_user.email.clone().expect("email"); - let lang = get_interface_language_from_settings(admin); - let subject = lang.new_application_subject(&settings.hostname, applicant_username); - let body = lang.new_application_body(applications_link); - send_email(&subject, email, &admin.person.name, &body, settings).await?; - } - Ok(()) -} - -/// Send a report to all admins -pub async fn send_new_report_email_to_admins( - reporter_username: &str, - reported_username: &str, - pool: &mut DbPool<'_>, - settings: &Settings, -) -> Result<(), LemmyError> { - // Collect the admins with emails - let admins = LocalUserView::list_admins_with_emails(pool).await?; - - let reports_link = &format!("{}/reports", settings.get_protocol_and_hostname(),); - - for admin in &admins { - let email = &admin.local_user.email.clone().expect("email"); - let lang = get_interface_language_from_settings(admin); - let subject = lang.new_report_subject(&settings.hostname, reported_username, reporter_username); - let body = lang.new_report_body(reports_link); - send_email(&subject, email, &admin.person.name, &body, settings).await?; - } - Ok(()) -} - -pub fn check_private_instance_and_federation_enabled( - local_site: &LocalSite, -) -> Result<(), LemmyError> { - if local_site.private_instance && local_site.federation_enabled { - Err(LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether)? - } else { - Ok(()) - } -} - -pub async fn purge_image_posts_for_person( - banned_person_id: PersonId, - context: &LemmyContext, -) -> Result<(), LemmyError> { - let pool = &mut context.pool(); - let posts = Post::fetch_pictrs_posts_for_creator(pool, banned_person_id).await?; - for post in posts { - if let Some(url) = post.url { - purge_image_from_pictrs(&url, context).await.ok(); - } - if let Some(thumbnail_url) = post.thumbnail_url { - purge_image_from_pictrs(&thumbnail_url, context).await.ok(); - } - } - - Post::remove_pictrs_post_images_and_thumbnails_for_creator(pool, banned_person_id).await?; - - Ok(()) -} - -pub async fn purge_image_posts_for_community( - banned_community_id: CommunityId, - context: &LemmyContext, -) -> Result<(), LemmyError> { - let pool = &mut context.pool(); - let posts = Post::fetch_pictrs_posts_for_community(pool, banned_community_id).await?; - for post in posts { - if let Some(url) = post.url { - purge_image_from_pictrs(&url, context).await.ok(); - } - if let Some(thumbnail_url) = post.thumbnail_url { - purge_image_from_pictrs(&thumbnail_url, context).await.ok(); - } - } - - Post::remove_pictrs_post_images_and_thumbnails_for_community(pool, banned_community_id).await?; - - Ok(()) -} - -pub async fn remove_user_data( - banned_person_id: PersonId, - context: &LemmyContext, -) -> Result<(), LemmyError> { - let pool = &mut context.pool(); - // Purge user images - let person = Person::read(pool, banned_person_id).await?; - if let Some(avatar) = person.avatar { - purge_image_from_pictrs(&avatar, context).await.ok(); - } - if let Some(banner) = person.banner { - purge_image_from_pictrs(&banner, context).await.ok(); - } - - // Update the fields to None - Person::update( - pool, - banned_person_id, - &PersonUpdateForm { - avatar: Some(None), - banner: Some(None), - bio: Some(None), - ..Default::default() - }, - ) - .await?; - - // Posts - Post::update_removed_for_creator(pool, banned_person_id, None, true).await?; - - // Purge image posts - purge_image_posts_for_person(banned_person_id, context).await?; - - // Communities - // Remove all communities where they're the top mod - // for now, remove the communities manually - let first_mod_communities = CommunityModeratorView::get_community_first_mods(pool).await?; - - // Filter to only this banned users top communities - let banned_user_first_communities: Vec = first_mod_communities - .into_iter() - .filter(|fmc| fmc.moderator.id == banned_person_id) - .collect(); - - for first_mod_community in banned_user_first_communities { - let community_id = first_mod_community.community.id; - Community::update( - pool, - community_id, - &CommunityUpdateForm { - removed: Some(true), - ..Default::default() - }, - ) - .await?; - - // Delete the community images - if let Some(icon) = first_mod_community.community.icon { - purge_image_from_pictrs(&icon, context).await.ok(); - } - if let Some(banner) = first_mod_community.community.banner { - purge_image_from_pictrs(&banner, context).await.ok(); - } - // Update the fields to None - Community::update( - pool, - community_id, - &CommunityUpdateForm { - icon: Some(None), - banner: Some(None), - ..Default::default() - }, - ) - .await?; - } - - // Comments - Comment::update_removed_for_creator(pool, banned_person_id, true).await?; - - Ok(()) -} - -pub async fn remove_user_data_in_community( - community_id: CommunityId, - banned_person_id: PersonId, - pool: &mut DbPool<'_>, -) -> Result<(), LemmyError> { - // Posts - Post::update_removed_for_creator(pool, banned_person_id, Some(community_id), true).await?; - - // Comments - // TODO Diesel doesn't allow updates with joins, so this has to be a loop - let comments = CommentQuery { - creator_id: Some(banned_person_id), - community_id: Some(community_id), - ..Default::default() - } - .list(pool) - .await?; - - for comment_view in &comments { - let comment_id = comment_view.comment.id; - Comment::update( - pool, - comment_id, - &CommentUpdateForm { - removed: Some(true), - ..Default::default() - }, - ) - .await?; - } - - Ok(()) -} - -pub async fn purge_user_account( - person_id: PersonId, - context: &LemmyContext, -) -> Result<(), LemmyError> { - let pool = &mut context.pool(); - // Delete their images - let person = Person::read(pool, person_id).await?; - if let Some(avatar) = person.avatar { - purge_image_from_pictrs(&avatar, context).await.ok(); - } - if let Some(banner) = person.banner { - purge_image_from_pictrs(&banner, context).await.ok(); - } - // No need to update avatar and banner, those are handled in Person::delete_account - - // Comments - Comment::permadelete_for_creator(pool, person_id) - .await - .with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?; - - // Posts - Post::permadelete_for_creator(pool, person_id) - .await - .with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?; - - // Purge image posts - purge_image_posts_for_person(person_id, context).await?; - - // Leave communities they mod - CommunityModerator::leave_all_communities(pool, person_id).await?; - - Person::delete_account(pool, person_id).await?; - - Ok(()) -} - -pub enum EndpointType { - Community, - Person, - Post, - Comment, - PrivateMessage, -} - -/// Generates an apub endpoint for a given domain, IE xyz.tld -pub fn generate_local_apub_endpoint( - endpoint_type: EndpointType, - name: &str, - domain: &str, -) -> Result { - let point = match endpoint_type { - EndpointType::Community => "c", - EndpointType::Person => "u", - EndpointType::Post => "post", - EndpointType::Comment => "comment", - EndpointType::PrivateMessage => "private_message", - }; - - Ok(Url::parse(&format!("{domain}/{point}/{name}"))?.into()) -} - -pub fn generate_followers_url(actor_id: &DbUrl) -> Result { - Ok(Url::parse(&format!("{actor_id}/followers"))?.into()) -} - -pub fn generate_inbox_url(actor_id: &DbUrl) -> Result { - Ok(Url::parse(&format!("{actor_id}/inbox"))?.into()) -} - -pub fn generate_shared_inbox_url(settings: &Settings) -> Result { - let url = format!("{}/inbox", settings.get_protocol_and_hostname()); - Ok(Url::parse(&url)?.into()) -} - -pub fn generate_outbox_url(actor_id: &DbUrl) -> Result { - Ok(Url::parse(&format!("{actor_id}/outbox"))?.into()) -} - -pub fn generate_featured_url(actor_id: &DbUrl) -> Result { - Ok(Url::parse(&format!("{actor_id}/featured"))?.into()) -} - -pub fn generate_moderators_url(community_id: &DbUrl) -> Result { - Ok(Url::parse(&format!("{community_id}/moderators"))?.into()) -} - -/// Ensure that ban/block expiry is in valid range. If its in past, throw error. If its more -/// than 10 years in future, convert to permanent ban. Otherwise return the same value. -pub fn check_expire_time(expires_unix_opt: Option) -> LemmyResult>> { - if let Some(expires_unix) = expires_unix_opt { - let expires = Utc - .timestamp_opt(expires_unix, 0) - .single() - .ok_or(LemmyErrorType::InvalidUnixTime)?; - - limit_expire_time(expires) - } else { - Ok(None) - } -} - -fn limit_expire_time(expires: DateTime) -> LemmyResult>> { - const MAX_BAN_TERM: Days = Days::new(10 * 365); - - if expires < Local::now() { - Err(LemmyErrorType::BanExpirationInPast)? - } else if expires > Local::now() + MAX_BAN_TERM { - Ok(None) - } else { - Ok(Some(expires)) - } -} - -#[cfg(test)] -mod tests { - #![allow(clippy::unwrap_used)] - #![allow(clippy::indexing_slicing)] - - use crate::utils::{honeypot_check, limit_expire_time, password_length_check}; - use chrono::{Days, Utc}; - - #[test] - #[rustfmt::skip] - fn password_length() { - assert!(password_length_check("Õ¼¾°3yË,o¸ãtÌÈú|ÇÁÙAøüÒI©·¤(T]/ð>æºWæ[C¤bªWöaÃÎñ·{=û³&§½K/c").is_ok()); - assert!(password_length_check("1234567890").is_ok()); - assert!(password_length_check("short").is_err()); - assert!(password_length_check("looooooooooooooooooooooooooooooooooooooooooooooooooooooooooong").is_err()); - } - - #[test] - fn honeypot() { - assert!(honeypot_check(&None).is_ok()); - assert!(honeypot_check(&Some(String::new())).is_ok()); - assert!(honeypot_check(&Some("1".to_string())).is_err()); - assert!(honeypot_check(&Some("message".to_string())).is_err()); - } - - #[test] - fn test_limit_ban_term() { - // Ban expires in past, should throw error - assert!(limit_expire_time(Utc::now() - Days::new(5)).is_err()); - - // Legitimate ban term, return same value - let fourteen_days = Utc::now() + Days::new(14); - assert_eq!( - limit_expire_time(fourteen_days).unwrap(), - Some(fourteen_days) - ); - let nine_years = Utc::now() + Days::new(365 * 9); - assert_eq!(limit_expire_time(nine_years).unwrap(), Some(nine_years)); - - // Too long ban term, changes to None (permanent ban) - assert_eq!( - limit_expire_time(Utc::now() + Days::new(365 * 11)).unwrap(), - None - ); - } -} diff --git a/crates/api_crud/src/external_auth/create.rs b/crates/api_crud/src/external_auth/create.rs index fc10435864..ae7f65b5d0 100644 --- a/crates/api_crud/src/external_auth/create.rs +++ b/crates/api_crud/src/external_auth/create.rs @@ -38,5 +38,7 @@ pub async fn create_external_auth( .build(); let external_auth = ExternalAuth::create(&mut context.pool(), &external_auth_form).await?; let view = ExternalAuthView::get(&mut context.pool(), external_auth.id).await?; - Ok(Json(ExternalAuthResponse { external_auth: view })) + Ok(Json(ExternalAuthResponse { + external_auth: view, + })) } diff --git a/crates/api_crud/src/external_auth/update.rs b/crates/api_crud/src/external_auth/update.rs index 56d423251d..505ee8adbf 100644 --- a/crates/api_crud/src/external_auth/update.rs +++ b/crates/api_crud/src/external_auth/update.rs @@ -2,7 +2,7 @@ use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ context::LemmyContext, - external_auth::{ExternalAuthResponse, EditExternalAuth}, + external_auth::{EditExternalAuth, ExternalAuthResponse}, utils::is_admin, }; use lemmy_db_schema::source::{ @@ -40,7 +40,10 @@ pub async fn update_external_auth( }) .scopes(data.scopes.to_string()); - let external_auth = ExternalAuth::update(&mut context.pool(), data.id, &external_auth_form.build()).await?; + let external_auth = + ExternalAuth::update(&mut context.pool(), data.id, &external_auth_form.build()).await?; let view = ExternalAuthView::get(&mut context.pool(), external_auth.id).await?; - Ok(Json(ExternalAuthResponse { external_auth: view })) + Ok(Json(ExternalAuthResponse { + external_auth: view, + })) } diff --git a/crates/api_crud/src/user/delete.rs b/crates/api_crud/src/user/delete.rs index 342b035b2b..fc39aa24c1 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 = local_user_view.local_user.password_encrypted == "" || verify( - &data.password, - &local_user_view.local_user.password_encrypted, - ) - .unwrap_or(false); + let valid: bool = local_user_view.local_user.password_encrypted == "" + || verify( + &data.password, + &local_user_view.local_user.password_encrypted, + ) + .unwrap_or(false); if !valid { Err(LemmyErrorType::IncorrectLogin)? } diff --git a/crates/db_schema/src/impls/external_auth.rs b/crates/db_schema/src/impls/external_auth.rs index 9eb8ee582e..567bc3a212 100644 --- a/crates/db_schema/src/impls/external_auth.rs +++ b/crates/db_schema/src/impls/external_auth.rs @@ -1,11 +1,7 @@ use crate::{ newtypes::ExternalAuthId, - schema::{ - external_auth::dsl::external_auth, - }, - source::{ - external_auth::{ExternalAuth, ExternalAuthInsertForm, ExternalAuthUpdateForm}, - }, + schema::external_auth::dsl::external_auth, + source::external_auth::{ExternalAuth, ExternalAuthInsertForm, ExternalAuthUpdateForm}, utils::{get_conn, DbPool}, }; use diesel::{dsl::insert_into, result::Error, QueryDsl}; @@ -30,11 +26,13 @@ impl ExternalAuth { .get_result::(conn) .await } - pub async fn delete(pool: &mut DbPool<'_>, external_auth_id: ExternalAuthId) -> Result { + pub async fn delete( + pool: &mut DbPool<'_>, + external_auth_id: ExternalAuthId + ) -> Result { let conn = &mut get_conn(pool).await?; diesel::delete(external_auth.find(external_auth_id)) .execute(conn) .await } } - \ No newline at end of file diff --git a/crates/db_views/Cargo.toml b/crates/db_views/Cargo.toml index 83d1bb79f3..847b743921 100644 --- a/crates/db_views/Cargo.toml +++ b/crates/db_views/Cargo.toml @@ -36,7 +36,6 @@ serde = { workspace = true } serde_with = { workspace = true } tracing = { workspace = true, optional = true } ts-rs = { workspace = true, optional = true } -url = { workspace = true } actix-web = { workspace = true, optional = true } [dev-dependencies] diff --git a/crates/db_views/src/external_auth_view.rs b/crates/db_views/src/external_auth_view.rs index 27344ca553..c03dd4dabf 100644 --- a/crates/db_views/src/external_auth_view.rs +++ b/crates/db_views/src/external_auth_view.rs @@ -3,7 +3,7 @@ use diesel::{result::Error, ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ newtypes::{ExternalAuthId, LocalSiteId}, - schema::{external_auth}, + schema::external_auth, source::external_auth::ExternalAuth, utils::{get_conn, DbPool}, }; @@ -28,7 +28,10 @@ impl ExternalAuthView { // client_secret is in its own function because it should never be sent to any frontends, // and will only be needed when performing an oauth request by the server - pub async fn get_client_secret(pool: &mut DbPool<'_>, external_auth_id: ExternalAuthId) -> Result { + pub async fn get_client_secret( + pool: &mut DbPool<'_>, + external_auth_id: ExternalAuthId + ) -> Result { let conn = &mut get_conn(pool).await?; let external_auths = external_auth::table .find(external_auth_id) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 6d24b32999..fdf6c32c2c 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -60,9 +60,9 @@ services: # use "build" to build your local lemmy ui image for development. make sure to comment out "image". # run: docker compose up --build - build: - context: ../../lemmy-ui # assuming lemmy-ui is cloned besides lemmy directory - dockerfile: dev.dockerfile + # build: + # context: ../../lemmy-ui # assuming lemmy-ui is cloned besides lemmy directory + # dockerfile: dev.dockerfile environment: # this needs to match the hostname defined in the lemmy service - LEMMY_UI_LEMMY_INTERNAL_HOST=lemmy:8536 diff --git a/migrations/2023-12-07-200127_add_oauth_registration/up.sql b/migrations/2023-12-07-200127_add_oauth_registration/up.sql index e8681638a7..041d5d4811 100644 --- a/migrations/2023-12-07-200127_add_oauth_registration/up.sql +++ b/migrations/2023-12-07-200127_add_oauth_registration/up.sql @@ -1,3 +1,3 @@ ALTER TABLE local_site - ADD COLUMN oauth_registration boolean DEFAULT false NOT NULL; + ADD COLUMN oauth_registration boolean DEFAULT FALSE NOT NULL; From e0cabff9486f17b538fcc7a3eaf906b30530b42e Mon Sep 17 00:00:00 2001 From: Anthony Lawn Date: Fri, 8 Dec 2023 13:49:54 -0600 Subject: [PATCH 07/18] Accidentally rm'ed some files --- crates/api/src/lib.rs | 172 ++++ crates/api_common/src/utils.rs | 884 ++++++++++++++++++ crates/db_schema/src/impls/external_auth.rs | 2 +- crates/db_views/src/external_auth_view.rs | 2 +- .../down.sql | 1 + 5 files changed, 1059 insertions(+), 2 deletions(-) create mode 100644 crates/api/src/lib.rs create mode 100644 crates/api_common/src/utils.rs diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs new file mode 100644 index 0000000000..9a55000217 --- /dev/null +++ b/crates/api/src/lib.rs @@ -0,0 +1,172 @@ +use actix_web::{http::header::Header, HttpRequest}; +use actix_web_httpauth::headers::authorization::{Authorization, Bearer}; +use base64::{engine::general_purpose::STANDARD_NO_PAD as base64, Engine}; +use captcha::Captcha; +use lemmy_api_common::{ + claims::Claims, + context::LemmyContext, + utils::{check_user_valid, local_site_to_slur_regex, AUTH_COOKIE_NAME}, +}; +use lemmy_db_schema::source::local_site::LocalSite; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::{ + error::{LemmyError, LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult}, + utils::slurs::check_slurs, +}; +use std::io::Cursor; +use totp_rs::{Secret, TOTP}; + +pub mod comment; +pub mod comment_report; +pub mod community; +pub mod local_user; +pub mod post; +pub mod post_report; +pub mod private_message; +pub mod private_message_report; +pub mod site; +pub mod sitemap; + +/// Converts the captcha to a base64 encoded wav audio file +pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> Result { + let letters = captcha.as_wav(); + + // Decode each wav file, concatenate the samples + let mut concat_samples: Vec = Vec::new(); + let mut any_header: Option = None; + for letter in letters { + let mut cursor = Cursor::new(letter.unwrap_or_default()); + let (header, samples) = wav::read(&mut cursor)?; + any_header = Some(header); + if let Some(samples16) = samples.as_sixteen() { + concat_samples.extend(samples16); + } else { + Err(LemmyErrorType::CouldntCreateAudioCaptcha)? + } + } + + // Encode the concatenated result as a wav file + let mut output_buffer = Cursor::new(vec![]); + if let Some(header) = any_header { + wav::write( + header, + &wav::BitDepth::Sixteen(concat_samples), + &mut output_buffer, + ) + .with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?; + + Ok(base64.encode(output_buffer.into_inner())) + } else { + Err(LemmyErrorType::CouldntCreateAudioCaptcha)? + } +} + +/// Check size of report +pub(crate) fn check_report_reason(reason: &str, local_site: &LocalSite) -> Result<(), LemmyError> { + let slur_regex = &local_site_to_slur_regex(local_site); + + check_slurs(reason, slur_regex)?; + if reason.is_empty() { + Err(LemmyErrorType::ReportReasonRequired)? + } else if reason.chars().count() > 1000 { + Err(LemmyErrorType::ReportTooLong)? + } else { + Ok(()) + } +} + +pub fn read_auth_token(req: &HttpRequest) -> Result, LemmyError> { + // Try reading jwt from auth header + if let Ok(header) = Authorization::::parse(req) { + Ok(Some(header.as_ref().token().to_string())) + } + // If that fails, try to read from cookie + else if let Some(cookie) = &req.cookie(AUTH_COOKIE_NAME) { + Ok(Some(cookie.value().to_string())) + } + // Otherwise, there's no auth + else { + Ok(None) + } +} + +pub(crate) fn check_totp_2fa_valid( + local_user_view: &LocalUserView, + totp_token: &Option, + site_name: &str, +) -> LemmyResult<()> { + // Throw an error if their token is missing + let token = totp_token + .as_deref() + .ok_or(LemmyErrorType::MissingTotpToken)?; + let secret = local_user_view + .local_user + .totp_2fa_secret + .as_deref() + .ok_or(LemmyErrorType::MissingTotpSecret)?; + + let totp = build_totp_2fa(site_name, &local_user_view.person.name, secret)?; + + let check_passed = totp.check_current(token)?; + if !check_passed { + return Err(LemmyErrorType::IncorrectTotpToken.into()); + } + + Ok(()) +} + +pub(crate) fn generate_totp_2fa_secret() -> String { + Secret::generate_secret().to_string() +} + +pub(crate) fn build_totp_2fa( + site_name: &str, + username: &str, + secret: &str, +) -> Result { + let sec = Secret::Raw(secret.as_bytes().to_vec()); + let sec_bytes = sec + .to_bytes() + .map_err(|_| LemmyErrorType::CouldntParseTotpSecret)?; + + TOTP::new( + totp_rs::Algorithm::SHA1, + 6, + 1, + 30, + sec_bytes, + Some(site_name.to_string()), + username.to_string(), + ) + .with_lemmy_type(LemmyErrorType::CouldntGenerateTotp) +} + +#[tracing::instrument(skip_all)] +pub async fn local_user_view_from_jwt( + jwt: &str, + context: &LemmyContext, +) -> Result { + let local_user_id = Claims::validate(jwt, context) + .await + .with_lemmy_type(LemmyErrorType::NotLoggedIn)?; + let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?; + check_user_valid(&local_user_view.person)?; + + Ok(local_user_view) +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + #![allow(clippy::indexing_slicing)] + + use super::*; + + #[test] + fn test_build_totp() { + let generated_secret = generate_totp_2fa_secret(); + let totp = build_totp_2fa("lemmy", "my_name", &generated_secret); + assert!(totp.is_ok()); + } +} + diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs new file mode 100644 index 0000000000..b8142e7b72 --- /dev/null +++ b/crates/api_common/src/utils.rs @@ -0,0 +1,884 @@ +use crate::{ + context::LemmyContext, + request::purge_image_from_pictrs, + site::{FederatedInstances, InstanceWithFederationState}, +}; +use chrono::{DateTime, Days, Local, TimeZone, Utc}; +use enum_map::{enum_map, EnumMap}; +use lemmy_db_schema::{ + newtypes::{CommunityId, DbUrl, InstanceId, PersonId, PostId}, + source::{ + comment::{Comment, CommentUpdateForm}, + community::{Community, CommunityModerator, CommunityUpdateForm}, + community_block::CommunityBlock, + email_verification::{EmailVerification, EmailVerificationForm}, + instance::Instance, + instance_block::InstanceBlock, + local_site::LocalSite, + local_site_rate_limit::LocalSiteRateLimit, + password_reset_request::PasswordResetRequest, + person::{Person, PersonUpdateForm}, + person_block::PersonBlock, + post::{Post, PostRead}, + }, + traits::Crud, + utils::DbPool, +}; +use lemmy_db_views::{comment_view::CommentQuery, structs::LocalUserView}; +use lemmy_db_views_actor::structs::{ + CommunityModeratorView, + CommunityPersonBanView, + CommunityView, +}; +use lemmy_utils::{ + email::{send_email, translations::Lang}, + error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult}, + rate_limit::{ActionType, BucketConfig}, + settings::structs::Settings, + utils::slurs::build_slur_regex, +}; +use regex::Regex; +use rosetta_i18n::{Language, LanguageId}; +use std::collections::HashSet; +use tracing::warn; +use url::{ParseError, Url}; + +pub static AUTH_COOKIE_NAME: &str = "jwt"; + +#[tracing::instrument(skip_all)] +pub async fn is_mod_or_admin( + pool: &mut DbPool<'_>, + person: &Person, + community_id: CommunityId, +) -> Result<(), LemmyError> { + check_user_valid(person)?; + + let is_mod_or_admin = CommunityView::is_mod_or_admin(pool, person.id, community_id).await?; + if !is_mod_or_admin { + Err(LemmyErrorType::NotAModOrAdmin)? + } else { + Ok(()) + } +} + +#[tracing::instrument(skip_all)] +pub async fn is_mod_or_admin_opt( + pool: &mut DbPool<'_>, + local_user_view: Option<&LocalUserView>, + community_id: Option, +) -> Result<(), LemmyError> { + if let Some(local_user_view) = local_user_view { + if let Some(community_id) = community_id { + is_mod_or_admin(pool, &local_user_view.person, community_id).await + } else { + is_admin(local_user_view) + } + } else { + Err(LemmyErrorType::NotAModOrAdmin)? + } +} + +/// Check that a person is either a mod of any community, or an admin +/// +/// Should only be used for read operations +#[tracing::instrument(skip_all)] +pub async fn check_community_mod_of_any_or_admin_action( + local_user_view: &LocalUserView, + pool: &mut DbPool<'_>, +) -> LemmyResult<()> { + let person = &local_user_view.person; + + check_user_valid(person)?; + + let is_mod_of_any_or_admin = CommunityView::is_mod_of_any_or_admin(pool, person.id).await?; + if !is_mod_of_any_or_admin { + Err(LemmyErrorType::NotAModOrAdmin)? + } else { + Ok(()) + } +} + +pub fn is_admin(local_user_view: &LocalUserView) -> Result<(), LemmyError> { + check_user_valid(&local_user_view.person)?; + if !local_user_view.local_user.admin { + Err(LemmyErrorType::NotAnAdmin)? + } else if local_user_view.person.banned { + Err(LemmyErrorType::Banned)? + } else { + Ok(()) + } +} + +pub fn is_top_mod( + local_user_view: &LocalUserView, + community_mods: &[CommunityModeratorView], +) -> Result<(), LemmyError> { + check_user_valid(&local_user_view.person)?; + if local_user_view.person.id + != community_mods + .first() + .map(|cm| cm.moderator.id) + .unwrap_or(PersonId(0)) + { + Err(LemmyErrorType::NotTopMod)? + } else { + Ok(()) + } +} + +#[tracing::instrument(skip_all)] +pub async fn get_post(post_id: PostId, pool: &mut DbPool<'_>) -> Result { + Post::read(pool, post_id) + .await + .with_lemmy_type(LemmyErrorType::CouldntFindPost) +} + +#[tracing::instrument(skip_all)] +pub async fn mark_post_as_read( + person_id: PersonId, + post_id: PostId, + pool: &mut DbPool<'_>, +) -> Result<(), LemmyError> { + PostRead::mark_as_read(pool, HashSet::from([post_id]), person_id) + .await + .with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?; + Ok(()) +} + +pub fn check_user_valid(person: &Person) -> Result<(), LemmyError> { + // Check for a site ban + if person.banned { + Err(LemmyErrorType::SiteBan)? + } + // check for account deletion + else if person.deleted { + Err(LemmyErrorType::Deleted)? + } else { + 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 +/// the user isn't banned. +pub async fn check_community_user_action( + person: &Person, + community_id: CommunityId, + pool: &mut DbPool<'_>, +) -> LemmyResult<()> { + check_user_valid(person)?; + check_community_deleted_removed(community_id, pool).await?; + check_community_ban(person, community_id, pool).await?; + Ok(()) +} + +async fn check_community_deleted_removed( + community_id: CommunityId, + pool: &mut DbPool<'_>, +) -> LemmyResult<()> { + let community = Community::read(pool, community_id) + .await + .with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?; + if community.deleted || community.removed { + Err(LemmyErrorType::Deleted)? + } + Ok(()) +} + +async fn check_community_ban( + person: &Person, + community_id: CommunityId, + pool: &mut DbPool<'_>, +) -> LemmyResult<()> { + // check if user was banned from site or community + let is_banned = CommunityPersonBanView::get(pool, person.id, community_id).await?; + if is_banned { + Err(LemmyErrorType::BannedFromCommunity)? + } + Ok(()) +} + +/// Check that the given user can perform a mod action in the community. +/// +/// In particular it checks that he is an admin or mod, wasn't banned and the community isn't +/// removed/deleted. +pub async fn check_community_mod_action( + person: &Person, + community_id: CommunityId, + allow_deleted: bool, + pool: &mut DbPool<'_>, +) -> LemmyResult<()> { + is_mod_or_admin(pool, person, community_id).await?; + check_community_ban(person, community_id, pool).await?; + + // it must be possible to restore deleted community + if !allow_deleted { + check_community_deleted_removed(community_id, pool).await?; + } + Ok(()) +} + +pub fn check_post_deleted_or_removed(post: &Post) -> Result<(), LemmyError> { + if post.deleted || post.removed { + Err(LemmyErrorType::Deleted)? + } else { + Ok(()) + } +} + +/// Throws an error if a recipient has blocked a person. +#[tracing::instrument(skip_all)] +pub async fn check_person_block( + my_id: PersonId, + potential_blocker_id: PersonId, + pool: &mut DbPool<'_>, +) -> Result<(), LemmyError> { + let is_blocked = PersonBlock::read(pool, potential_blocker_id, my_id).await?; + if is_blocked { + Err(LemmyErrorType::PersonIsBlocked)? + } else { + Ok(()) + } +} + +/// Throws an error if a recipient has blocked a community. +#[tracing::instrument(skip_all)] +async fn check_community_block( + community_id: CommunityId, + person_id: PersonId, + pool: &mut DbPool<'_>, +) -> Result<(), LemmyError> { + let is_blocked = CommunityBlock::read(pool, person_id, community_id).await?; + if is_blocked { + Err(LemmyErrorType::CommunityIsBlocked)? + } else { + Ok(()) + } +} + +/// Throws an error if a recipient has blocked an instance. +#[tracing::instrument(skip_all)] +async fn check_instance_block( + instance_id: InstanceId, + person_id: PersonId, + pool: &mut DbPool<'_>, +) -> Result<(), LemmyError> { + let is_blocked = InstanceBlock::read(pool, person_id, instance_id).await?; + if is_blocked { + Err(LemmyErrorType::InstanceIsBlocked)? + } else { + Ok(()) + } +} + +#[tracing::instrument(skip_all)] +pub async fn check_person_instance_community_block( + my_id: PersonId, + potential_blocker_id: PersonId, + instance_id: InstanceId, + community_id: CommunityId, + pool: &mut DbPool<'_>, +) -> Result<(), LemmyError> { + check_person_block(my_id, potential_blocker_id, pool).await?; + check_instance_block(instance_id, potential_blocker_id, pool).await?; + check_community_block(community_id, potential_blocker_id, pool).await?; + Ok(()) +} + +#[tracing::instrument(skip_all)] +pub fn check_downvotes_enabled(score: i16, local_site: &LocalSite) -> Result<(), LemmyError> { + if score == -1 && !local_site.enable_downvotes { + Err(LemmyErrorType::DownvotesAreDisabled)? + } else { + Ok(()) + } +} + +/// Dont allow bots to do certain actions, like voting +#[tracing::instrument(skip_all)] +pub fn check_bot_account(person: &Person) -> Result<(), LemmyError> { + if person.bot_account { + Err(LemmyErrorType::InvalidBotAction)? + } else { + Ok(()) + } +} + +#[tracing::instrument(skip_all)] +pub fn check_private_instance( + local_user_view: &Option, + local_site: &LocalSite, +) -> Result<(), LemmyError> { + if local_user_view.is_none() && local_site.private_instance { + Err(LemmyErrorType::InstanceIsPrivate)? + } else { + Ok(()) + } +} + +#[tracing::instrument(skip_all)] +pub async fn build_federated_instances( + local_site: &LocalSite, + pool: &mut DbPool<'_>, +) -> Result, LemmyError> { + if local_site.federation_enabled { + let mut linked = Vec::new(); + let mut allowed = Vec::new(); + let mut blocked = Vec::new(); + + let all = Instance::read_all_with_fed_state(pool).await?; + for (instance, federation_state, is_blocked, is_allowed) in all { + let i = InstanceWithFederationState { + instance, + federation_state: federation_state.map(std::convert::Into::into), + }; + if is_blocked { + // blocked instances will only have an entry here if they had been federated with in the past. + blocked.push(i); + } else if is_allowed { + allowed.push(i.clone()); + linked.push(i); + } else { + // not explicitly allowed but implicitly linked + linked.push(i); + } + } + + Ok(Some(FederatedInstances { + linked, + allowed, + blocked, + })) + } else { + Ok(None) + } +} + +/// Checks the password length +pub fn password_length_check(pass: &str) -> Result<(), LemmyError> { + if !(10..=60).contains(&pass.chars().count()) { + Err(LemmyErrorType::InvalidPassword)? + } else { + Ok(()) + } +} + +/// Checks for a honeypot. If this field is filled, fail the rest of the function +pub fn honeypot_check(honeypot: &Option) -> Result<(), LemmyError> { + if honeypot.is_some() && honeypot != &Some(String::new()) { + Err(LemmyErrorType::HoneypotFailed)? + } else { + Ok(()) + } +} + +pub async fn send_email_to_user( + local_user_view: &LocalUserView, + subject: &str, + body: &str, + settings: &Settings, +) { + if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email { + return; + } + + if let Some(user_email) = &local_user_view.local_user.email { + match send_email( + subject, + user_email, + &local_user_view.person.name, + body, + settings, + ) + .await + { + Ok(_o) => _o, + Err(e) => warn!("{}", e), + }; + } +} + +pub async fn send_password_reset_email( + user: &LocalUserView, + pool: &mut DbPool<'_>, + settings: &Settings, +) -> Result<(), LemmyError> { + // Generate a random token + let token = uuid::Uuid::new_v4().to_string(); + + // Insert the row + let local_user_id = user.local_user.id; + PasswordResetRequest::create_token(pool, local_user_id, token.clone()).await?; + + let email = &user.local_user.email.clone().expect("email"); + let lang = get_interface_language(user); + let subject = &lang.password_reset_subject(&user.person.name); + let protocol_and_hostname = settings.get_protocol_and_hostname(); + let reset_link = format!("{}/password_change/{}", protocol_and_hostname, &token); + let body = &lang.password_reset_body(reset_link, &user.person.name); + send_email(subject, email, &user.person.name, body, settings).await +} + +/// Send a verification email +pub async fn send_verification_email( + user: &LocalUserView, + new_email: &str, + pool: &mut DbPool<'_>, + settings: &Settings, +) -> Result<(), LemmyError> { + let form = EmailVerificationForm { + local_user_id: user.local_user.id, + email: new_email.to_string(), + verification_token: uuid::Uuid::new_v4().to_string(), + }; + let verify_link = format!( + "{}/verify_email/{}", + settings.get_protocol_and_hostname(), + &form.verification_token + ); + EmailVerification::create(pool, &form).await?; + + let lang = get_interface_language(user); + let subject = lang.verify_email_subject(&settings.hostname); + let body = lang.verify_email_body(&settings.hostname, &user.person.name, verify_link); + send_email(&subject, new_email, &user.person.name, &body, settings).await?; + + Ok(()) +} + +pub fn get_interface_language(user: &LocalUserView) -> Lang { + lang_str_to_lang(&user.local_user.interface_language) +} + +pub fn get_interface_language_from_settings(user: &LocalUserView) -> Lang { + lang_str_to_lang(&user.local_user.interface_language) +} + +fn lang_str_to_lang(lang: &str) -> Lang { + let lang_id = LanguageId::new(lang); + Lang::from_language_id(&lang_id).unwrap_or_else(|| { + let en = LanguageId::new("en"); + Lang::from_language_id(&en).expect("default language") + }) +} + +pub fn local_site_rate_limit_to_rate_limit_config( + l: &LocalSiteRateLimit, +) -> EnumMap { + enum_map! { + ActionType::Message => (l.message, l.message_per_second), + ActionType::Post => (l.post, l.post_per_second), + ActionType::Register => (l.register, l.register_per_second), + ActionType::Image => (l.image, l.image_per_second), + ActionType::Comment => (l.comment, l.comment_per_second), + ActionType::Search => (l.search, l.search_per_second), + ActionType::ImportUserSettings => (l.import_user_settings, l.import_user_settings_per_second), + } + .map(|_key, (capacity, secs_to_refill)| BucketConfig { + capacity: u32::try_from(capacity).unwrap_or(0), + secs_to_refill: u32::try_from(secs_to_refill).unwrap_or(0), + }) +} + +pub fn local_site_to_slur_regex(local_site: &LocalSite) -> Option { + build_slur_regex(local_site.slur_filter_regex.as_deref()) +} + +pub fn local_site_opt_to_slur_regex(local_site: &Option) -> Option { + local_site + .as_ref() + .map(local_site_to_slur_regex) + .unwrap_or(None) +} + +pub fn local_site_opt_to_sensitive(local_site: &Option) -> bool { + local_site + .as_ref() + .map(|site| site.enable_nsfw) + .unwrap_or(false) +} + +pub async fn send_application_approved_email( + user: &LocalUserView, + settings: &Settings, +) -> Result<(), LemmyError> { + let email = &user.local_user.email.clone().expect("email"); + let lang = get_interface_language(user); + let subject = lang.registration_approved_subject(&user.person.actor_id); + let body = lang.registration_approved_body(&settings.hostname); + send_email(&subject, email, &user.person.name, &body, settings).await +} + +/// Send a new applicant email notification to all admins +pub async fn send_new_applicant_email_to_admins( + applicant_username: &str, + pool: &mut DbPool<'_>, + settings: &Settings, +) -> Result<(), LemmyError> { + // Collect the admins with emails + let admins = LocalUserView::list_admins_with_emails(pool).await?; + + let applications_link = &format!( + "{}/registration_applications", + settings.get_protocol_and_hostname(), + ); + + for admin in &admins { + let email = &admin.local_user.email.clone().expect("email"); + let lang = get_interface_language_from_settings(admin); + let subject = lang.new_application_subject(&settings.hostname, applicant_username); + let body = lang.new_application_body(applications_link); + send_email(&subject, email, &admin.person.name, &body, settings).await?; + } + Ok(()) +} + +/// Send a report to all admins +pub async fn send_new_report_email_to_admins( + reporter_username: &str, + reported_username: &str, + pool: &mut DbPool<'_>, + settings: &Settings, +) -> Result<(), LemmyError> { + // Collect the admins with emails + let admins = LocalUserView::list_admins_with_emails(pool).await?; + + let reports_link = &format!("{}/reports", settings.get_protocol_and_hostname(),); + + for admin in &admins { + let email = &admin.local_user.email.clone().expect("email"); + let lang = get_interface_language_from_settings(admin); + let subject = lang.new_report_subject(&settings.hostname, reported_username, reporter_username); + let body = lang.new_report_body(reports_link); + send_email(&subject, email, &admin.person.name, &body, settings).await?; + } + Ok(()) +} + +pub fn check_private_instance_and_federation_enabled( + local_site: &LocalSite, +) -> Result<(), LemmyError> { + if local_site.private_instance && local_site.federation_enabled { + Err(LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether)? + } else { + Ok(()) + } +} + +pub async fn purge_image_posts_for_person( + banned_person_id: PersonId, + context: &LemmyContext, +) -> Result<(), LemmyError> { + let pool = &mut context.pool(); + let posts = Post::fetch_pictrs_posts_for_creator(pool, banned_person_id).await?; + for post in posts { + if let Some(url) = post.url { + purge_image_from_pictrs(&url, context).await.ok(); + } + if let Some(thumbnail_url) = post.thumbnail_url { + purge_image_from_pictrs(&thumbnail_url, context).await.ok(); + } + } + + Post::remove_pictrs_post_images_and_thumbnails_for_creator(pool, banned_person_id).await?; + + Ok(()) +} + +pub async fn purge_image_posts_for_community( + banned_community_id: CommunityId, + context: &LemmyContext, +) -> Result<(), LemmyError> { + let pool = &mut context.pool(); + let posts = Post::fetch_pictrs_posts_for_community(pool, banned_community_id).await?; + for post in posts { + if let Some(url) = post.url { + purge_image_from_pictrs(&url, context).await.ok(); + } + if let Some(thumbnail_url) = post.thumbnail_url { + purge_image_from_pictrs(&thumbnail_url, context).await.ok(); + } + } + + Post::remove_pictrs_post_images_and_thumbnails_for_community(pool, banned_community_id).await?; + + Ok(()) +} + +pub async fn remove_user_data( + banned_person_id: PersonId, + context: &LemmyContext, +) -> Result<(), LemmyError> { + let pool = &mut context.pool(); + // Purge user images + let person = Person::read(pool, banned_person_id).await?; + if let Some(avatar) = person.avatar { + purge_image_from_pictrs(&avatar, context).await.ok(); + } + if let Some(banner) = person.banner { + purge_image_from_pictrs(&banner, context).await.ok(); + } + + // Update the fields to None + Person::update( + pool, + banned_person_id, + &PersonUpdateForm { + avatar: Some(None), + banner: Some(None), + bio: Some(None), + ..Default::default() + }, + ) + .await?; + + // Posts + Post::update_removed_for_creator(pool, banned_person_id, None, true).await?; + + // Purge image posts + purge_image_posts_for_person(banned_person_id, context).await?; + + // Communities + // Remove all communities where they're the top mod + // for now, remove the communities manually + let first_mod_communities = CommunityModeratorView::get_community_first_mods(pool).await?; + + // Filter to only this banned users top communities + let banned_user_first_communities: Vec = first_mod_communities + .into_iter() + .filter(|fmc| fmc.moderator.id == banned_person_id) + .collect(); + + for first_mod_community in banned_user_first_communities { + let community_id = first_mod_community.community.id; + Community::update( + pool, + community_id, + &CommunityUpdateForm { + removed: Some(true), + ..Default::default() + }, + ) + .await?; + + // Delete the community images + if let Some(icon) = first_mod_community.community.icon { + purge_image_from_pictrs(&icon, context).await.ok(); + } + if let Some(banner) = first_mod_community.community.banner { + purge_image_from_pictrs(&banner, context).await.ok(); + } + // Update the fields to None + Community::update( + pool, + community_id, + &CommunityUpdateForm { + icon: Some(None), + banner: Some(None), + ..Default::default() + }, + ) + .await?; + } + + // Comments + Comment::update_removed_for_creator(pool, banned_person_id, true).await?; + + Ok(()) +} + +pub async fn remove_user_data_in_community( + community_id: CommunityId, + banned_person_id: PersonId, + pool: &mut DbPool<'_>, +) -> Result<(), LemmyError> { + // Posts + Post::update_removed_for_creator(pool, banned_person_id, Some(community_id), true).await?; + + // Comments + // TODO Diesel doesn't allow updates with joins, so this has to be a loop + let comments = CommentQuery { + creator_id: Some(banned_person_id), + community_id: Some(community_id), + ..Default::default() + } + .list(pool) + .await?; + + for comment_view in &comments { + let comment_id = comment_view.comment.id; + Comment::update( + pool, + comment_id, + &CommentUpdateForm { + removed: Some(true), + ..Default::default() + }, + ) + .await?; + } + + Ok(()) +} + +pub async fn purge_user_account( + person_id: PersonId, + context: &LemmyContext, +) -> Result<(), LemmyError> { + let pool = &mut context.pool(); + // Delete their images + let person = Person::read(pool, person_id).await?; + if let Some(avatar) = person.avatar { + purge_image_from_pictrs(&avatar, context).await.ok(); + } + if let Some(banner) = person.banner { + purge_image_from_pictrs(&banner, context).await.ok(); + } + // No need to update avatar and banner, those are handled in Person::delete_account + + // Comments + Comment::permadelete_for_creator(pool, person_id) + .await + .with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?; + + // Posts + Post::permadelete_for_creator(pool, person_id) + .await + .with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?; + + // Purge image posts + purge_image_posts_for_person(person_id, context).await?; + + // Leave communities they mod + CommunityModerator::leave_all_communities(pool, person_id).await?; + + Person::delete_account(pool, person_id).await?; + + Ok(()) +} + +pub enum EndpointType { + Community, + Person, + Post, + Comment, + PrivateMessage, +} + +/// Generates an apub endpoint for a given domain, IE xyz.tld +pub fn generate_local_apub_endpoint( + endpoint_type: EndpointType, + name: &str, + domain: &str, +) -> Result { + let point = match endpoint_type { + EndpointType::Community => "c", + EndpointType::Person => "u", + EndpointType::Post => "post", + EndpointType::Comment => "comment", + EndpointType::PrivateMessage => "private_message", + }; + + Ok(Url::parse(&format!("{domain}/{point}/{name}"))?.into()) +} + +pub fn generate_followers_url(actor_id: &DbUrl) -> Result { + Ok(Url::parse(&format!("{actor_id}/followers"))?.into()) +} + +pub fn generate_inbox_url(actor_id: &DbUrl) -> Result { + Ok(Url::parse(&format!("{actor_id}/inbox"))?.into()) +} + +pub fn generate_shared_inbox_url(settings: &Settings) -> Result { + let url = format!("{}/inbox", settings.get_protocol_and_hostname()); + Ok(Url::parse(&url)?.into()) +} + +pub fn generate_outbox_url(actor_id: &DbUrl) -> Result { + Ok(Url::parse(&format!("{actor_id}/outbox"))?.into()) +} + +pub fn generate_featured_url(actor_id: &DbUrl) -> Result { + Ok(Url::parse(&format!("{actor_id}/featured"))?.into()) +} + +pub fn generate_moderators_url(community_id: &DbUrl) -> Result { + Ok(Url::parse(&format!("{community_id}/moderators"))?.into()) +} + +/// Ensure that ban/block expiry is in valid range. If its in past, throw error. If its more +/// than 10 years in future, convert to permanent ban. Otherwise return the same value. +pub fn check_expire_time(expires_unix_opt: Option) -> LemmyResult>> { + if let Some(expires_unix) = expires_unix_opt { + let expires = Utc + .timestamp_opt(expires_unix, 0) + .single() + .ok_or(LemmyErrorType::InvalidUnixTime)?; + + limit_expire_time(expires) + } else { + Ok(None) + } +} + +fn limit_expire_time(expires: DateTime) -> LemmyResult>> { + const MAX_BAN_TERM: Days = Days::new(10 * 365); + + if expires < Local::now() { + Err(LemmyErrorType::BanExpirationInPast)? + } else if expires > Local::now() + MAX_BAN_TERM { + Ok(None) + } else { + Ok(Some(expires)) + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + #![allow(clippy::indexing_slicing)] + + use crate::utils::{honeypot_check, limit_expire_time, password_length_check}; + use chrono::{Days, Utc}; + + #[test] + #[rustfmt::skip] + fn password_length() { + assert!(password_length_check("Õ¼¾°3yË,o¸ãtÌÈú|ÇÁÙAøüÒI©·¤(T]/ð>æºWæ[C¤bªWöaÃÎñ·{=û³&§½K/c").is_ok()); + assert!(password_length_check("1234567890").is_ok()); + assert!(password_length_check("short").is_err()); + assert!(password_length_check("looooooooooooooooooooooooooooooooooooooooooooooooooooooooooong").is_err()); + } + + #[test] + fn honeypot() { + assert!(honeypot_check(&None).is_ok()); + assert!(honeypot_check(&Some(String::new())).is_ok()); + assert!(honeypot_check(&Some("1".to_string())).is_err()); + assert!(honeypot_check(&Some("message".to_string())).is_err()); + } + + #[test] + fn test_limit_ban_term() { + // Ban expires in past, should throw error + assert!(limit_expire_time(Utc::now() - Days::new(5)).is_err()); + + // Legitimate ban term, return same value + let fourteen_days = Utc::now() + Days::new(14); + assert_eq!( + limit_expire_time(fourteen_days).unwrap(), + Some(fourteen_days) + ); + let nine_years = Utc::now() + Days::new(365 * 9); + assert_eq!(limit_expire_time(nine_years).unwrap(), Some(nine_years)); + + // Too long ban term, changes to None (permanent ban) + assert_eq!( + limit_expire_time(Utc::now() + Days::new(365 * 11)).unwrap(), + None + ); + } +} + diff --git a/crates/db_schema/src/impls/external_auth.rs b/crates/db_schema/src/impls/external_auth.rs index 567bc3a212..44bd132c60 100644 --- a/crates/db_schema/src/impls/external_auth.rs +++ b/crates/db_schema/src/impls/external_auth.rs @@ -28,7 +28,7 @@ impl ExternalAuth { } pub async fn delete( pool: &mut DbPool<'_>, - external_auth_id: ExternalAuthId + external_auth_id: ExternalAuthId, ) -> Result { let conn = &mut get_conn(pool).await?; diesel::delete(external_auth.find(external_auth_id)) diff --git a/crates/db_views/src/external_auth_view.rs b/crates/db_views/src/external_auth_view.rs index c03dd4dabf..9c5cb390b0 100644 --- a/crates/db_views/src/external_auth_view.rs +++ b/crates/db_views/src/external_auth_view.rs @@ -30,7 +30,7 @@ impl ExternalAuthView { // and will only be needed when performing an oauth request by the server pub async fn get_client_secret( pool: &mut DbPool<'_>, - external_auth_id: ExternalAuthId + external_auth_id: ExternalAuthId, ) -> Result { let conn = &mut get_conn(pool).await?; let external_auths = external_auth::table diff --git a/migrations/2023-11-03-131721_create_external_auth/down.sql b/migrations/2023-11-03-131721_create_external_auth/down.sql index d619666427..428ad4f888 100644 --- a/migrations/2023-11-03-131721_create_external_auth/down.sql +++ b/migrations/2023-11-03-131721_create_external_auth/down.sql @@ -1 +1,2 @@ DROP TABLE external_auth; + From 3361faf3c96191be88f80a7cfd75c697e67600f2 Mon Sep 17 00:00:00 2001 From: Anthony Lawn Date: Fri, 8 Dec 2023 13:59:17 -0600 Subject: [PATCH 08/18] More fixes --- crates/api/src/lib.rs | 1 - crates/api/src/local_user/oauth_callback.rs | 4 ++-- crates/api_common/src/utils.rs | 1 - 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 9a55000217..faa74824ec 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -169,4 +169,3 @@ mod tests { assert!(totp.is_ok()); } } - diff --git a/crates/api/src/local_user/oauth_callback.rs b/crates/api/src/local_user/oauth_callback.rs index fb37502755..f06bb5c963 100644 --- a/crates/api/src/local_user/oauth_callback.rs +++ b/crates/api/src/local_user/oauth_callback.rs @@ -19,7 +19,7 @@ pub async fn oauth_callback( ) -> HttpResponse { let site_view = SiteView::read_local(&mut context.pool()).await; - if !site_view.is_ok() { + if !site_view.is_ok() { return HttpResponse::Found() .append_header(("Location", "/login?err=internal")) .finish(); @@ -60,7 +60,7 @@ pub async fn oauth_callback( { external_auth.issuer.to_string() } else { - format!("{}/.well-known/openid-configuration", external_auth.issuer) + format!("{}/.well-known/openid-configuration", external_auth.issuer) }; let res = context.client().get(discovery_endpoint).send().await; if !res.is_ok() { diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index b8142e7b72..0ea27f794c 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -881,4 +881,3 @@ mod tests { ); } } - From fc6206eac2ce6a7d5b71c24f6376a6b50e580e8d Mon Sep 17 00:00:00 2001 From: Anthony Lawn Date: Fri, 8 Dec 2023 17:34:09 -0600 Subject: [PATCH 09/18] Fixed update issues --- crates/api/src/local_user/oauth_callback.rs | 16 ++++++++++++---- crates/api_crud/src/user/create.rs | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/api/src/local_user/oauth_callback.rs b/crates/api/src/local_user/oauth_callback.rs index f06bb5c963..96813c7478 100644 --- a/crates/api/src/local_user/oauth_callback.rs +++ b/crates/api/src/local_user/oauth_callback.rs @@ -1,10 +1,16 @@ use activitypub_federation::config::Data; -use actix_web::{http::StatusCode, web::Query, HttpRequest, HttpResponse}; +use actix_web::{ + cookie::{Cookie, SameSite}, + http::StatusCode, + web::Query, + HttpRequest, + HttpResponse, +}; use lemmy_api_common::{ claims::Claims, context::LemmyContext, external_auth::{OAuth, OAuthResponse, TokenResponse}, - utils::create_login_cookie, + utils::AUTH_COOKIE_NAME, }; use lemmy_api_crud::user::create::register_from_oauth; use lemmy_db_schema::{newtypes::ExternalAuthId, source::local_user::LocalUser, RegistrationMode}; @@ -224,9 +230,11 @@ pub async fn oauth_callback( let mut res = HttpResponse::build(StatusCode::FOUND) .insert_header(("Location", oauth_state.client_redirect_uri)) .finish(); - let mut cookie = create_login_cookie(jwt.unwrap()); - cookie.set_path("/"); + let mut cookie = Cookie::new(AUTH_COOKIE_NAME, jwt.unwrap().into_inner()); + cookie.set_secure(true); + cookie.set_same_site(SameSite::Lax); cookie.set_http_only(false); // We'll need to access the cookie via document.cookie for this req + cookie.set_path("/"); if !res.add_cookie(&cookie).is_ok() { return HttpResponse::Found() .append_header(("Location", "/login?err=jwt")) diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index ee8124f8f1..cd1bbe0683 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -229,7 +229,7 @@ pub async fn register_from_oauth( .private_key(Some(actor_keypair.private_key)) .public_key(actor_keypair.public_key) .inbox_url(Some(generate_inbox_url(&actor_id)?)) - .shared_inbox_url(Some(generate_shared_inbox_url(&actor_id)?)) + .shared_inbox_url(Some(generate_shared_inbox_url(context.settings())?)) .instance_id(site_view.site.instance_id) .build(); From 4c1c2904285b146baa7ca5d4d1a7562db28a1e51 Mon Sep 17 00:00:00 2001 From: Anthony Lawn Date: Mon, 11 Dec 2023 06:42:30 -0600 Subject: [PATCH 10/18] Updated last diesel migration --- crates/db_schema/src/schema.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 744fd0001c..1df007f305 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -289,8 +289,8 @@ diesel::table! { client_id -> Text, client_secret -> Text, scopes -> Text, - published -> Timestamptz, - updated -> Nullable, + published -> Timestamp, + updated -> Nullable, } } @@ -389,9 +389,9 @@ diesel::table! { published -> Timestamptz, updated -> Nullable, registration_mode -> RegistrationModeEnum, - oauth_registration -> Bool, reports_email_admins -> Bool, federation_signed_fetch -> Bool, + oauth_registration -> Bool, } } From 31518f1767d6ffdd243c6f53ddf48de5921d4ca1 Mon Sep 17 00:00:00 2001 From: Anthony Lawn Date: Wed, 13 Dec 2023 05:36:35 -0600 Subject: [PATCH 11/18] Cleaned up migrations --- migrations/2023-11-03-131721_create_external_auth/down.sql | 3 +++ migrations/2023-11-03-131721_create_external_auth/up.sql | 7 +++++-- .../2023-12-07-200127_add_oauth_registration/down.sql | 3 --- migrations/2023-12-07-200127_add_oauth_registration/up.sql | 3 --- 4 files changed, 8 insertions(+), 8 deletions(-) delete mode 100644 migrations/2023-12-07-200127_add_oauth_registration/down.sql delete mode 100644 migrations/2023-12-07-200127_add_oauth_registration/up.sql diff --git a/migrations/2023-11-03-131721_create_external_auth/down.sql b/migrations/2023-11-03-131721_create_external_auth/down.sql index 428ad4f888..cc80dc62b6 100644 --- a/migrations/2023-11-03-131721_create_external_auth/down.sql +++ b/migrations/2023-11-03-131721_create_external_auth/down.sql @@ -1,2 +1,5 @@ DROP TABLE external_auth; +ALTER TABLE local_site + DROP COLUMN oauth_registration; + diff --git a/migrations/2023-11-03-131721_create_external_auth/up.sql b/migrations/2023-11-03-131721_create_external_auth/up.sql index 0cf1140829..58fadafe89 100644 --- a/migrations/2023-11-03-131721_create_external_auth/up.sql +++ b/migrations/2023-11-03-131721_create_external_auth/up.sql @@ -11,7 +11,10 @@ CREATE TABLE external_auth ( client_id text NOT NULL UNIQUE, client_secret text NOT NULL, scopes text NOT NULL, - published timestamp without time zone DEFAULT now() NOT NULL, - updated timestamp without time zone + published timestamptz without time zone DEFAULT now() NOT NULL, + updated timestamptz without time zone ); +ALTER TABLE local_site + ADD COLUMN oauth_registration boolean DEFAULT FALSE NOT NULL; + diff --git a/migrations/2023-12-07-200127_add_oauth_registration/down.sql b/migrations/2023-12-07-200127_add_oauth_registration/down.sql deleted file mode 100644 index 8caef3c015..0000000000 --- a/migrations/2023-12-07-200127_add_oauth_registration/down.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE local_site - DROP COLUMN oauth_registration; - diff --git a/migrations/2023-12-07-200127_add_oauth_registration/up.sql b/migrations/2023-12-07-200127_add_oauth_registration/up.sql deleted file mode 100644 index 041d5d4811..0000000000 --- a/migrations/2023-12-07-200127_add_oauth_registration/up.sql +++ /dev/null @@ -1,3 +0,0 @@ -ALTER TABLE local_site - ADD COLUMN oauth_registration boolean DEFAULT FALSE NOT NULL; - From 601d53a8669f22577ce942c87c6582b25a11ee91 Mon Sep 17 00:00:00 2001 From: Anthony Lawn Date: Wed, 13 Dec 2023 07:27:53 -0600 Subject: [PATCH 12/18] Remove external_auth.local_site_id --- crates/api/src/site/leave_admin.rs | 2 +- crates/api_crud/src/external_auth/create.rs | 2 -- crates/api_crud/src/external_auth/update.rs | 1 - crates/api_crud/src/site/read.rs | 2 +- crates/db_schema/src/schema.rs | 2 -- crates/db_schema/src/source/external_auth.rs | 11 ++--------- crates/db_views/src/external_auth_view.rs | 11 +++-------- .../2023-11-03-131721_create_external_auth/up.sql | 1 - 8 files changed, 7 insertions(+), 25 deletions(-) diff --git a/crates/api/src/site/leave_admin.rs b/crates/api/src/site/leave_admin.rs index e2ff07ec38..607c8e3c72 100644 --- a/crates/api/src/site/leave_admin.rs +++ b/crates/api/src/site/leave_admin.rs @@ -63,7 +63,7 @@ pub async fn leave_admin( let custom_emojis = CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?; let external_auths = - ExternalAuthView::get_all(&mut context.pool(), site_view.local_site.id).await?; + ExternalAuthView::get_all(&mut context.pool()).await?; Ok(Json(GetSiteResponse { site_view, diff --git a/crates/api_crud/src/external_auth/create.rs b/crates/api_crud/src/external_auth/create.rs index ae7f65b5d0..6d4ceb95a0 100644 --- a/crates/api_crud/src/external_auth/create.rs +++ b/crates/api_crud/src/external_auth/create.rs @@ -18,13 +18,11 @@ pub async fn create_external_auth( context: Data, local_user_view: LocalUserView, ) -> Result, LemmyError> { - let local_site = LocalSite::read(&mut context.pool()).await?; // Make sure user is an admin is_admin(&local_user_view)?; let cloned_data = data.clone(); let external_auth_form = ExternalAuthInsertForm::builder() - .local_site_id(local_site.id) .display_name(cloned_data.display_name.into()) .auth_type(data.auth_type.to_string()) .auth_endpoint(cloned_data.auth_endpoint.into()) diff --git a/crates/api_crud/src/external_auth/update.rs b/crates/api_crud/src/external_auth/update.rs index 505ee8adbf..82c2c32bac 100644 --- a/crates/api_crud/src/external_auth/update.rs +++ b/crates/api_crud/src/external_auth/update.rs @@ -24,7 +24,6 @@ pub async fn update_external_auth( let cloned_data = data.clone(); let external_auth_form = ExternalAuthUpdateForm::builder() - .local_site_id(local_site.id) .display_name(cloned_data.display_name.into()) .auth_type(data.auth_type.to_string()) .auth_endpoint(cloned_data.auth_endpoint.into()) diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index 31459c67a8..5cb2a9215b 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -81,7 +81,7 @@ pub async fn get_site( let custom_emojis = CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?; let external_auths = - ExternalAuthView::get_all(&mut context.pool(), site_view.local_site.id).await?; + ExternalAuthView::get_all(&mut context.pool()).await?; Ok(Json(GetSiteResponse { site_view, diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 1df007f305..29029a78f3 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -277,7 +277,6 @@ diesel::table! { diesel::table! { external_auth (id) { id -> Int4, - local_site_id -> Int4, display_name -> Text, #[max_length = 128] auth_type -> Varchar, @@ -939,7 +938,6 @@ diesel::joinable!(community_person_ban -> person (person_id)); diesel::joinable!(custom_emoji -> local_site (local_site_id)); diesel::joinable!(custom_emoji_keyword -> custom_emoji (custom_emoji_id)); diesel::joinable!(email_verification -> local_user (local_user_id)); -diesel::joinable!(external_auth -> local_site (local_site_id)); diesel::joinable!(federation_allowlist -> instance (instance_id)); diesel::joinable!(federation_blocklist -> instance (instance_id)); diesel::joinable!(federation_queue_state -> instance (instance_id)); diff --git a/crates/db_schema/src/source/external_auth.rs b/crates/db_schema/src/source/external_auth.rs index 7886d0cbca..b119e488ee 100644 --- a/crates/db_schema/src/source/external_auth.rs +++ b/crates/db_schema/src/source/external_auth.rs @@ -1,4 +1,4 @@ -use crate::newtypes::{ExternalAuthId, LocalSiteId}; +use crate::newtypes::ExternalAuthId; #[cfg(feature = "full")] use crate::schema::external_auth; use chrono::{DateTime, Utc}; @@ -10,17 +10,12 @@ use typed_builder::TypedBuilder; #[skip_serializing_none] #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] -#[cfg_attr(feature = "full", derive(Queryable, Associations, Identifiable, TS))] +#[cfg_attr(feature = "full", derive(Queryable, Identifiable, TS))] #[cfg_attr(feature = "full", diesel(table_name = external_auth))] -#[cfg_attr( - feature = "full", - diesel(belongs_to(crate::source::local_site::LocalSite)) -)] #[cfg_attr(feature = "full", ts(export))] /// An external auth method. pub struct ExternalAuth { pub id: ExternalAuthId, - pub local_site_id: LocalSiteId, pub display_name: String, pub auth_type: String, pub auth_endpoint: String, @@ -39,7 +34,6 @@ pub struct ExternalAuth { #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = external_auth))] pub struct ExternalAuthInsertForm { - pub local_site_id: LocalSiteId, pub display_name: String, pub auth_type: String, pub auth_endpoint: String, @@ -56,7 +50,6 @@ pub struct ExternalAuthInsertForm { #[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] #[cfg_attr(feature = "full", diesel(table_name = external_auth))] pub struct ExternalAuthUpdateForm { - pub local_site_id: LocalSiteId, pub display_name: String, pub auth_type: String, pub auth_endpoint: String, diff --git a/crates/db_views/src/external_auth_view.rs b/crates/db_views/src/external_auth_view.rs index 9c5cb390b0..b1039ab679 100644 --- a/crates/db_views/src/external_auth_view.rs +++ b/crates/db_views/src/external_auth_view.rs @@ -1,8 +1,8 @@ use crate::structs::ExternalAuthView; -use diesel::{result::Error, ExpressionMethods, QueryDsl}; +use diesel::{result::Error, QueryDsl}; use diesel_async::RunQueryDsl; use lemmy_db_schema::{ - newtypes::{ExternalAuthId, LocalSiteId}, + newtypes::ExternalAuthId, schema::external_auth, source::external_auth::ExternalAuth, utils::{get_conn, DbPool}, @@ -45,13 +45,9 @@ impl ExternalAuthView { } } - pub async fn get_all( - pool: &mut DbPool<'_>, - for_local_site_id: LocalSiteId, - ) -> Result, Error> { + pub async fn get_all(pool: &mut DbPool<'_>) -> Result, Error> { let conn = &mut get_conn(pool).await?; let external_auths = external_auth::table - .filter(external_auth::local_site_id.eq(for_local_site_id)) .order(external_auth::id) .select(external_auth::all_columns) .load::(conn) @@ -67,7 +63,6 @@ impl ExternalAuthView { // Can't just clone entire object because client_secret must be stripped external_auth: ExternalAuth { id: item.id.clone(), - local_site_id: item.local_site_id.clone(), display_name: item.display_name.clone(), auth_type: item.auth_type.clone(), auth_endpoint: item.auth_endpoint.clone(), diff --git a/migrations/2023-11-03-131721_create_external_auth/up.sql b/migrations/2023-11-03-131721_create_external_auth/up.sql index 58fadafe89..419b2d1f01 100644 --- a/migrations/2023-11-03-131721_create_external_auth/up.sql +++ b/migrations/2023-11-03-131721_create_external_auth/up.sql @@ -1,6 +1,5 @@ CREATE TABLE external_auth ( id serial PRIMARY KEY, - local_site_id int REFERENCES local_site ON UPDATE CASCADE ON DELETE CASCADE NOT NULL, display_name text NOT NULL, auth_type varchar(128) NOT NULL, auth_endpoint text NOT NULL, From 64ee7c97f60c60cefa06345cd0edbc7fe82571e1 Mon Sep 17 00:00:00 2001 From: Anthony Lawn Date: Wed, 13 Dec 2023 07:28:19 -0600 Subject: [PATCH 13/18] Fix missed timestamp -> timestamptz --- crates/db_schema/src/schema.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index 29029a78f3..5a0a5ce30f 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -288,8 +288,8 @@ diesel::table! { client_id -> Text, client_secret -> Text, scopes -> Text, - published -> Timestamp, - updated -> Nullable, + published -> Timestamptz, + updated -> Nullable, } } From 069d626ecf5d9ab6ce985deaa11780a5ce4e6816 Mon Sep 17 00:00:00 2001 From: Anthony Lawn Date: Wed, 13 Dec 2023 07:34:31 -0600 Subject: [PATCH 14/18] Documented oauth_callback --- crates/api/src/local_user/oauth_callback.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/api/src/local_user/oauth_callback.rs b/crates/api/src/local_user/oauth_callback.rs index 96813c7478..4a9a14a610 100644 --- a/crates/api/src/local_user/oauth_callback.rs +++ b/crates/api/src/local_user/oauth_callback.rs @@ -17,6 +17,7 @@ use lemmy_db_schema::{newtypes::ExternalAuthId, source::local_user::LocalUser, R use lemmy_db_views::structs::{ExternalAuthView, LocalUserView, SiteView}; use url::Url; +// Login user using response from external identity provider #[tracing::instrument(skip(context))] pub async fn oauth_callback( data: Query, From ddc3aee22306769b0204c76b871ccc39c52e584d Mon Sep 17 00:00:00 2001 From: Anthony Lawn Date: Wed, 13 Dec 2023 08:21:49 -0600 Subject: [PATCH 15/18] Moved part of registration code to utility function --- crates/api_crud/src/external_auth/create.rs | 5 +- crates/api_crud/src/external_auth/update.rs | 6 +- crates/api_crud/src/user/create.rs | 88 ++++++++++----------- 3 files changed, 46 insertions(+), 53 deletions(-) diff --git a/crates/api_crud/src/external_auth/create.rs b/crates/api_crud/src/external_auth/create.rs index 6d4ceb95a0..7b69e43c87 100644 --- a/crates/api_crud/src/external_auth/create.rs +++ b/crates/api_crud/src/external_auth/create.rs @@ -5,10 +5,7 @@ use lemmy_api_common::{ external_auth::{CreateExternalAuth, ExternalAuthResponse}, utils::is_admin, }; -use lemmy_db_schema::source::{ - external_auth::{ExternalAuth, ExternalAuthInsertForm}, - local_site::LocalSite, -}; +use lemmy_db_schema::source::external_auth::{ExternalAuth, ExternalAuthInsertForm}; use lemmy_db_views::structs::{ExternalAuthView, LocalUserView}; use lemmy_utils::error::LemmyError; diff --git a/crates/api_crud/src/external_auth/update.rs b/crates/api_crud/src/external_auth/update.rs index 82c2c32bac..efbd0dd622 100644 --- a/crates/api_crud/src/external_auth/update.rs +++ b/crates/api_crud/src/external_auth/update.rs @@ -5,10 +5,7 @@ use lemmy_api_common::{ external_auth::{EditExternalAuth, ExternalAuthResponse}, utils::is_admin, }; -use lemmy_db_schema::source::{ - external_auth::{ExternalAuth, ExternalAuthUpdateForm}, - local_site::LocalSite, -}; +use lemmy_db_schema::source::external_auth::{ExternalAuth, ExternalAuthUpdateForm}; use lemmy_db_views::structs::{ExternalAuthView, LocalUserView}; use lemmy_utils::error::LemmyError; @@ -18,7 +15,6 @@ pub async fn update_external_auth( context: Data, local_user_view: LocalUserView, ) -> Result, LemmyError> { - let local_site = LocalSite::read(&mut context.pool()).await?; // Make sure user is an admin is_admin(&local_user_view)?; diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index cd1bbe0683..8a133ec1d8 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -18,8 +18,10 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ aggregates::structs::PersonAggregates, + newtypes::InstanceId, source::{ captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer}, + local_site::LocalSite, local_user::{LocalUser, LocalUserInsertForm}, person::{Person, PersonInsertForm}, registration_application::{RegistrationApplication, RegistrationApplicationInsertForm}, @@ -89,14 +91,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)? @@ -104,22 +98,13 @@ pub async fn register( } // We have to create both a person, and local_user - - // Register the new person - let person_form = PersonInsertForm::builder() - .name(data.username.clone()) - .actor_id(Some(actor_id.clone())) - .private_key(Some(actor_keypair.private_key)) - .public_key(actor_keypair.public_key) - .inbox_url(Some(generate_inbox_url(&actor_id)?)) - .shared_inbox_url(Some(generate_shared_inbox_url(context.settings())?)) - .instance_id(site_view.site.instance_id) - .build(); - - // 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. @@ -212,6 +197,38 @@ pub async fn register_from_oauth( let slur_regex = local_site_to_slur_regex(&local_site); check_slurs(&username, &slur_regex)?; + // We have to create both a person, and local_user + let inserted_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(inserted_person.id) + .email(Some(str::to_lowercase(&email))) + .password_encrypted("".to_string()) + .show_nsfw(Some(false)) + .accepted_application(Some(true)) + .email_verified(Some(true)) + .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(); + + let inserted_local_user = LocalUser::create(&mut context.pool(), &local_user_form).await?; + Ok(inserted_local_user) +} + +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( @@ -220,37 +237,20 @@ pub async fn register_from_oauth( &context.settings().get_protocol_and_hostname(), )?; - // We have to create both a person, and local_user - // Register the new person let person_form = PersonInsertForm::builder() - .name(username.clone()) + .name(username) .actor_id(Some(actor_id.clone())) .private_key(Some(actor_keypair.private_key)) .public_key(actor_keypair.public_key) .inbox_url(Some(generate_inbox_url(&actor_id)?)) .shared_inbox_url(Some(generate_shared_inbox_url(context.settings())?)) - .instance_id(site_view.site.instance_id) + .instance_id(instance_id) .build(); // insert the person let inserted_person = Person::create(&mut context.pool(), &person_form) .await .with_lemmy_type(LemmyErrorType::UserAlreadyExists)?; - - // Create the local user - let local_user_form = LocalUserInsertForm::builder() - .person_id(inserted_person.id) - .email(Some(str::to_lowercase(&email))) - .password_encrypted("".to_string()) - .show_nsfw(Some(false)) - .accepted_application(Some(true)) - .email_verified(Some(true)) - .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(); - - let inserted_local_user = LocalUser::create(&mut context.pool(), &local_user_form).await?; - Ok(inserted_local_user) + Ok(inserted_person) } From c55b65690af0be0c7965d80dbf5fd2de1dc5ab7a Mon Sep 17 00:00:00 2001 From: Anthony Lawn Date: Wed, 13 Dec 2023 09:08:07 -0600 Subject: [PATCH 16/18] Use CRUD trait --- crates/api_crud/src/external_auth/create.rs | 5 ++++- crates/api_crud/src/external_auth/delete.rs | 5 ++++- crates/api_crud/src/external_auth/update.rs | 5 ++++- crates/db_schema/src/impls/external_auth.rs | 14 ++++++++++---- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/crates/api_crud/src/external_auth/create.rs b/crates/api_crud/src/external_auth/create.rs index 7b69e43c87..2c2e8a82ba 100644 --- a/crates/api_crud/src/external_auth/create.rs +++ b/crates/api_crud/src/external_auth/create.rs @@ -5,7 +5,10 @@ use lemmy_api_common::{ external_auth::{CreateExternalAuth, ExternalAuthResponse}, utils::is_admin, }; -use lemmy_db_schema::source::external_auth::{ExternalAuth, ExternalAuthInsertForm}; +use lemmy_db_schema::{ + source::external_auth::{ExternalAuth, ExternalAuthInsertForm}, + traits::Crud, +}; use lemmy_db_views::structs::{ExternalAuthView, LocalUserView}; use lemmy_utils::error::LemmyError; diff --git a/crates/api_crud/src/external_auth/delete.rs b/crates/api_crud/src/external_auth/delete.rs index f6c77b7f98..e832282797 100644 --- a/crates/api_crud/src/external_auth/delete.rs +++ b/crates/api_crud/src/external_auth/delete.rs @@ -6,7 +6,10 @@ use lemmy_api_common::{ utils::is_admin, SuccessResponse, }; -use lemmy_db_schema::source::external_auth::ExternalAuth; +use lemmy_db_schema::{ + source::external_auth::ExternalAuth, + traits::Crud, +}; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyError; diff --git a/crates/api_crud/src/external_auth/update.rs b/crates/api_crud/src/external_auth/update.rs index efbd0dd622..62eab2794f 100644 --- a/crates/api_crud/src/external_auth/update.rs +++ b/crates/api_crud/src/external_auth/update.rs @@ -5,7 +5,10 @@ use lemmy_api_common::{ external_auth::{EditExternalAuth, ExternalAuthResponse}, utils::is_admin, }; -use lemmy_db_schema::source::external_auth::{ExternalAuth, ExternalAuthUpdateForm}; +use lemmy_db_schema::{ + source::external_auth::{ExternalAuth, ExternalAuthUpdateForm}, + traits::Crud, +}; use lemmy_db_views::structs::{ExternalAuthView, LocalUserView}; use lemmy_utils::error::LemmyError; diff --git a/crates/db_schema/src/impls/external_auth.rs b/crates/db_schema/src/impls/external_auth.rs index 44bd132c60..1e77ab5caf 100644 --- a/crates/db_schema/src/impls/external_auth.rs +++ b/crates/db_schema/src/impls/external_auth.rs @@ -2,20 +2,26 @@ use crate::{ newtypes::ExternalAuthId, schema::external_auth::dsl::external_auth, source::external_auth::{ExternalAuth, ExternalAuthInsertForm, ExternalAuthUpdateForm}, + traits::Crud, utils::{get_conn, DbPool}, }; use diesel::{dsl::insert_into, result::Error, QueryDsl}; use diesel_async::RunQueryDsl; -impl ExternalAuth { - pub async fn create(pool: &mut DbPool<'_>, form: &ExternalAuthInsertForm) -> Result { +#[async_trait] +impl Crud for ExternalAuth { + type InsertForm = ExternalAuthInsertForm; + type UpdateForm = ExternalAuthUpdateForm; + type IdType = ExternalAuthId; + + async fn create(pool: &mut DbPool<'_>, form: &ExternalAuthInsertForm) -> Result { let conn = &mut get_conn(pool).await?; insert_into(external_auth) .values(form) .get_result::(conn) .await } - pub async fn update( + async fn update( pool: &mut DbPool<'_>, external_auth_id: ExternalAuthId, form: &ExternalAuthUpdateForm, @@ -26,7 +32,7 @@ impl ExternalAuth { .get_result::(conn) .await } - pub async fn delete( + async fn delete( pool: &mut DbPool<'_>, external_auth_id: ExternalAuthId, ) -> Result { From c4367ffc413eb2ee7ff583708dac2c01d0068b7f Mon Sep 17 00:00:00 2001 From: Anthony Lawn Date: Wed, 13 Dec 2023 09:28:08 -0600 Subject: [PATCH 17/18] Code style fixes --- crates/api/src/site/leave_admin.rs | 3 +-- crates/api_crud/src/external_auth/delete.rs | 5 +---- crates/api_crud/src/site/read.rs | 3 +-- crates/api_crud/src/user/create.rs | 11 +++-------- crates/db_schema/src/impls/external_auth.rs | 5 +---- 5 files changed, 7 insertions(+), 20 deletions(-) diff --git a/crates/api/src/site/leave_admin.rs b/crates/api/src/site/leave_admin.rs index 607c8e3c72..8e8b06ba7c 100644 --- a/crates/api/src/site/leave_admin.rs +++ b/crates/api/src/site/leave_admin.rs @@ -60,8 +60,7 @@ pub async fn leave_admin( 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 custom_emojis = CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?; let external_auths = ExternalAuthView::get_all(&mut context.pool()).await?; diff --git a/crates/api_crud/src/external_auth/delete.rs b/crates/api_crud/src/external_auth/delete.rs index e832282797..4e7176c773 100644 --- a/crates/api_crud/src/external_auth/delete.rs +++ b/crates/api_crud/src/external_auth/delete.rs @@ -6,10 +6,7 @@ use lemmy_api_common::{ utils::is_admin, SuccessResponse, }; -use lemmy_db_schema::{ - source::external_auth::ExternalAuth, - traits::Crud, -}; +use lemmy_db_schema::{source::external_auth::ExternalAuth, traits::Crud}; use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyError; diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index 5cb2a9215b..9af3bbc0bd 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -80,8 +80,7 @@ pub async fn get_site( 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 external_auths = - ExternalAuthView::get_all(&mut context.pool()).await?; + let external_auths = ExternalAuthView::get_all(&mut context.pool()).await?; Ok(Json(GetSiteResponse { site_view, diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index 8a133ec1d8..b782ee8bb1 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -102,7 +102,7 @@ pub async fn register( data.username.clone(), &local_site, site_view.site.instance_id, - &context + &context, ) .await?; @@ -198,13 +198,8 @@ pub async fn register_from_oauth( check_slurs(&username, &slur_regex)?; // We have to create both a person, and local_user - let inserted_person = create_person( - username, - &local_site, - site_view.site.instance_id, - &context - ) - .await?; + let inserted_person = + create_person(username, &local_site, site_view.site.instance_id, &context).await?; // Create the local user let local_user_form = LocalUserInsertForm::builder() diff --git a/crates/db_schema/src/impls/external_auth.rs b/crates/db_schema/src/impls/external_auth.rs index 1e77ab5caf..140fd06993 100644 --- a/crates/db_schema/src/impls/external_auth.rs +++ b/crates/db_schema/src/impls/external_auth.rs @@ -32,10 +32,7 @@ impl Crud for ExternalAuth { .get_result::(conn) .await } - async fn delete( - pool: &mut DbPool<'_>, - external_auth_id: ExternalAuthId, - ) -> Result { + async fn delete(pool: &mut DbPool<'_>, external_auth_id: ExternalAuthId) -> Result { let conn = &mut get_conn(pool).await?; diesel::delete(external_auth.find(external_auth_id)) .execute(conn) From a533896ef0407560652450aaab26b7e04eaa484b Mon Sep 17 00:00:00 2001 From: Anthony Lawn Date: Sat, 20 Jan 2024 17:12:05 -0600 Subject: [PATCH 18/18] WIP remove external auth view --- crates/api/src/local_user/oauth_callback.rs | 13 ++-- crates/api/src/site/leave_admin.rs | 6 +- crates/api_common/src/external_auth.rs | 8 -- crates/api_common/src/site.rs | 4 +- crates/api_crud/src/external_auth/create.rs | 11 +-- crates/api_crud/src/external_auth/update.rs | 12 ++- crates/api_crud/src/site/read.rs | 5 +- crates/db_schema/src/impls/external_auth.rs | 53 ++++++++++++- crates/db_views/src/external_auth_view.rs | 83 --------------------- crates/db_views/src/structs.rs | 8 -- crates/utils/translations | 2 +- 11 files changed, 78 insertions(+), 127 deletions(-) delete mode 100644 crates/db_views/src/external_auth_view.rs diff --git a/crates/api/src/local_user/oauth_callback.rs b/crates/api/src/local_user/oauth_callback.rs index 4a9a14a610..1094536859 100644 --- a/crates/api/src/local_user/oauth_callback.rs +++ b/crates/api/src/local_user/oauth_callback.rs @@ -13,8 +13,12 @@ use lemmy_api_common::{ utils::AUTH_COOKIE_NAME, }; use lemmy_api_crud::user::create::register_from_oauth; -use lemmy_db_schema::{newtypes::ExternalAuthId, source::local_user::LocalUser, RegistrationMode}; -use lemmy_db_views::structs::{ExternalAuthView, LocalUserView, SiteView}; +use lemmy_db_schema::{ + newtypes::ExternalAuthId, + source::{external_auth::ExternalAuth, local_user::LocalUser}, + RegistrationMode, +}; +use lemmy_db_views::structs::{LocalUserView, SiteView}; use url::Url; // Login user using response from external identity provider @@ -42,15 +46,14 @@ pub async fn oauth_callback( // Fetch the auth method let external_auth_id = ExternalAuthId(oauth_state.external_auth); - let external_auth_view = ExternalAuthView::get(&mut context.pool(), external_auth_id).await; + let external_auth_view = ExternalAuth::get(&mut context.pool(), external_auth_id).await; if !external_auth_view.is_ok() { return HttpResponse::Found() .append_header(("Location", "/login?err=external_auth")) .finish(); } let external_auth = external_auth_view.unwrap().external_auth; - let client_secret = - ExternalAuthView::get_client_secret(&mut context.pool(), external_auth_id).await; + let client_secret = ExternalAuth::get_client_secret(&mut context.pool(), external_auth_id).await; if !client_secret.is_ok() { return HttpResponse::Found() .append_header(("Location", "/login?err=external_auth")) diff --git a/crates/api/src/site/leave_admin.rs b/crates/api/src/site/leave_admin.rs index 8e8b06ba7c..a053f4fc89 100644 --- a/crates/api/src/site/leave_admin.rs +++ b/crates/api/src/site/leave_admin.rs @@ -3,6 +3,7 @@ use lemmy_api_common::{context::LemmyContext, site::GetSiteResponse, utils::is_a use lemmy_db_schema::{ source::{ actor_language::SiteLanguage, + external_auth::ExternalAuth, language::Language, local_user::{LocalUser, LocalUserUpdateForm}, moderator::{ModAdd, ModAddForm}, @@ -10,7 +11,7 @@ use lemmy_db_schema::{ }, traits::Crud, }; -use lemmy_db_views::structs::{CustomEmojiView, ExternalAuthView, LocalUserView, SiteView}; +use lemmy_db_views::structs::{CustomEmojiView, LocalUserView, SiteView}; use lemmy_db_views_actor::structs::PersonView; use lemmy_utils::{ error::{LemmyError, LemmyErrorType}, @@ -61,8 +62,7 @@ pub async fn leave_admin( 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 external_auths = - ExternalAuthView::get_all(&mut context.pool()).await?; + let external_auths = ExternalAuth::get_all(&mut context.pool()).await?; Ok(Json(GetSiteResponse { site_view, diff --git a/crates/api_common/src/external_auth.rs b/crates/api_common/src/external_auth.rs index 1dfe76cc80..4033b5b02a 100644 --- a/crates/api_common/src/external_auth.rs +++ b/crates/api_common/src/external_auth.rs @@ -48,14 +48,6 @@ pub struct DeleteExternalAuth { pub id: ExternalAuthId, } -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// A response for an external auth method. -pub struct ExternalAuthResponse { - pub external_auth: ExternalAuthView, -} - #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, Default)] #[cfg_attr(feature = "full", derive(TS))] diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index cdd8781c5a..eba14bcf99 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -3,6 +3,7 @@ use chrono::{DateTime, Utc}; use lemmy_db_schema::{ newtypes::{CommentId, CommunityId, InstanceId, LanguageId, PersonId, PostId}, source::{ + external_auth::ExternalAuth, federation_queue_state::FederationQueueState, instance::Instance, language::Language, @@ -17,7 +18,6 @@ use lemmy_db_schema::{ use lemmy_db_views::structs::{ CommentView, CustomEmojiView, - ExternalAuthView, LocalUserView, PostView, RegistrationApplicationView, @@ -297,7 +297,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 external_auths: Vec, + pub external_auths: Vec, } #[skip_serializing_none] diff --git a/crates/api_crud/src/external_auth/create.rs b/crates/api_crud/src/external_auth/create.rs index 2c2e8a82ba..9ef037e0bc 100644 --- a/crates/api_crud/src/external_auth/create.rs +++ b/crates/api_crud/src/external_auth/create.rs @@ -2,14 +2,14 @@ use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ context::LemmyContext, - external_auth::{CreateExternalAuth, ExternalAuthResponse}, + external_auth::CreateExternalAuth, utils::is_admin, }; use lemmy_db_schema::{ source::external_auth::{ExternalAuth, ExternalAuthInsertForm}, traits::Crud, }; -use lemmy_db_views::structs::{ExternalAuthView, LocalUserView}; +use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyError; #[tracing::instrument(skip(context))] @@ -17,7 +17,7 @@ pub async fn create_external_auth( data: Json, context: Data, local_user_view: LocalUserView, -) -> Result, LemmyError> { +) -> Result, LemmyError> { // Make sure user is an admin is_admin(&local_user_view)?; @@ -35,8 +35,5 @@ pub async fn create_external_auth( .scopes(data.scopes.to_string()) .build(); let external_auth = ExternalAuth::create(&mut context.pool(), &external_auth_form).await?; - let view = ExternalAuthView::get(&mut context.pool(), external_auth.id).await?; - Ok(Json(ExternalAuthResponse { - external_auth: view, - })) + Ok(Json(external_auth)) } diff --git a/crates/api_crud/src/external_auth/update.rs b/crates/api_crud/src/external_auth/update.rs index 62eab2794f..6be88359bf 100644 --- a/crates/api_crud/src/external_auth/update.rs +++ b/crates/api_crud/src/external_auth/update.rs @@ -2,14 +2,14 @@ use activitypub_federation::config::Data; use actix_web::web::Json; use lemmy_api_common::{ context::LemmyContext, - external_auth::{EditExternalAuth, ExternalAuthResponse}, + external_auth::EditExternalAuth, utils::is_admin, }; use lemmy_db_schema::{ source::external_auth::{ExternalAuth, ExternalAuthUpdateForm}, traits::Crud, }; -use lemmy_db_views::structs::{ExternalAuthView, LocalUserView}; +use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::LemmyError; #[tracing::instrument(skip(context))] @@ -17,7 +17,7 @@ pub async fn update_external_auth( data: Json, context: Data, local_user_view: LocalUserView, -) -> Result, LemmyError> { +) -> Result, LemmyError> { // Make sure user is an admin is_admin(&local_user_view)?; @@ -40,8 +40,6 @@ pub async fn update_external_auth( let external_auth = ExternalAuth::update(&mut context.pool(), data.id, &external_auth_form.build()).await?; - let view = ExternalAuthView::get(&mut context.pool(), external_auth.id).await?; - Ok(Json(ExternalAuthResponse { - external_auth: view, - })) + let view = ExternalAuth::get(&mut context.pool(), external_auth.id).await?; + Ok(Json(external_auth)) } diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs index 9af3bbc0bd..6384b52b09 100644 --- a/crates/api_crud/src/site/read.rs +++ b/crates/api_crud/src/site/read.rs @@ -5,10 +5,11 @@ use lemmy_api_common::{ }; use lemmy_db_schema::source::{ actor_language::{LocalUserLanguage, SiteLanguage}, + external_auth::ExternalAuth, language::Language, tagline::Tagline, }; -use lemmy_db_views::structs::{CustomEmojiView, ExternalAuthView, LocalUserView, SiteView}; +use lemmy_db_views::structs::{CustomEmojiView, LocalUserView, SiteView}; use lemmy_db_views_actor::structs::{ CommunityBlockView, CommunityFollowerView, @@ -80,7 +81,7 @@ pub async fn get_site( 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 external_auths = ExternalAuthView::get_all(&mut context.pool()).await?; + let external_auths = ExternalAuth::get_all(&mut context.pool()).await?; Ok(Json(GetSiteResponse { site_view, diff --git a/crates/db_schema/src/impls/external_auth.rs b/crates/db_schema/src/impls/external_auth.rs index 140fd06993..afca522f3f 100644 --- a/crates/db_schema/src/impls/external_auth.rs +++ b/crates/db_schema/src/impls/external_auth.rs @@ -5,7 +5,12 @@ use crate::{ traits::Crud, utils::{get_conn, DbPool}, }; -use diesel::{dsl::insert_into, result::Error, QueryDsl}; +use diesel::{ + associations::HasTable, + dsl::insert_into, + result::Error, + QueryDsl, +}; use diesel_async::RunQueryDsl; #[async_trait] @@ -39,3 +44,49 @@ impl Crud for ExternalAuth { .await } } + +impl ExternalAuth { + pub async fn get(pool: &mut DbPool<'_>, external_auth_id: ExternalAuthId) -> Result { + let conn = &mut get_conn(pool).await?; + let external_auths = external_auth::table + .find(external_auth_id) + .select(external_auth::all_columns) + .load::(conn) + .await?; + if let Some(external_auth) = external_auths.into_iter().next() { + Ok(external_auth) + } else { + Err(diesel::result::Error::NotFound) + } + } + + // client_secret is in its own function because it should never be sent to any frontends, + // and will only be needed when performing an oauth request by the server + pub async fn get_client_secret( + pool: &mut DbPool<'_>, + external_auth_id: ExternalAuthId, + ) -> Result { + let conn = &mut get_conn(pool).await?; + let external_auths = external_auth::table + .find(external_auth_id) + .select(external_auth::client_secret) + .load::(conn) + .await?; + if let Some(external_auth) = external_auths.into_iter().next() { + Ok(external_auth) + } else { + Err(diesel::result::Error::NotFound) + } + } + + pub async fn get_all(pool: &mut DbPool<'_>) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + let external_auths = external_auth::table + .order(external_auth::id) + .select(external_auth::all_columns) + .load::(conn) + .await?; + + Ok(external_auths) + } +} diff --git a/crates/db_views/src/external_auth_view.rs b/crates/db_views/src/external_auth_view.rs deleted file mode 100644 index b1039ab679..0000000000 --- a/crates/db_views/src/external_auth_view.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::structs::ExternalAuthView; -use diesel::{result::Error, QueryDsl}; -use diesel_async::RunQueryDsl; -use lemmy_db_schema::{ - newtypes::ExternalAuthId, - schema::external_auth, - source::external_auth::ExternalAuth, - utils::{get_conn, DbPool}, -}; - -impl ExternalAuthView { - pub async fn get(pool: &mut DbPool<'_>, external_auth_id: ExternalAuthId) -> Result { - let conn = &mut get_conn(pool).await?; - let external_auths = external_auth::table - .find(external_auth_id) - .select(external_auth::all_columns) - .load::(conn) - .await?; - if let Some(external_auth) = ExternalAuthView::from_tuple_to_vec(external_auths) - .into_iter() - .next() - { - Ok(external_auth) - } else { - Err(diesel::result::Error::NotFound) - } - } - - // client_secret is in its own function because it should never be sent to any frontends, - // and will only be needed when performing an oauth request by the server - pub async fn get_client_secret( - pool: &mut DbPool<'_>, - external_auth_id: ExternalAuthId, - ) -> Result { - let conn = &mut get_conn(pool).await?; - let external_auths = external_auth::table - .find(external_auth_id) - .select(external_auth::client_secret) - .load::(conn) - .await?; - if let Some(external_auth) = external_auths.into_iter().next() { - Ok(external_auth) - } else { - Err(diesel::result::Error::NotFound) - } - } - - pub async fn get_all(pool: &mut DbPool<'_>) -> Result, Error> { - let conn = &mut get_conn(pool).await?; - let external_auths = external_auth::table - .order(external_auth::id) - .select(external_auth::all_columns) - .load::(conn) - .await?; - - Ok(ExternalAuthView::from_tuple_to_vec(external_auths)) - } - - fn from_tuple_to_vec(items: Vec) -> Vec { - let mut result = Vec::new(); - for item in &items { - result.push(ExternalAuthView { - // Can't just clone entire object because client_secret must be stripped - external_auth: ExternalAuth { - id: item.id.clone(), - display_name: item.display_name.clone(), - auth_type: item.auth_type.clone(), - auth_endpoint: item.auth_endpoint.clone(), - token_endpoint: item.token_endpoint.clone(), - user_endpoint: item.user_endpoint.clone(), - id_attribute: item.id_attribute.clone(), - issuer: item.issuer.clone(), - client_id: item.client_id.clone(), - client_secret: String::new(), - scopes: item.scopes.clone(), - published: item.published.clone(), - updated: item.updated.clone(), - }, - }); - } - result - } -} diff --git a/crates/db_views/src/structs.rs b/crates/db_views/src/structs.rs index 9b005dd7ef..2acd4d5b37 100644 --- a/crates/db_views/src/structs.rs +++ b/crates/db_views/src/structs.rs @@ -185,11 +185,3 @@ pub struct CustomEmojiView { pub custom_emoji: CustomEmoji, pub keywords: Vec, } - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[cfg_attr(feature = "full", derive(TS))] -#[cfg_attr(feature = "full", ts(export))] -/// An external auth view. -pub struct ExternalAuthView { - pub external_auth: ExternalAuth, -} diff --git a/crates/utils/translations b/crates/utils/translations index b3343aef72..e943f97fe4 160000 --- a/crates/utils/translations +++ b/crates/utils/translations @@ -1 +1 @@ -Subproject commit b3343aef72e5a7e5df34cf328b910ed798027270 +Subproject commit e943f97fe481dc425acdebc8872bf1fdcabaf875