From 2708b3e0b129f85b316547475b566005535b36f5 Mon Sep 17 00:00:00 2001 From: boxbeam Date: Thu, 22 Feb 2024 11:31:52 -0500 Subject: [PATCH] feat(webserver): Implement license service (#1491) * feat(db): Add enterprise license to server_setting * Rename field * feat(webserver): Implement license service * Add unit test * Rename variant back to TEAM * [autofix.ci] apply automated fixes * Fix tests * Test for expired license in service * Change graphql endpoint * Apply suggestions * [autofix.ci] apply automated fixes * Fix license validation so JWT still decodes an expired license * Rebase and fix errors * Update schema.graphql * Rename endpoint to license * [autofix.ci] apply automated fixes * Apply suggestions * [autofix.ci] apply automated fixes * Make RawLicenseInfo private * Rename RawLicenseInfo --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- ee/tabby-db/schema.sqlite | Bin 106496 -> 106496 bytes ee/tabby-webserver/graphql/schema.graphql | 19 ++++ ee/tabby-webserver/src/handler.rs | 4 +- ee/tabby-webserver/src/schema/license.rs | 32 ++++++ ee/tabby-webserver/src/schema/mod.rs | 16 +++ ee/tabby-webserver/src/service/license.rs | 122 +++++++++++++++++++--- ee/tabby-webserver/src/service/mod.rs | 9 +- ee/tabby-webserver/src/service/setting.rs | 11 +- 8 files changed, 190 insertions(+), 23 deletions(-) create mode 100644 ee/tabby-webserver/src/schema/license.rs diff --git a/ee/tabby-db/schema.sqlite b/ee/tabby-db/schema.sqlite index 69f48a76390ca4cf2ab30829c36d9e81a457dc07..28cb5151c58aa8aefbb29e6d0d889984f924d583 100644 GIT binary patch delta 417 zcmZoTz}9epZ3CNtf(HZtOny7QyL|P0ioCmcV|jk_OyM!(zQUc#&B3*b%VV>lf)dwc zHz^HvODj_oDB8yOgx=o%R58d@qCf_)CuvO>Ony7QJAAc#^1M5Eqj`SvOyV)&zQmo(&BnEY%Wbovf&$lM z4=D|H3o8=~D?`J{V00The4xoPrm8H8RfS)kYpoyoDX z2y5GcVotXsChw9(=zRwibNtgaSy2vQhmV|rp5wOaSQd6hO=Hd+7M7yaw4&7F4Be9a z?9{wsBLgE7T>~RsLrVoin9o0!>yE6XfNEXmBDY$zuJcPh8M p0Sm)EA(P2=@``Z5G>, ) -> Result, StatusCode> { - let setting = match locator.setting().read_security_setting().await { + let security_setting = match locator.setting().read_security_setting().await { Ok(x) => x, Err(err) => { warn!("Failed to read security setting {}", err); @@ -85,6 +85,6 @@ async fn server_setting( }; Ok(Json(ServerSetting { - disable_client_side_telemetry: setting.disable_client_side_telemetry, + disable_client_side_telemetry: security_setting.disable_client_side_telemetry, })) } diff --git a/ee/tabby-webserver/src/schema/license.rs b/ee/tabby-webserver/src/schema/license.rs new file mode 100644 index 000000000000..f2ec52230c63 --- /dev/null +++ b/ee/tabby-webserver/src/schema/license.rs @@ -0,0 +1,32 @@ +use anyhow::Result; +use async_trait::async_trait; +use chrono::{DateTime, Utc}; +use juniper::{GraphQLEnum, GraphQLObject}; +use serde::Deserialize; + +#[derive(Debug, Deserialize, GraphQLEnum)] +#[serde(rename_all = "UPPERCASE")] +pub enum LicenseType { + Team, +} + +#[derive(GraphQLEnum, PartialEq, Debug)] +pub enum LicenseStatus { + Ok, + Expired, +} + +#[derive(GraphQLObject)] +pub struct LicenseInfo { + pub r#type: LicenseType, + pub status: LicenseStatus, + pub seats: i32, + pub issued_at: DateTime, + pub expires_at: DateTime, +} + +#[async_trait] +pub trait LicenseService: Send + Sync { + async fn read_license(&self) -> Result>; + async fn update_license(&self, license: Option) -> Result>; +} diff --git a/ee/tabby-webserver/src/schema/mod.rs b/ee/tabby-webserver/src/schema/mod.rs index b4642fc67a7e..5e09b52ca0f4 100644 --- a/ee/tabby-webserver/src/schema/mod.rs +++ b/ee/tabby-webserver/src/schema/mod.rs @@ -1,6 +1,7 @@ pub mod auth; pub mod email; pub mod job; +pub mod license; pub mod repository; pub mod setting; pub mod worker; @@ -28,6 +29,7 @@ use worker::{Worker, WorkerService}; use self::{ auth::{PasswordResetInput, RequestPasswordResetEmailInput, UpdateOAuthCredentialInput}, email::{EmailService, EmailSetting, EmailSettingInput}, + license::{LicenseInfo, LicenseService, LicenseStatus}, repository::RepositoryService, setting::{ NetworkSetting, NetworkSettingInput, SecuritySetting, SecuritySettingInput, SettingService, @@ -47,6 +49,7 @@ pub trait ServiceLocator: Send + Sync { fn repository(&self) -> Arc; fn email(&self) -> Arc; fn setting(&self) -> Arc; + fn license(&self) -> Arc; } pub struct Context { @@ -298,6 +301,10 @@ impl Query { allow_self_signup: ctx.locator.auth().allow_self_signup().await?, }) } + + async fn license(ctx: &Context) -> Result> { + Ok(ctx.locator.license().read_license().await?) + } } #[derive(GraphQLObject)] @@ -495,6 +502,15 @@ impl Mutation { ctx.locator.email().delete_email_setting().await?; Ok(true) } + + async fn upload_license( + ctx: &Context, + license: Option, + ) -> Result> { + check_admin(ctx)?; + let status = ctx.locator.license().update_license(license).await?; + Ok(status) + } } fn from_validation_errors(error: ValidationErrors) -> FieldError { diff --git a/ee/tabby-webserver/src/service/license.rs b/ee/tabby-webserver/src/service/license.rs index 16036c6a34d8..b04177864afa 100644 --- a/ee/tabby-webserver/src/service/license.rs +++ b/ee/tabby-webserver/src/service/license.rs @@ -1,6 +1,12 @@ +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use chrono::{DateTime, NaiveDateTime, Utc}; use jsonwebtoken as jwt; use lazy_static::lazy_static; use serde::Deserialize; +use tabby_db::DbConn; + +use crate::schema::license::{LicenseInfo, LicenseService, LicenseStatus, LicenseType}; lazy_static! { static ref LICENSE_DECODING_KEY: jwt::DecodingKey = @@ -8,13 +14,8 @@ lazy_static! { } #[derive(Debug, Deserialize)] -#[serde(rename_all = "UPPERCASE")] -pub enum LicenseType { - Team, -} - -#[derive(Debug, Deserialize)] -pub struct LicenseInfo { +#[allow(unused)] +struct LicenseJWTPayload { /// Expiration time (as UTC timestamp) pub exp: i64, @@ -34,11 +35,12 @@ pub struct LicenseInfo { pub num: usize, } -pub fn validate_license(token: &str) -> Result { +fn validate_license(token: &str) -> Result { let mut validation = jwt::Validation::new(jwt::Algorithm::RS512); + validation.validate_exp = false; validation.set_issuer(&["tabbyml.com"]); validation.set_required_spec_claims(&["exp", "iat", "sub", "iss"]); - let data = jwt::decode::(token, &LICENSE_DECODING_KEY, &validation); + let data = jwt::decode::(token, &LICENSE_DECODING_KEY, &validation); let data = data.map_err(|err| match err.kind() { // Map json error (missing failed, parse error) as missing required claims. jwt::errors::ErrorKind::Json(err) => { @@ -49,16 +51,78 @@ pub fn validate_license(token: &str) -> Result Result> { + Ok(NaiveDateTime::from_timestamp_opt(secs, 0) + .ok_or_else(|| anyhow!("Timestamp is corrupt"))? + .and_utc()) +} + +struct LicenseServiceImpl { + db: DbConn, +} + +pub fn new_license_service(db: DbConn) -> impl LicenseService { + LicenseServiceImpl { db } +} + +fn license_info_from_raw(raw: LicenseJWTPayload) -> Result { + let issued_at = jwt_timestamp_to_utc(raw.iat)?; + let expires_at = jwt_timestamp_to_utc(raw.exp)?; + + let status = if expires_at < Utc::now() { + LicenseStatus::Expired + } else { + LicenseStatus::Ok + }; + + let license = LicenseInfo { + r#type: raw.typ, + status, + seats: raw.num as i32, + issued_at, + expires_at, + }; + Ok(license) +} + +#[async_trait] +impl LicenseService for LicenseServiceImpl { + async fn read_license(&self) -> Result> { + let Some(license) = self.db.read_enterprise_license().await? else { + return Ok(None); + }; + let license = + validate_license(&license).map_err(|e| anyhow!("License is corrupt: {e:?}"))?; + let license = license_info_from_raw(license)?; + + Ok(Some(license)) + } + + async fn update_license(&self, license: Option) -> Result> { + let mut status = None; + if let Some(license) = &license { + let raw = + validate_license(license).map_err(|e| anyhow!("License is corrupt: {e:?}"))?; + status = Some(license_info_from_raw(raw)?.status); + } + self.db.update_enterprise_license(license).await?; + Ok(status) + } +} + #[cfg(test)] mod tests { use assert_matches::assert_matches; use super::*; + const VALID_TOKEN: &str = "eyJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJ0YWJieW1sLmNvbSIsInN1YiI6ImZha2VAdGFiYnltbC5jb20iLCJpYXQiOjE3MDUxOTgxMDIsImV4cCI6MTgwNzM5ODcwMiwidHlwIjoiVEVBTSIsIm51bSI6MTB9.vVo7PDevytGw2KXU5E-KMdJBijwOWsD1zKIf26rcjfxa3wDesGY40zuYZWyZFMfmAtBTO7DBgqdWnriHnF_HOnoAEDCycrgoxuSJW5TS9XsCWto-3rDhUsjRZ1wls-ztQu3Gxo_84UHUFwrXe-RHmJi_3w_YO-2L-nVw7JDd5zR8CEdLxeccD47vBrumYA7ybultoDHpHxSppjHlW1VPXavoaBIO1Twnbf52uJlbzJmloViDxoq-_9lxcN1hDN3KKE3crzO9uHK4jjZy_1KNHhCIIcnINek6SBl6lWZw9R88UfdP6uaVOTOHDFbGwv544TSLA_oKZXXntXhldKCp94YN8J4djHim91WwYBQARrpQKiQGP1APEQQdv_YO4iUC3QTLOVw_NMjyma0feVjzHYAap_2Q9HgnxyJfMH-KiH2zaR6BcdOfWV86crO5M0qNoP-XOgy4uU8eE2-PevOKM6uVwYiwoNZL4e9ttH6ratJj0tyqGW_3HYpsVyThzqDPisEz95knsrVL-iagwHRd00l6Mqfwcjbn-gOuUOV9knRIpPvUmfKjjjHgb-JI0qMAIdgeVtwQp0pNqPsKwenMwkpYQH1awfuB_Ia7SyMUNEzTAY8k_J4R6kCZ5XKJ2VTCljd9aJFSZpw-K57reUX1eLc6-Cwt1iI4d23M5UlYjvs"; + const EXPIRED_TOKEN: &str = "eyJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJ0YWJieW1sLmNvbSIsInN1YiI6ImZha2VAdGFiYnltbC5jb20iLCJpYXQiOjE3MDUxOTgxMDIsImV4cCI6MTcwNzM5ODcwMiwidHlwIjoiVEVBTSIsIm51bSI6MTB9.19wrmSSZUQAj_nfnBljUARD3vz_XEIDh4wpi_U2P6LDRcvm7QYCro__LxUjIf45aE9BBiZCPBRTVOw_tMbegTAv5yK9G9cllGPdRDKWjf24BJpHt2wBKOwhCToUKp8R8D50bQ3cxHuz7J3XxcOMtwKxNRlwaufO-vgxX73v13z_bN6y5ix8FC5JEjY1z3fNPc_TnuuHnaXXqgqL9OJTrxhh5FErqR52kmxGGn2KCM8rm2Nfu0It2IZQuyJHSceZ3-iiIxsrVdXxbO4KHXLEOXos0xJRV8QG9_9VjAo6qui6BioygwrcPqHT7OoG3WfcT8XE9rcEX-s9PZ54_XxLm0yh81g54xPI92n94pe32XfE9T-YXNK3MLAdZWwDhp_sKXTcMSIr7mI9OA7eczZUpvI4BuDM8s1irNx4DKdfTwNchHDfEPmGmO53RHyVEbrS72jF9GBRBIwPmpGppWhcwpVNmlRJw3j1Sa_ttcGikPnBZBrUxGqzynq4q1VpeCpRoTzO9_nw5eciKMpaKww0P5Edqm5kKgg48aABfsTU3hLqTIr9rgjXePL_gEse6MJX_JC8I7-R17iQmMxKiNa9bTqSIk56qlB6gwZTzcjEtpnYlzZ05Ci6D3JBH9ZdO_F3UZDt5JdAD5dqsKl8PfWpxaWpg7FXNlqxYO9BpxCwr_7g"; + const INCOMPLETE_TOKEN: &str = "eyJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJ0YWJieW1sLmNvbSIsInN1YiI6ImZha2VAdGFiYnltbC5jb20iLCJpYXQiOjE3MDUxOTgxMDIsImV4cCI6MTgwNzM5ODcwMiwidHlwIjoiVEVBTSJ9.Xdp7Tgi39RN3qBfDAT_RncCDF2lSSouT4fjR0YT8F4qN8qkocxgvCa6JyxlksaiqGKWb_aYJvkhCviMHnT_pnoNpR8YaLvB4vezEAdDWLf3jBqzhlsrCCbMGh72wFYKRIODhIHeTzldU4F06I9sz5HdtQpn42Q8WC8tAzG109vHtxcdC7D85u0CumJ35DcV7lTfpfIkil3PORReg0ysjZNjQ2JbiFqMF1VbBmC-DsoTrJoHlrxdHowMQsXv89C80pchx4UFSm7Z9tHiMUTOzfErScsGJI1VC5p8SYA3N4nsrPn-iup1CxOBIdK57BHedKGpd_hi1AVWYB4zXcc8HzzpqgwHulfaw_5vNvRMdkDGj3X2afU3O3rZ4jT_KLGjY-3Krgol8JHgJYiPXkBypiajFU6rVeMLScx-X-2-n3KBdR4GQ9la90QHSyIQUpiGRRfPhviBFDtAfcjJYo1Irlu6MGVhgFq9JH5SOVTn57V0A_VeAbj8WZNdML9hio9xqxP86DprnP_ApHpO_xbi-sx2GCmUyfC10eKnX8_sAB1n7z0AaHz4e-6SGm1I-wQsWcXjZfRYw0Vtogz7wVuyAIpm8lF58XjtOwQ9bP1kD03TGIcBTvEtgA6QUhRcximGJ5buK9X2TTd4TlHjFF1krrmYAUEDgFsorseoKvMkspVE"; + #[test] fn test_validate_license() { - let token = "eyJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJ0YWJieW1sLmNvbSIsInN1YiI6ImZha2VAdGFiYnltbC5jb20iLCJpYXQiOjE3MDUxOTgxMDIsImV4cCI6MTgwNzM5ODcwMiwidHlwIjoiVEVBTSIsIm51bSI6MTB9.vVo7PDevytGw2KXU5E-KMdJBijwOWsD1zKIf26rcjfxa3wDesGY40zuYZWyZFMfmAtBTO7DBgqdWnriHnF_HOnoAEDCycrgoxuSJW5TS9XsCWto-3rDhUsjRZ1wls-ztQu3Gxo_84UHUFwrXe-RHmJi_3w_YO-2L-nVw7JDd5zR8CEdLxeccD47vBrumYA7ybultoDHpHxSppjHlW1VPXavoaBIO1Twnbf52uJlbzJmloViDxoq-_9lxcN1hDN3KKE3crzO9uHK4jjZy_1KNHhCIIcnINek6SBl6lWZw9R88UfdP6uaVOTOHDFbGwv544TSLA_oKZXXntXhldKCp94YN8J4djHim91WwYBQARrpQKiQGP1APEQQdv_YO4iUC3QTLOVw_NMjyma0feVjzHYAap_2Q9HgnxyJfMH-KiH2zaR6BcdOfWV86crO5M0qNoP-XOgy4uU8eE2-PevOKM6uVwYiwoNZL4e9ttH6ratJj0tyqGW_3HYpsVyThzqDPisEz95knsrVL-iagwHRd00l6Mqfwcjbn-gOuUOV9knRIpPvUmfKjjjHgb-JI0qMAIdgeVtwQp0pNqPsKwenMwkpYQH1awfuB_Ia7SyMUNEzTAY8k_J4R6kCZ5XKJ2VTCljd9aJFSZpw-K57reUX1eLc6-Cwt1iI4d23M5UlYjvs"; - let license = validate_license(token).unwrap(); + let license = validate_license(VALID_TOKEN).unwrap(); assert_eq!(license.iss, "tabbyml.com"); assert_eq!(license.sub, "fake@tabbyml.com"); assert_matches!(license.typ, LicenseType::Team); @@ -66,18 +130,44 @@ mod tests { #[test] fn test_expired_license() { - let token = "eyJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJ0YWJieW1sLmNvbSIsInN1YiI6ImZha2VAdGFiYnltbC5jb20iLCJpYXQiOjE3MDUxOTgxMDIsImV4cCI6MTcwNzM5ODcwMiwidHlwIjoiVEVBTSIsIm51bSI6MTB9.19wrmSSZUQAj_nfnBljUARD3vz_XEIDh4wpi_U2P6LDRcvm7QYCro__LxUjIf45aE9BBiZCPBRTVOw_tMbegTAv5yK9G9cllGPdRDKWjf24BJpHt2wBKOwhCToUKp8R8D50bQ3cxHuz7J3XxcOMtwKxNRlwaufO-vgxX73v13z_bN6y5ix8FC5JEjY1z3fNPc_TnuuHnaXXqgqL9OJTrxhh5FErqR52kmxGGn2KCM8rm2Nfu0It2IZQuyJHSceZ3-iiIxsrVdXxbO4KHXLEOXos0xJRV8QG9_9VjAo6qui6BioygwrcPqHT7OoG3WfcT8XE9rcEX-s9PZ54_XxLm0yh81g54xPI92n94pe32XfE9T-YXNK3MLAdZWwDhp_sKXTcMSIr7mI9OA7eczZUpvI4BuDM8s1irNx4DKdfTwNchHDfEPmGmO53RHyVEbrS72jF9GBRBIwPmpGppWhcwpVNmlRJw3j1Sa_ttcGikPnBZBrUxGqzynq4q1VpeCpRoTzO9_nw5eciKMpaKww0P5Edqm5kKgg48aABfsTU3hLqTIr9rgjXePL_gEse6MJX_JC8I7-R17iQmMxKiNa9bTqSIk56qlB6gwZTzcjEtpnYlzZ05Ci6D3JBH9ZdO_F3UZDt5JdAD5dqsKl8PfWpxaWpg7FXNlqxYO9BpxCwr_7g"; - let license = validate_license(token); - assert_matches!(license, Err(jwt::errors::ErrorKind::ExpiredSignature)); + let license = validate_license(EXPIRED_TOKEN).unwrap(); + let license = license_info_from_raw(license).unwrap(); + assert_matches!(license.status, LicenseStatus::Expired); } #[test] fn test_missing_field() { - let token = "eyJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJ0YWJieW1sLmNvbSIsInN1YiI6ImZha2VAdGFiYnltbC5jb20iLCJpYXQiOjE3MDUxOTgxMDIsImV4cCI6MTgwNzM5ODcwMiwidHlwIjoiVEVBTSJ9.Xdp7Tgi39RN3qBfDAT_RncCDF2lSSouT4fjR0YT8F4qN8qkocxgvCa6JyxlksaiqGKWb_aYJvkhCviMHnT_pnoNpR8YaLvB4vezEAdDWLf3jBqzhlsrCCbMGh72wFYKRIODhIHeTzldU4F06I9sz5HdtQpn42Q8WC8tAzG109vHtxcdC7D85u0CumJ35DcV7lTfpfIkil3PORReg0ysjZNjQ2JbiFqMF1VbBmC-DsoTrJoHlrxdHowMQsXv89C80pchx4UFSm7Z9tHiMUTOzfErScsGJI1VC5p8SYA3N4nsrPn-iup1CxOBIdK57BHedKGpd_hi1AVWYB4zXcc8HzzpqgwHulfaw_5vNvRMdkDGj3X2afU3O3rZ4jT_KLGjY-3Krgol8JHgJYiPXkBypiajFU6rVeMLScx-X-2-n3KBdR4GQ9la90QHSyIQUpiGRRfPhviBFDtAfcjJYo1Irlu6MGVhgFq9JH5SOVTn57V0A_VeAbj8WZNdML9hio9xqxP86DprnP_ApHpO_xbi-sx2GCmUyfC10eKnX8_sAB1n7z0AaHz4e-6SGm1I-wQsWcXjZfRYw0Vtogz7wVuyAIpm8lF58XjtOwQ9bP1kD03TGIcBTvEtgA6QUhRcximGJ5buK9X2TTd4TlHjFF1krrmYAUEDgFsorseoKvMkspVE"; - let license = validate_license(token); + let license = validate_license(INCOMPLETE_TOKEN); assert_matches!( license, Err(jwt::errors::ErrorKind::MissingRequiredClaim(_)) ); } + + #[tokio::test] + async fn test_create_delete_license() { + let db = DbConn::new_in_memory().await.unwrap(); + let service = new_license_service(db); + + assert!(service + .update_license(Some("bad_token".into())) + .await + .is_err()); + + service + .update_license(Some(VALID_TOKEN.into())) + .await + .unwrap(); + assert!(service.read_license().await.unwrap().is_some()); + + service.update_license(None).await.unwrap(); + assert!(service.read_license().await.unwrap().is_none()); + + service + .update_license(Some(EXPIRED_TOKEN.into())) + .await + .unwrap(); + let info = service.read_license().await.unwrap().unwrap(); + assert_eq!(info.status, LicenseStatus::Expired); + } } diff --git a/ee/tabby-webserver/src/service/mod.rs b/ee/tabby-webserver/src/service/mod.rs index d2354501e721..63e380256a2f 100644 --- a/ee/tabby-webserver/src/service/mod.rs +++ b/ee/tabby-webserver/src/service/mod.rs @@ -27,11 +27,14 @@ use tabby_common::{ use tabby_db::DbConn; use tracing::{info, warn}; -use self::{auth::new_authentication_service, email::new_email_service}; +use self::{ + auth::new_authentication_service, email::new_email_service, license::new_license_service, +}; use crate::schema::{ auth::AuthenticationService, email::EmailService, job::JobService, + license::LicenseService, repository::RepositoryService, setting::SettingService, worker::{RegisterWorkerError, Worker, WorkerKind, WorkerService}, @@ -244,6 +247,10 @@ impl ServiceLocator for Arc { fn setting(&self) -> Arc { Arc::new(self.db_conn.clone()) } + + fn license(&self) -> Arc { + Arc::new(new_license_service(self.db_conn.clone())) + } } pub async fn create_service_locator( diff --git a/ee/tabby-webserver/src/service/setting.rs b/ee/tabby-webserver/src/service/setting.rs index bdb2282afb9a..9851064a9860 100644 --- a/ee/tabby-webserver/src/service/setting.rs +++ b/ee/tabby-webserver/src/service/setting.rs @@ -9,7 +9,7 @@ use crate::schema::setting::{ #[async_trait] impl SettingService for DbConn { async fn read_security_setting(&self) -> Result { - Ok(self.read_server_setting().await?.into()) + Ok((self as &DbConn).read_server_setting().await?.into()) } async fn update_security_setting(&self, input: SecuritySettingInput) -> Result<()> { @@ -19,16 +19,19 @@ impl SettingService for DbConn { Some(input.allowed_register_domain_list.join(",")) }; - self.update_security_setting(domains, input.disable_client_side_telemetry) + (self as &DbConn) + .update_security_setting(domains, input.disable_client_side_telemetry) .await } async fn read_network_setting(&self) -> Result { - Ok(self.read_server_setting().await?.into()) + Ok((self as &DbConn).read_server_setting().await?.into()) } async fn update_network_setting(&self, input: NetworkSettingInput) -> Result<()> { - self.update_network_setting(input.external_url).await + (self as &DbConn) + .update_network_setting(input.external_url) + .await } }