Alternative ATProto PDS implementation

prototype account_manager skeleton

+494
src/account_manager/helpers/account.rs
··· 1 + //! Based on https://github.com/blacksky-algorithms/rsky/blob/main/rsky-pds/src/account_manager/helpers/account.rs 2 + //! blacksky-algorithms/rsky is licensed under the Apache License 2.0 3 + //! 4 + //! Modified for SQLite backend 5 + use crate::db::DbConn; 6 + use crate::schema::pds::account::dsl as AccountSchema; 7 + use crate::schema::pds::account::table as AccountTable; 8 + use crate::schema::pds::actor::dsl as ActorSchema; 9 + use crate::schema::pds::actor::table as ActorTable; 10 + use anyhow::Result; 11 + use chrono::DateTime; 12 + use chrono::offset::Utc as UtcOffset; 13 + use diesel::dsl::{LeftJoinOn, exists, not}; 14 + use diesel::helper_types::{Eq, IntoBoxed}; 15 + use diesel::pg::Pg; 16 + use diesel::result::{DatabaseErrorKind, Error as DieselError}; 17 + use diesel::*; 18 + use rsky_common; 19 + use rsky_common::RFC3339_VARIANT; 20 + use rsky_lexicon::com::atproto::admin::StatusAttr; 21 + use std::ops::Add; 22 + use std::time::SystemTime; 23 + use thiserror::Error; 24 + 25 + #[derive(Error, Debug)] 26 + pub enum AccountHelperError { 27 + #[error("UserAlreadyExistsError")] 28 + UserAlreadyExistsError, 29 + #[error("DatabaseError: `{0}`")] 30 + DieselError(String), 31 + } 32 + 33 + pub struct AvailabilityFlags { 34 + pub include_taken_down: Option<bool>, 35 + pub include_deactivated: Option<bool>, 36 + } 37 + 38 + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 39 + pub enum AccountStatus { 40 + Active, 41 + Takendown, 42 + Suspended, 43 + Deleted, 44 + Deactivated, 45 + Desynchronized, 46 + Throttled, 47 + } 48 + 49 + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 50 + pub struct FormattedAccountStatus { 51 + pub active: bool, 52 + pub status: Option<AccountStatus>, 53 + } 54 + 55 + #[derive(Debug)] 56 + pub struct GetAccountAdminStatusOutput { 57 + pub takedown: StatusAttr, 58 + pub deactivated: StatusAttr, 59 + } 60 + 61 + pub type ActorJoinAccount = 62 + LeftJoinOn<ActorTable, AccountTable, Eq<ActorSchema::did, AccountSchema::did>>; 63 + pub type BoxedQuery<'a> = IntoBoxed<'a, ActorJoinAccount, Pg>; 64 + 65 + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 66 + pub struct ActorAccount { 67 + pub did: String, 68 + pub handle: Option<String>, 69 + #[serde(rename = "createdAt")] 70 + pub created_at: String, 71 + #[serde(rename = "takedownRef")] 72 + pub takedown_ref: Option<String>, 73 + #[serde(rename = "deactivatedAt")] 74 + pub deactivated_at: Option<String>, 75 + #[serde(rename = "deleteAfter")] 76 + pub delete_after: Option<String>, 77 + pub email: Option<String>, 78 + #[serde(rename = "invitesDisabled")] 79 + pub invites_disabled: Option<i16>, 80 + #[serde(rename = "emailConfirmedAt")] 81 + pub email_confirmed_at: Option<String>, 82 + } 83 + 84 + pub fn select_account_qb(flags: Option<AvailabilityFlags>) -> BoxedQuery<'static> { 85 + let AvailabilityFlags { 86 + include_taken_down, 87 + include_deactivated, 88 + } = flags.unwrap_or_else(|| AvailabilityFlags { 89 + include_taken_down: Some(false), 90 + include_deactivated: Some(false), 91 + }); 92 + let include_taken_down = include_taken_down.unwrap_or(false); 93 + let include_deactivated = include_deactivated.unwrap_or(false); 94 + 95 + let mut builder = ActorSchema::actor 96 + .left_join(AccountSchema::account.on(ActorSchema::did.eq(AccountSchema::did))) 97 + .into_boxed(); 98 + if !include_taken_down { 99 + builder = builder.filter(ActorSchema::takedownRef.is_null()); 100 + } 101 + if !include_deactivated { 102 + builder = builder.filter(ActorSchema::deactivatedAt.is_null()); 103 + } 104 + builder 105 + } 106 + 107 + pub async fn get_account( 108 + _handle_or_did: &str, 109 + flags: Option<AvailabilityFlags>, 110 + db: &DbConn, 111 + ) -> Result<Option<ActorAccount>> { 112 + let handle_or_did = _handle_or_did.to_owned(); 113 + let found = db 114 + .run(move |conn| { 115 + let mut builder = select_account_qb(flags); 116 + if handle_or_did.starts_with("did:") { 117 + builder = builder.filter(ActorSchema::did.eq(handle_or_did)); 118 + } else { 119 + builder = builder.filter(ActorSchema::handle.eq(handle_or_did)); 120 + } 121 + 122 + builder 123 + .select(( 124 + ActorSchema::did, 125 + ActorSchema::handle, 126 + ActorSchema::createdAt, 127 + ActorSchema::takedownRef, 128 + ActorSchema::deactivatedAt, 129 + ActorSchema::deleteAfter, 130 + AccountSchema::email.nullable(), 131 + AccountSchema::emailConfirmedAt.nullable(), 132 + AccountSchema::invitesDisabled.nullable(), 133 + )) 134 + .first::<( 135 + String, 136 + Option<String>, 137 + String, 138 + Option<String>, 139 + Option<String>, 140 + Option<String>, 141 + Option<String>, 142 + Option<String>, 143 + Option<i16>, 144 + )>(conn) 145 + .map(|res| ActorAccount { 146 + did: res.0, 147 + handle: res.1, 148 + created_at: res.2, 149 + takedown_ref: res.3, 150 + deactivated_at: res.4, 151 + delete_after: res.5, 152 + email: res.6, 153 + email_confirmed_at: res.7, 154 + invites_disabled: res.8, 155 + }) 156 + .optional() 157 + }) 158 + .await?; 159 + Ok(found) 160 + } 161 + 162 + pub async fn get_account_by_email( 163 + _email: &str, 164 + flags: Option<AvailabilityFlags>, 165 + db: &DbConn, 166 + ) -> Result<Option<ActorAccount>> { 167 + let email = _email.to_owned(); 168 + let found = db 169 + .run(move |conn| { 170 + select_account_qb(flags) 171 + .select(( 172 + ActorSchema::did, 173 + ActorSchema::handle, 174 + ActorSchema::createdAt, 175 + ActorSchema::takedownRef, 176 + ActorSchema::deactivatedAt, 177 + ActorSchema::deleteAfter, 178 + AccountSchema::email.nullable(), 179 + AccountSchema::emailConfirmedAt.nullable(), 180 + AccountSchema::invitesDisabled.nullable(), 181 + )) 182 + .filter(AccountSchema::email.eq(email.to_lowercase())) 183 + .first::<( 184 + String, 185 + Option<String>, 186 + String, 187 + Option<String>, 188 + Option<String>, 189 + Option<String>, 190 + Option<String>, 191 + Option<String>, 192 + Option<i16>, 193 + )>(conn) 194 + .map(|res| ActorAccount { 195 + did: res.0, 196 + handle: res.1, 197 + created_at: res.2, 198 + takedown_ref: res.3, 199 + deactivated_at: res.4, 200 + delete_after: res.5, 201 + email: res.6, 202 + email_confirmed_at: res.7, 203 + invites_disabled: res.8, 204 + }) 205 + .optional() 206 + }) 207 + .await?; 208 + Ok(found) 209 + } 210 + 211 + pub async fn register_actor( 212 + did: String, 213 + handle: String, 214 + deactivated: Option<bool>, 215 + db: &DbConn, 216 + ) -> Result<()> { 217 + let system_time = SystemTime::now(); 218 + let dt: DateTime<UtcOffset> = system_time.into(); 219 + let created_at = format!("{}", dt.format(RFC3339_VARIANT)); 220 + let deactivate_at = match deactivated { 221 + Some(true) => Some(created_at.clone()), 222 + _ => None, 223 + }; 224 + let deactivate_after = match deactivated { 225 + Some(true) => { 226 + let exp = dt.add(chrono::Duration::days(3)); 227 + Some(format!("{}", exp.format(RFC3339_VARIANT))) 228 + } 229 + _ => None, 230 + }; 231 + 232 + let _: String = db 233 + .run(move |conn| { 234 + insert_into(ActorSchema::actor) 235 + .values(( 236 + ActorSchema::did.eq(did), 237 + ActorSchema::handle.eq(handle), 238 + ActorSchema::createdAt.eq(created_at), 239 + ActorSchema::deactivatedAt.eq(deactivate_at), 240 + ActorSchema::deleteAfter.eq(deactivate_after), 241 + )) 242 + .on_conflict_do_nothing() 243 + .returning(ActorSchema::did) 244 + .get_result(conn) 245 + }) 246 + .await?; 247 + Ok(()) 248 + } 249 + 250 + pub async fn register_account( 251 + did: String, 252 + email: String, 253 + password: String, 254 + db: &DbConn, 255 + ) -> Result<()> { 256 + let created_at = rsky_common::now(); 257 + 258 + // @TODO record recovery key for bring your own recovery key 259 + let _: String = db 260 + .run(move |conn| { 261 + insert_into(AccountSchema::account) 262 + .values(( 263 + AccountSchema::did.eq(did), 264 + AccountSchema::email.eq(email), 265 + AccountSchema::password.eq(password), 266 + AccountSchema::createdAt.eq(created_at), 267 + )) 268 + .on_conflict_do_nothing() 269 + .returning(AccountSchema::did) 270 + .get_result(conn) 271 + }) 272 + .await?; 273 + Ok(()) 274 + } 275 + 276 + pub async fn delete_account(did: &str, db: &DbConn) -> Result<()> { 277 + use crate::schema::pds::email_token::dsl as EmailTokenSchema; 278 + use crate::schema::pds::refresh_token::dsl as RefreshTokenSchema; 279 + use crate::schema::pds::repo_root::dsl as RepoRootSchema; 280 + 281 + let did = did.to_owned(); 282 + db.run(move |conn| { 283 + delete(RepoRootSchema::repo_root) 284 + .filter(RepoRootSchema::did.eq(&did)) 285 + .execute(conn)?; 286 + delete(EmailTokenSchema::email_token) 287 + .filter(EmailTokenSchema::did.eq(&did)) 288 + .execute(conn)?; 289 + delete(RefreshTokenSchema::refresh_token) 290 + .filter(RefreshTokenSchema::did.eq(&did)) 291 + .execute(conn)?; 292 + delete(AccountSchema::account) 293 + .filter(AccountSchema::did.eq(&did)) 294 + .execute(conn)?; 295 + delete(ActorSchema::actor) 296 + .filter(ActorSchema::did.eq(&did)) 297 + .execute(conn) 298 + }) 299 + .await?; 300 + Ok(()) 301 + } 302 + 303 + pub async fn update_account_takedown_status( 304 + did: &str, 305 + takedown: StatusAttr, 306 + db: &DbConn, 307 + ) -> Result<()> { 308 + let takedown_ref: Option<String> = match takedown.applied { 309 + true => match takedown.r#ref { 310 + Some(takedown_ref) => Some(takedown_ref), 311 + None => Some(rsky_common::now()), 312 + }, 313 + false => None, 314 + }; 315 + let did = did.to_owned(); 316 + db.run(move |conn| { 317 + update(ActorSchema::actor) 318 + .filter(ActorSchema::did.eq(did)) 319 + .set((ActorSchema::takedownRef.eq(takedown_ref),)) 320 + .execute(conn) 321 + }) 322 + .await?; 323 + Ok(()) 324 + } 325 + 326 + pub async fn deactivate_account( 327 + did: &str, 328 + delete_after: Option<String>, 329 + db: &DbConn, 330 + ) -> Result<()> { 331 + let did = did.to_owned(); 332 + db.run(move |conn| { 333 + update(ActorSchema::actor) 334 + .filter(ActorSchema::did.eq(did)) 335 + .set(( 336 + ActorSchema::deactivatedAt.eq(rsky_common::now()), 337 + ActorSchema::deleteAfter.eq(delete_after), 338 + )) 339 + .execute(conn) 340 + }) 341 + .await?; 342 + Ok(()) 343 + } 344 + 345 + pub async fn activate_account(did: &str, db: &DbConn) -> Result<()> { 346 + let did = did.to_owned(); 347 + db.run(move |conn| { 348 + update(ActorSchema::actor) 349 + .filter(ActorSchema::did.eq(did)) 350 + .set(( 351 + ActorSchema::deactivatedAt.eq::<Option<String>>(None), 352 + ActorSchema::deleteAfter.eq::<Option<String>>(None), 353 + )) 354 + .execute(conn) 355 + }) 356 + .await?; 357 + Ok(()) 358 + } 359 + 360 + pub async fn update_email(did: &str, email: &str, db: &DbConn) -> Result<()> { 361 + let did = did.to_owned(); 362 + let email = email.to_owned(); 363 + let res = db 364 + .run(move |conn| { 365 + update(AccountSchema::account) 366 + .filter(AccountSchema::did.eq(did)) 367 + .set(( 368 + AccountSchema::email.eq(email.to_lowercase()), 369 + AccountSchema::emailConfirmedAt.eq::<Option<String>>(None), 370 + )) 371 + .execute(conn) 372 + }) 373 + .await; 374 + 375 + match res { 376 + Ok(_) => Ok(()), 377 + Err(DieselError::DatabaseError(kind, _)) => match kind { 378 + DatabaseErrorKind::UniqueViolation => Err(anyhow::Error::new( 379 + AccountHelperError::UserAlreadyExistsError, 380 + )), 381 + _ => Err(anyhow::Error::new(AccountHelperError::DieselError( 382 + format!("{:?}", kind), 383 + ))), 384 + }, 385 + Err(e) => Err(anyhow::Error::new(e)), 386 + } 387 + } 388 + 389 + pub async fn update_handle(did: &str, handle: &str, db: &DbConn) -> Result<()> { 390 + use crate::schema::pds::actor; 391 + 392 + let actor2 = diesel::alias!(actor as actor2); 393 + 394 + let did = did.to_owned(); 395 + let handle = handle.to_owned(); 396 + let res = db 397 + .run(move |conn| { 398 + update(ActorSchema::actor) 399 + .filter(ActorSchema::did.eq(did)) 400 + .filter(not(exists(actor2.filter(ActorSchema::handle.eq(&handle))))) 401 + .set((ActorSchema::handle.eq(&handle),)) 402 + .execute(conn) 403 + }) 404 + .await?; 405 + 406 + if res < 1 { 407 + return Err(anyhow::Error::new( 408 + AccountHelperError::UserAlreadyExistsError, 409 + )); 410 + } 411 + Ok(()) 412 + } 413 + 414 + pub async fn set_email_confirmed_at( 415 + did: &str, 416 + email_confirmed_at: String, 417 + db: &DbConn, 418 + ) -> Result<()> { 419 + let did = did.to_owned(); 420 + db.run(move |conn| { 421 + update(AccountSchema::account) 422 + .filter(AccountSchema::did.eq(did)) 423 + .set(AccountSchema::emailConfirmedAt.eq(email_confirmed_at)) 424 + .execute(conn) 425 + }) 426 + .await?; 427 + Ok(()) 428 + } 429 + 430 + pub async fn get_account_admin_status( 431 + did: &str, 432 + db: &DbConn, 433 + ) -> Result<Option<GetAccountAdminStatusOutput>> { 434 + let did = did.to_owned(); 435 + let res: Option<(Option<String>, Option<String>)> = db 436 + .run(move |conn| { 437 + ActorSchema::actor 438 + .filter(ActorSchema::did.eq(did)) 439 + .select((ActorSchema::takedownRef, ActorSchema::deactivatedAt)) 440 + .first(conn) 441 + .optional() 442 + }) 443 + .await?; 444 + match res { 445 + None => Ok(None), 446 + Some(res) => { 447 + let takedown = match res.0 { 448 + Some(takedown_ref) => StatusAttr { 449 + applied: true, 450 + r#ref: Some(takedown_ref), 451 + }, 452 + None => StatusAttr { 453 + applied: false, 454 + r#ref: None, 455 + }, 456 + }; 457 + let deactivated = match res.1 { 458 + Some(_) => StatusAttr { 459 + applied: true, 460 + r#ref: None, 461 + }, 462 + None => StatusAttr { 463 + applied: false, 464 + r#ref: None, 465 + }, 466 + }; 467 + Ok(Some(GetAccountAdminStatusOutput { 468 + takedown, 469 + deactivated, 470 + })) 471 + } 472 + } 473 + } 474 + 475 + pub fn format_account_status(account: Option<ActorAccount>) -> FormattedAccountStatus { 476 + match account { 477 + None => FormattedAccountStatus { 478 + active: false, 479 + status: Some(AccountStatus::Deleted), 480 + }, 481 + Some(got) if got.takedown_ref.is_some() => FormattedAccountStatus { 482 + active: false, 483 + status: Some(AccountStatus::Takendown), 484 + }, 485 + Some(got) if got.deactivated_at.is_some() => FormattedAccountStatus { 486 + active: false, 487 + status: Some(AccountStatus::Deactivated), 488 + }, 489 + _ => FormattedAccountStatus { 490 + active: true, 491 + status: None, 492 + }, 493 + } 494 + }
+349
src/account_manager/helpers/auth.rs
··· 1 + //! Based on https://github.com/blacksky-algorithms/rsky/blob/main/rsky-pds/src/account_manager/helpers/auth.rs 2 + //! blacksky-algorithms/rsky is licensed under the Apache License 2.0 3 + //! 4 + //! Modified for SQLite backend 5 + use crate::auth_verifier::AuthScope; 6 + use crate::db::DbConn; 7 + use crate::models; 8 + use anyhow::Result; 9 + use diesel::*; 10 + use jwt_simple::prelude::*; 11 + use rsky_common::time::{MINUTE, from_micros_to_utc}; 12 + use rsky_common::{RFC3339_VARIANT, get_random_str, json_to_b64url}; 13 + use secp256k1::{Keypair, Message, SecretKey}; 14 + use sha2::{Digest, Sha256}; 15 + use std::time::SystemTime; 16 + use thiserror::Error; 17 + 18 + pub struct CreateTokensOpts { 19 + pub did: String, 20 + pub jwt_key: Keypair, 21 + pub service_did: String, 22 + pub scope: Option<AuthScope>, 23 + pub jti: Option<String>, 24 + pub expires_in: Option<Duration>, 25 + } 26 + 27 + pub struct RefreshGracePeriodOpts { 28 + pub id: String, 29 + pub expires_at: String, 30 + pub next_id: String, 31 + } 32 + 33 + pub struct AuthToken { 34 + pub scope: AuthScope, 35 + pub sub: String, 36 + pub exp: Duration, 37 + } 38 + 39 + pub struct RefreshToken { 40 + pub scope: AuthScope, // AuthScope::Refresh 41 + pub sub: String, 42 + pub exp: Duration, 43 + pub jti: String, 44 + } 45 + 46 + #[derive(Debug, Deserialize, Serialize, Clone)] 47 + pub struct ServiceJwtPayload { 48 + pub iss: String, 49 + pub aud: String, 50 + pub exp: Option<u64>, 51 + pub lxm: Option<String>, 52 + pub jti: Option<String>, 53 + } 54 + 55 + #[derive(Debug, Deserialize, Serialize, Clone)] 56 + pub struct ServiceJwtHeader { 57 + pub typ: String, 58 + pub alg: String, 59 + } 60 + 61 + pub struct ServiceJwtParams { 62 + pub iss: String, 63 + pub aud: String, 64 + pub exp: Option<u64>, 65 + pub lxm: Option<String>, 66 + pub jti: Option<String>, 67 + pub keypair: SecretKey, 68 + } 69 + 70 + #[derive(Serialize, Deserialize)] 71 + pub struct CustomClaimObj { 72 + pub scope: String, 73 + } 74 + 75 + #[derive(Error, Debug)] 76 + pub enum AuthHelperError { 77 + #[error("ConcurrentRefreshError")] 78 + ConcurrentRefresh, 79 + } 80 + 81 + pub fn create_tokens(opts: CreateTokensOpts) -> Result<(String, String)> { 82 + let CreateTokensOpts { 83 + did, 84 + jwt_key, 85 + service_did, 86 + scope, 87 + jti, 88 + expires_in, 89 + } = opts; 90 + let access_jwt = create_access_token(CreateTokensOpts { 91 + did: did.clone(), 92 + jwt_key, 93 + service_did: service_did.clone(), 94 + scope, 95 + expires_in, 96 + jti: None, 97 + })?; 98 + let refresh_jwt = create_refresh_token(CreateTokensOpts { 99 + did, 100 + jwt_key, 101 + service_did, 102 + jti, 103 + expires_in, 104 + scope: None, 105 + })?; 106 + Ok((access_jwt, refresh_jwt)) 107 + } 108 + 109 + pub fn create_access_token(opts: CreateTokensOpts) -> Result<String> { 110 + let CreateTokensOpts { 111 + did, 112 + jwt_key, 113 + service_did, 114 + scope, 115 + expires_in, 116 + .. 117 + } = opts; 118 + let scope = scope.unwrap_or(AuthScope::Access); 119 + let expires_in = expires_in.unwrap_or_else(|| Duration::from_hours(2)); 120 + let claims = Claims::with_custom_claims( 121 + CustomClaimObj { 122 + scope: scope.as_str().to_owned(), 123 + }, 124 + expires_in, 125 + ) 126 + .with_audience(service_did) 127 + .with_subject(did); 128 + // alg ES256K 129 + let key = ES256kKeyPair::from_bytes(jwt_key.secret_bytes().as_slice())?; 130 + let token = key.sign(claims)?; 131 + Ok(token) 132 + } 133 + 134 + pub fn create_refresh_token(opts: CreateTokensOpts) -> Result<String> { 135 + let CreateTokensOpts { 136 + did, 137 + jwt_key, 138 + service_did, 139 + jti, 140 + expires_in, 141 + .. 142 + } = opts; 143 + let jti = jti.unwrap_or_else(get_random_str); 144 + let expires_in = expires_in.unwrap_or_else(|| Duration::from_days(90)); 145 + let claims = Claims::with_custom_claims( 146 + CustomClaimObj { 147 + scope: AuthScope::Refresh.as_str().to_owned(), 148 + }, 149 + expires_in, 150 + ) 151 + .with_audience(service_did) 152 + .with_subject(did) 153 + .with_jwt_id(jti); 154 + // alg ES256K 155 + let key = ES256kKeyPair::from_bytes(jwt_key.secret_bytes().as_slice())?; 156 + let token = key.sign(claims)?; 157 + Ok(token) 158 + } 159 + 160 + pub async fn create_service_jwt(params: ServiceJwtParams) -> Result<String> { 161 + let ServiceJwtParams { 162 + iss, aud, keypair, .. 163 + } = params; 164 + let now = SystemTime::now() 165 + .duration_since(SystemTime::UNIX_EPOCH) 166 + .expect("timestamp in micros since UNIX epoch") 167 + .as_micros() as usize; 168 + let exp = params 169 + .exp 170 + .unwrap_or(((now + MINUTE as usize) / 1000) as u64); 171 + let lxm = params.lxm; 172 + let jti = get_random_str(); 173 + let header = ServiceJwtHeader { 174 + typ: "JWT".to_string(), 175 + alg: "ES256K".to_string(), 176 + }; 177 + let payload = ServiceJwtPayload { 178 + iss, 179 + aud, 180 + exp: Some(exp), 181 + lxm, 182 + jti: Some(jti), 183 + }; 184 + let to_sign_str = format!( 185 + "{0}.{1}", 186 + json_to_b64url(&header)?, 187 + json_to_b64url(&payload)? 188 + ); 189 + let hash = Sha256::digest(to_sign_str.clone()); 190 + let message = Message::from_digest_slice(hash.as_ref())?; 191 + let mut sig = keypair.sign_ecdsa(message); 192 + // Convert to low-s 193 + sig.normalize_s(); 194 + // ASN.1 encoded per decode_dss_signature 195 + let compact_sig = sig.serialize_compact(); 196 + Ok(format!( 197 + "{0}.{1}", 198 + to_sign_str, 199 + base64_url::encode(&compact_sig).replace("=", "") // Base 64 encode signature bytes 200 + )) 201 + } 202 + 203 + // @NOTE unsafe for verification, should only be used w/ direct output from createRefreshToken() or createTokens() 204 + pub fn decode_refresh_token(jwt: String, jwt_key: Keypair) -> Result<RefreshToken> { 205 + let key = ES256kKeyPair::from_bytes(jwt_key.secret_bytes().as_slice())?; 206 + let public_key = key.public_key(); 207 + let claims = public_key.verify_token::<CustomClaimObj>(&jwt, None)?; 208 + assert_eq!( 209 + claims.custom.scope, 210 + AuthScope::Refresh.as_str().to_owned(), 211 + "not a refresh token" 212 + ); 213 + Ok(RefreshToken { 214 + scope: AuthScope::from_str(&claims.custom.scope)?, 215 + sub: claims.subject.unwrap(), 216 + exp: claims.expires_at.unwrap(), 217 + jti: claims.jwt_id.unwrap(), 218 + }) 219 + } 220 + 221 + pub async fn store_refresh_token( 222 + payload: RefreshToken, 223 + app_password_name: Option<String>, 224 + db: &DbConn, 225 + ) -> Result<()> { 226 + use crate::schema::pds::refresh_token::dsl as RefreshTokenSchema; 227 + 228 + let exp = from_micros_to_utc((payload.exp.as_millis() / 1000) as i64); 229 + 230 + db.run(move |conn| { 231 + insert_into(RefreshTokenSchema::refresh_token) 232 + .values(( 233 + RefreshTokenSchema::id.eq(payload.jti), 234 + RefreshTokenSchema::did.eq(payload.sub), 235 + RefreshTokenSchema::appPasswordName.eq(app_password_name), 236 + RefreshTokenSchema::expiresAt.eq(format!("{}", exp.format(RFC3339_VARIANT))), 237 + )) 238 + .on_conflict_do_nothing() // E.g. when re-granting during a refresh grace period 239 + .execute(conn) 240 + }) 241 + .await?; 242 + 243 + Ok(()) 244 + } 245 + 246 + pub async fn revoke_refresh_token(id: String, db: &DbConn) -> Result<bool> { 247 + use crate::schema::pds::refresh_token::dsl as RefreshTokenSchema; 248 + db.run(move |conn| { 249 + let deleted_rows = delete(RefreshTokenSchema::refresh_token) 250 + .filter(RefreshTokenSchema::id.eq(id)) 251 + .get_results::<models::RefreshToken>(conn)?; 252 + 253 + Ok(!deleted_rows.is_empty()) 254 + }) 255 + .await 256 + } 257 + 258 + pub async fn revoke_refresh_tokens_by_did(did: &str, db: &DbConn) -> Result<bool> { 259 + use crate::schema::pds::refresh_token::dsl as RefreshTokenSchema; 260 + let did = did.to_owned(); 261 + db.run(move |conn| { 262 + let deleted_rows = delete(RefreshTokenSchema::refresh_token) 263 + .filter(RefreshTokenSchema::did.eq(did)) 264 + .get_results::<models::RefreshToken>(conn)?; 265 + 266 + Ok(!deleted_rows.is_empty()) 267 + }) 268 + .await 269 + } 270 + 271 + pub async fn revoke_app_password_refresh_token( 272 + did: &str, 273 + app_pass_name: &str, 274 + db: &DbConn, 275 + ) -> Result<bool> { 276 + use crate::schema::pds::refresh_token::dsl as RefreshTokenSchema; 277 + 278 + let did = did.to_owned(); 279 + let app_pass_name = app_pass_name.to_owned(); 280 + db.run(move |conn| { 281 + let deleted_rows = delete(RefreshTokenSchema::refresh_token) 282 + .filter(RefreshTokenSchema::did.eq(did)) 283 + .filter(RefreshTokenSchema::appPasswordName.eq(app_pass_name)) 284 + .get_results::<models::RefreshToken>(conn)?; 285 + 286 + Ok(!deleted_rows.is_empty()) 287 + }) 288 + .await 289 + } 290 + 291 + pub async fn get_refresh_token(id: &str, db: &DbConn) -> Result<Option<models::RefreshToken>> { 292 + use crate::schema::pds::refresh_token::dsl as RefreshTokenSchema; 293 + let id = id.to_owned(); 294 + db.run(move |conn| { 295 + Ok(RefreshTokenSchema::refresh_token 296 + .find(id) 297 + .first(conn) 298 + .optional()?) 299 + }) 300 + .await 301 + } 302 + 303 + pub async fn delete_expired_refresh_tokens(did: &str, now: String, db: &DbConn) -> Result<()> { 304 + use crate::schema::pds::refresh_token::dsl as RefreshTokenSchema; 305 + let did = did.to_owned(); 306 + 307 + db.run(move |conn| { 308 + delete(RefreshTokenSchema::refresh_token) 309 + .filter(RefreshTokenSchema::did.eq(did)) 310 + .filter(RefreshTokenSchema::expiresAt.le(now)) 311 + .execute(conn)?; 312 + Ok(()) 313 + }) 314 + .await 315 + } 316 + 317 + pub async fn add_refresh_grace_period(opts: RefreshGracePeriodOpts, db: &DbConn) -> Result<()> { 318 + db.run(move |conn| { 319 + let RefreshGracePeriodOpts { 320 + id, 321 + expires_at, 322 + next_id, 323 + } = opts; 324 + use crate::schema::pds::refresh_token::dsl as RefreshTokenSchema; 325 + 326 + update(RefreshTokenSchema::refresh_token) 327 + .filter(RefreshTokenSchema::id.eq(id)) 328 + .filter( 329 + RefreshTokenSchema::nextId 330 + .is_null() 331 + .or(RefreshTokenSchema::nextId.eq(&next_id)), 332 + ) 333 + .set(( 334 + RefreshTokenSchema::expiresAt.eq(expires_at), 335 + RefreshTokenSchema::nextId.eq(&next_id), 336 + )) 337 + .returning(models::RefreshToken::as_select()) 338 + .get_results(conn) 339 + .map_err(|error| { 340 + anyhow::Error::new(AuthHelperError::ConcurrentRefresh).context(error) 341 + })?; 342 + Ok(()) 343 + }) 344 + .await 345 + } 346 + 347 + pub fn get_refresh_token_id() -> String { 348 + get_random_str() 349 + }
+136
src/account_manager/helpers/email_token.rs
··· 1 + //! Based on https://github.com/blacksky-algorithms/rsky/blob/main/rsky-pds/src/account_manager/helpers/email_token.rs 2 + //! blacksky-algorithms/rsky is licensed under the Apache License 2.0 3 + //! 4 + //! Modified for SQLite backend 5 + use crate::apis::com::atproto::server::get_random_token; 6 + use crate::db::DbConn; 7 + use crate::models::EmailToken; 8 + use crate::models::models::EmailTokenPurpose; 9 + use anyhow::{Result, bail}; 10 + use diesel::*; 11 + use rsky_common; 12 + use rsky_common::time::{MINUTE, from_str_to_utc, less_than_ago_s}; 13 + 14 + pub async fn create_email_token( 15 + did: &str, 16 + purpose: EmailTokenPurpose, 17 + db: &DbConn, 18 + ) -> Result<String> { 19 + use crate::schema::pds::email_token::dsl as EmailTokenSchema; 20 + let token = get_random_token().to_uppercase(); 21 + let now = rsky_common::now(); 22 + 23 + let did = did.to_owned(); 24 + db.run(move |conn| { 25 + insert_into(EmailTokenSchema::email_token) 26 + .values(( 27 + EmailTokenSchema::purpose.eq(purpose), 28 + EmailTokenSchema::did.eq(did), 29 + EmailTokenSchema::token.eq(&token), 30 + EmailTokenSchema::requestedAt.eq(&now), 31 + )) 32 + .on_conflict((EmailTokenSchema::purpose, EmailTokenSchema::did)) 33 + .do_update() 34 + .set(( 35 + EmailTokenSchema::token.eq(&token), 36 + EmailTokenSchema::requestedAt.eq(&now), 37 + )) 38 + .execute(conn)?; 39 + Ok(token) 40 + }) 41 + .await 42 + } 43 + 44 + pub async fn assert_valid_token( 45 + did: &str, 46 + purpose: EmailTokenPurpose, 47 + token: &str, 48 + expiration_len: Option<i32>, 49 + db: &DbConn, 50 + ) -> Result<()> { 51 + let expiration_len = expiration_len.unwrap_or(MINUTE * 15); 52 + use crate::schema::pds::email_token::dsl as EmailTokenSchema; 53 + 54 + let did = did.to_owned(); 55 + let token = token.to_owned(); 56 + let res = db 57 + .run(move |conn| { 58 + EmailTokenSchema::email_token 59 + .filter(EmailTokenSchema::purpose.eq(purpose)) 60 + .filter(EmailTokenSchema::did.eq(did)) 61 + .filter(EmailTokenSchema::token.eq(token.to_uppercase())) 62 + .select(EmailToken::as_select()) 63 + .first(conn) 64 + .optional() 65 + }) 66 + .await?; 67 + if let Some(res) = res { 68 + let requested_at = from_str_to_utc(&res.requested_at); 69 + let expired = !less_than_ago_s(requested_at, expiration_len); 70 + if expired { 71 + bail!("Token is expired") 72 + } 73 + Ok(()) 74 + } else { 75 + bail!("Token is invalid") 76 + } 77 + } 78 + 79 + pub async fn assert_valid_token_and_find_did( 80 + purpose: EmailTokenPurpose, 81 + token: &str, 82 + expiration_len: Option<i32>, 83 + db: &DbConn, 84 + ) -> Result<String> { 85 + let expiration_len = expiration_len.unwrap_or(MINUTE * 15); 86 + use crate::schema::pds::email_token::dsl as EmailTokenSchema; 87 + 88 + let token = token.to_owned(); 89 + let res = db 90 + .run(move |conn| { 91 + EmailTokenSchema::email_token 92 + .filter(EmailTokenSchema::purpose.eq(purpose)) 93 + .filter(EmailTokenSchema::token.eq(token.to_uppercase())) 94 + .select(EmailToken::as_select()) 95 + .first(conn) 96 + .optional() 97 + }) 98 + .await?; 99 + if let Some(res) = res { 100 + let requested_at = from_str_to_utc(&res.requested_at); 101 + let expired = !less_than_ago_s(requested_at, expiration_len); 102 + if expired { 103 + bail!("Token is expired") 104 + } 105 + Ok(res.did) 106 + } else { 107 + bail!("Token is invalid") 108 + } 109 + } 110 + 111 + pub async fn delete_email_token(did: &str, purpose: EmailTokenPurpose, db: &DbConn) -> Result<()> { 112 + use crate::schema::pds::email_token::dsl as EmailTokenSchema; 113 + let did = did.to_owned(); 114 + db.run(move |conn| { 115 + delete(EmailTokenSchema::email_token) 116 + .filter(EmailTokenSchema::did.eq(did)) 117 + .filter(EmailTokenSchema::purpose.eq(purpose)) 118 + .execute(conn) 119 + }) 120 + .await?; 121 + Ok(()) 122 + } 123 + 124 + pub async fn delete_all_email_tokens(did: &str, db: &DbConn) -> Result<()> { 125 + use crate::schema::pds::email_token::dsl as EmailTokenSchema; 126 + 127 + let did = did.to_owned(); 128 + db.run(move |conn| { 129 + delete(EmailTokenSchema::email_token) 130 + .filter(EmailTokenSchema::did.eq(did)) 131 + .execute(conn) 132 + }) 133 + .await?; 134 + 135 + Ok(()) 136 + }
+322
src/account_manager/helpers/invite.rs
··· 1 + //! Based on https://github.com/blacksky-algorithms/rsky/blob/main/rsky-pds/src/account_manager/helpers/invite.rs 2 + //! blacksky-algorithms/rsky is licensed under the Apache License 2.0 3 + //! 4 + //! Modified for SQLite backend 5 + use crate::account_manager::DisableInviteCodesOpts; 6 + use crate::db::DbConn; 7 + use crate::models::models; 8 + use anyhow::{Result, bail}; 9 + use diesel::*; 10 + use rsky_common; 11 + use rsky_lexicon::com::atproto::server::AccountCodes; 12 + use rsky_lexicon::com::atproto::server::{ 13 + InviteCode as LexiconInviteCode, InviteCodeUse as LexiconInviteCodeUse, 14 + }; 15 + use std::collections::BTreeMap; 16 + use std::mem; 17 + 18 + pub type CodeUse = LexiconInviteCodeUse; 19 + pub type CodeDetail = LexiconInviteCode; 20 + 21 + pub async fn ensure_invite_is_available(invite_code: String, db: &DbConn) -> Result<()> { 22 + use crate::schema::pds::actor::dsl as ActorSchema; 23 + use crate::schema::pds::invite_code::dsl as InviteCodeSchema; 24 + use crate::schema::pds::invite_code_use::dsl as InviteCodeUseSchema; 25 + 26 + db.run(move |conn| { 27 + let invite: Option<models::InviteCode> = InviteCodeSchema::invite_code 28 + .left_join( 29 + ActorSchema::actor.on(InviteCodeSchema::forAccount 30 + .eq(ActorSchema::did) 31 + .and(ActorSchema::takedownRef.is_null())), 32 + ) 33 + .filter(InviteCodeSchema::code.eq(&invite_code)) 34 + .select(models::InviteCode::as_select()) 35 + .first(conn) 36 + .optional()?; 37 + 38 + if invite.is_none() || invite.clone().unwrap().disabled > 0 { 39 + bail!("InvalidInviteCode: None or disabled. Provided invite code not available `{invite_code:?}`") 40 + } 41 + 42 + let uses: i64 = InviteCodeUseSchema::invite_code_use 43 + .count() 44 + .filter(InviteCodeUseSchema::code.eq(&invite_code)) 45 + .first(conn)?; 46 + 47 + if invite.unwrap().available_uses as i64 <= uses { 48 + bail!("InvalidInviteCode: Not enough uses. Provided invite code not available `{invite_code:?}`") 49 + } 50 + Ok(()) 51 + }).await?; 52 + 53 + Ok(()) 54 + } 55 + 56 + pub async fn record_invite_use( 57 + did: String, 58 + invite_code: Option<String>, 59 + now: String, 60 + db: &DbConn, 61 + ) -> Result<()> { 62 + if let Some(invite_code) = invite_code { 63 + use crate::schema::pds::invite_code_use::dsl as InviteCodeUseSchema; 64 + 65 + db.run(move |conn| { 66 + insert_into(InviteCodeUseSchema::invite_code_use) 67 + .values(( 68 + InviteCodeUseSchema::code.eq(invite_code), 69 + InviteCodeUseSchema::usedBy.eq(did), 70 + InviteCodeUseSchema::usedAt.eq(now), 71 + )) 72 + .execute(conn) 73 + }) 74 + .await?; 75 + } 76 + Ok(()) 77 + } 78 + 79 + pub async fn create_invite_codes( 80 + to_create: Vec<AccountCodes>, 81 + use_count: i32, 82 + db: &DbConn, 83 + ) -> Result<()> { 84 + use crate::schema::pds::invite_code::dsl as InviteCodeSchema; 85 + let created_at = rsky_common::now(); 86 + 87 + db.run(move |conn| { 88 + let rows: Vec<models::InviteCode> = to_create 89 + .into_iter() 90 + .flat_map(|account| { 91 + let for_account = account.account; 92 + account 93 + .codes 94 + .iter() 95 + .map(|code| models::InviteCode { 96 + code: code.clone(), 97 + available_uses: use_count, 98 + disabled: 0, 99 + for_account: for_account.clone(), 100 + created_by: "admin".to_owned(), 101 + created_at: created_at.clone(), 102 + }) 103 + .collect::<Vec<models::InviteCode>>() 104 + }) 105 + .collect(); 106 + insert_into(InviteCodeSchema::invite_code) 107 + .values(&rows) 108 + .execute(conn) 109 + }) 110 + .await?; 111 + Ok(()) 112 + } 113 + 114 + pub async fn create_account_invite_codes( 115 + for_account: &str, 116 + codes: Vec<String>, 117 + expected_total: usize, 118 + disabled: bool, 119 + db: &DbConn, 120 + ) -> Result<Vec<CodeDetail>> { 121 + use crate::schema::pds::invite_code::dsl as InviteCodeSchema; 122 + 123 + let for_account = for_account.to_owned(); 124 + let rows = db 125 + .run(move |conn| { 126 + let now = rsky_common::now(); 127 + 128 + let rows: Vec<models::InviteCode> = codes 129 + .into_iter() 130 + .map(|code| models::InviteCode { 131 + code, 132 + available_uses: 1, 133 + disabled: if disabled { 1 } else { 0 }, 134 + for_account: for_account.clone(), 135 + created_by: for_account.clone(), 136 + created_at: now.clone(), 137 + }) 138 + .collect(); 139 + 140 + insert_into(InviteCodeSchema::invite_code) 141 + .values(&rows) 142 + .execute(conn)?; 143 + 144 + let final_routine_invite_codes: Vec<models::InviteCode> = InviteCodeSchema::invite_code 145 + .filter(InviteCodeSchema::forAccount.eq(for_account)) 146 + .filter(InviteCodeSchema::createdBy.ne("admin")) // don't count admin-gifted codes against the user 147 + .select(models::InviteCode::as_select()) 148 + .get_results(conn)?; 149 + 150 + if final_routine_invite_codes.len() > expected_total { 151 + bail!("DuplicateCreate: attempted to create additional codes in another request") 152 + } 153 + 154 + Ok(rows.into_iter().map(|row| CodeDetail { 155 + code: row.code, 156 + available: 1, 157 + disabled: row.disabled == 1, 158 + for_account: row.for_account, 159 + created_by: row.created_by, 160 + created_at: row.created_at, 161 + uses: Vec::new(), 162 + })) 163 + }) 164 + .await?; 165 + Ok(rows.collect()) 166 + } 167 + 168 + pub async fn get_account_invite_codes(did: &str, db: &DbConn) -> Result<Vec<CodeDetail>> { 169 + use crate::schema::pds::invite_code::dsl as InviteCodeSchema; 170 + 171 + let did = did.to_owned(); 172 + let res: Vec<models::InviteCode> = db 173 + .run(move |conn| { 174 + InviteCodeSchema::invite_code 175 + .filter(InviteCodeSchema::forAccount.eq(did)) 176 + .select(models::InviteCode::as_select()) 177 + .get_results(conn) 178 + }) 179 + .await?; 180 + 181 + let codes: Vec<String> = res.iter().map(|row| row.code.clone()).collect(); 182 + let mut uses = get_invite_codes_uses_v2(codes, db).await?; 183 + Ok(res 184 + .into_iter() 185 + .map(|row| CodeDetail { 186 + code: row.code.clone(), 187 + available: row.available_uses, 188 + disabled: row.disabled == 1, 189 + for_account: row.for_account, 190 + created_by: row.created_by, 191 + created_at: row.created_at, 192 + uses: mem::take(uses.get_mut(&row.code).unwrap_or(&mut Vec::new())), 193 + }) 194 + .collect::<Vec<CodeDetail>>()) 195 + } 196 + 197 + pub async fn get_invite_codes_uses_v2( 198 + codes: Vec<String>, 199 + db: &DbConn, 200 + ) -> Result<BTreeMap<String, Vec<CodeUse>>> { 201 + use crate::schema::pds::invite_code_use::dsl as InviteCodeUseSchema; 202 + 203 + let mut uses: BTreeMap<String, Vec<CodeUse>> = BTreeMap::new(); 204 + if !codes.is_empty() { 205 + let uses_res: Vec<models::InviteCodeUse> = db 206 + .run(|conn| { 207 + InviteCodeUseSchema::invite_code_use 208 + .filter(InviteCodeUseSchema::code.eq_any(codes)) 209 + .order_by(InviteCodeUseSchema::usedAt.desc()) 210 + .select(models::InviteCodeUse::as_select()) 211 + .get_results(conn) 212 + }) 213 + .await?; 214 + for invite_code_use in uses_res { 215 + let models::InviteCodeUse { 216 + code, 217 + used_by, 218 + used_at, 219 + } = invite_code_use; 220 + match uses.get_mut(&code) { 221 + None => { 222 + uses.insert(code, vec![CodeUse { used_by, used_at }]); 223 + } 224 + Some(matched_uses) => matched_uses.push(CodeUse { used_by, used_at }), 225 + }; 226 + } 227 + } 228 + Ok(uses) 229 + } 230 + 231 + pub async fn get_invited_by_for_accounts( 232 + dids: Vec<String>, 233 + db: &DbConn, 234 + ) -> Result<BTreeMap<String, CodeDetail>> { 235 + if dids.is_empty() { 236 + return Ok(BTreeMap::new()); 237 + } 238 + use crate::schema::pds::invite_code::dsl as InviteCodeSchema; 239 + use crate::schema::pds::invite_code_use::dsl as InviteCodeUseSchema; 240 + 241 + let dids = dids.clone(); 242 + let res: Vec<models::InviteCode> = db 243 + .run(|conn| { 244 + InviteCodeSchema::invite_code 245 + .filter( 246 + InviteCodeSchema::forAccount.eq_any( 247 + InviteCodeUseSchema::invite_code_use 248 + .filter(InviteCodeUseSchema::usedBy.eq_any(dids)) 249 + .select(InviteCodeUseSchema::code) 250 + .distinct(), 251 + ), 252 + ) 253 + .select(models::InviteCode::as_select()) 254 + .get_results(conn) 255 + }) 256 + .await?; 257 + let codes: Vec<String> = res.iter().map(|row| row.code.clone()).collect(); 258 + let mut uses = get_invite_codes_uses_v2(codes, db).await?; 259 + 260 + let code_details = res 261 + .into_iter() 262 + .map(|row| CodeDetail { 263 + code: row.code.clone(), 264 + available: row.available_uses, 265 + disabled: row.disabled == 1, 266 + for_account: row.for_account, 267 + created_by: row.created_by, 268 + created_at: row.created_at, 269 + uses: mem::take(uses.get_mut(&row.code).unwrap_or(&mut Vec::new())), 270 + }) 271 + .collect::<Vec<CodeDetail>>(); 272 + 273 + Ok(code_details.iter().fold( 274 + BTreeMap::new(), 275 + |mut acc: BTreeMap<String, CodeDetail>, cur| { 276 + for code_use in &cur.uses { 277 + acc.insert(code_use.used_by.clone(), cur.clone()); 278 + } 279 + acc 280 + }, 281 + )) 282 + } 283 + 284 + pub async fn set_account_invites_disabled(did: &str, disabled: bool, db: &DbConn) -> Result<()> { 285 + use crate::schema::pds::account::dsl as AccountSchema; 286 + 287 + let disabled: i16 = if disabled { 1 } else { 0 }; 288 + let did = did.to_owned(); 289 + db.run(move |conn| { 290 + update(AccountSchema::account) 291 + .filter(AccountSchema::did.eq(did)) 292 + .set((AccountSchema::invitesDisabled.eq(disabled),)) 293 + .execute(conn) 294 + }) 295 + .await?; 296 + Ok(()) 297 + } 298 + 299 + pub async fn disable_invite_codes(opts: DisableInviteCodesOpts, db: &DbConn) -> Result<()> { 300 + use crate::schema::pds::invite_code::dsl as InviteCodeSchema; 301 + 302 + let DisableInviteCodesOpts { codes, accounts } = opts; 303 + if !codes.is_empty() { 304 + db.run(move |conn| { 305 + update(InviteCodeSchema::invite_code) 306 + .filter(InviteCodeSchema::code.eq_any(&codes)) 307 + .set((InviteCodeSchema::disabled.eq(1),)) 308 + .execute(conn) 309 + }) 310 + .await?; 311 + } 312 + if !accounts.is_empty() { 313 + db.run(move |conn| { 314 + update(InviteCodeSchema::invite_code) 315 + .filter(InviteCodeSchema::forAccount.eq_any(&accounts)) 316 + .set((InviteCodeSchema::disabled.eq(1),)) 317 + .execute(conn) 318 + }) 319 + .await?; 320 + } 321 + Ok(()) 322 + }
+181
src/account_manager/helpers/password.rs
··· 1 + //! Based on https://github.com/blacksky-algorithms/rsky/blob/main/rsky-pds/src/account_manager/helpers/password.rs 2 + //! blacksky-algorithms/rsky is licensed under the Apache License 2.0 3 + //! 4 + //! Modified for SQLite backend 5 + use crate::db::DbConn; 6 + use crate::models; 7 + use crate::models::AppPassword; 8 + use anyhow::{Result, anyhow, bail}; 9 + use argon2::{ 10 + Argon2, 11 + password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng}, 12 + }; 13 + use base64ct::{Base64, Encoding}; 14 + use diesel::*; 15 + use rsky_common::{get_random_str, now}; 16 + use rsky_lexicon::com::atproto::server::CreateAppPasswordOutput; 17 + use sha2::{Digest, Sha256}; 18 + 19 + pub struct UpdateUserPasswordOpts { 20 + pub did: String, 21 + pub password_encrypted: String, 22 + } 23 + 24 + pub async fn verify_account_password(did: &str, password: &String, db: &DbConn) -> Result<bool> { 25 + use crate::schema::pds::account::dsl as AccountSchema; 26 + 27 + let did = did.to_owned(); 28 + let found = db 29 + .run(move |conn| { 30 + AccountSchema::account 31 + .filter(AccountSchema::did.eq(did)) 32 + .select(models::Account::as_select()) 33 + .first(conn) 34 + .optional() 35 + }) 36 + .await?; 37 + if let Some(found) = found { 38 + verify(password, &found.password) 39 + } else { 40 + Ok(false) 41 + } 42 + } 43 + 44 + pub async fn verify_app_password(did: &str, password: &str, db: &DbConn) -> Result<Option<String>> { 45 + use crate::schema::pds::app_password::dsl as AppPasswordSchema; 46 + 47 + let did = did.to_owned(); 48 + let password = password.to_owned(); 49 + let password_encrypted = hash_app_password(&did, &password).await?; 50 + let found = db 51 + .run(move |conn| { 52 + AppPasswordSchema::app_password 53 + .filter(AppPasswordSchema::did.eq(did)) 54 + .filter(AppPasswordSchema::password.eq(password_encrypted)) 55 + .select(AppPassword::as_select()) 56 + .first(conn) 57 + .optional() 58 + }) 59 + .await?; 60 + if let Some(found) = found { 61 + Ok(Some(found.name)) 62 + } else { 63 + Ok(None) 64 + } 65 + } 66 + 67 + // We use Argon because it's 3x faster than scrypt. 68 + pub fn gen_salt_and_hash(password: String) -> Result<String> { 69 + let salt = SaltString::generate(&mut OsRng); 70 + // Hash password to PHC string 71 + let argon2 = Argon2::default(); 72 + let password_hash = argon2 73 + .hash_password(password.as_ref(), &salt) 74 + .map_err(|error| anyhow!(error.to_string()))? 75 + .to_string(); 76 + Ok(password_hash) 77 + } 78 + 79 + pub fn hash_with_salt(password: &String, salt: &str) -> Result<String> { 80 + let salt = SaltString::from_b64(salt).map_err(|error| anyhow!(error.to_string()))?; 81 + let argon2 = Argon2::default(); 82 + let password_hash = argon2 83 + .hash_password(password.as_ref(), &salt) 84 + .map_err(|error| anyhow!(error.to_string()))? 85 + .to_string(); 86 + Ok(password_hash) 87 + } 88 + 89 + pub fn verify(password: &String, stored_hash: &str) -> Result<bool> { 90 + let parsed_hash = PasswordHash::new(stored_hash).map_err(|error| anyhow!(error.to_string()))?; 91 + Ok(Argon2::default() 92 + .verify_password(password.as_ref(), &parsed_hash) 93 + .is_ok()) 94 + } 95 + 96 + pub async fn hash_app_password(did: &String, password: &String) -> Result<String> { 97 + let hash = Sha256::digest(did); 98 + let salt = Base64::encode_string(&hash).replace("=", ""); 99 + hash_with_salt(password, &salt) 100 + } 101 + 102 + /// create an app password with format: 103 + /// 1234-abcd-5678-efgh 104 + pub async fn create_app_password( 105 + did: String, 106 + name: String, 107 + db: &DbConn, 108 + ) -> Result<CreateAppPasswordOutput> { 109 + let str = &get_random_str()[0..16].to_lowercase(); 110 + let chunks = [&str[0..4], &str[4..8], &str[8..12], &str[12..16]]; 111 + let password = chunks.join("-"); 112 + let password_encrypted = hash_app_password(&did, &password).await?; 113 + 114 + use crate::schema::pds::app_password::dsl as AppPasswordSchema; 115 + 116 + let created_at = now(); 117 + 118 + db.run(move |conn| { 119 + let got: Option<AppPassword> = insert_into(AppPasswordSchema::app_password) 120 + .values(( 121 + AppPasswordSchema::did.eq(did), 122 + AppPasswordSchema::name.eq(&name), 123 + AppPasswordSchema::password.eq(password_encrypted), 124 + AppPasswordSchema::createdAt.eq(&created_at), 125 + )) 126 + .returning(AppPassword::as_select()) 127 + .get_result(conn) 128 + .optional()?; 129 + if got.is_some() { 130 + Ok(CreateAppPasswordOutput { 131 + name, 132 + password, 133 + created_at, 134 + }) 135 + } else { 136 + bail!("could not create app-specific password") 137 + } 138 + }) 139 + .await 140 + } 141 + 142 + pub async fn list_app_passwords(did: &str, db: &DbConn) -> Result<Vec<(String, String)>> { 143 + use crate::schema::pds::app_password::dsl as AppPasswordSchema; 144 + 145 + let did = did.to_owned(); 146 + db.run(move |conn| { 147 + Ok(AppPasswordSchema::app_password 148 + .filter(AppPasswordSchema::did.eq(did)) 149 + .select((AppPasswordSchema::name, AppPasswordSchema::createdAt)) 150 + .get_results(conn)?) 151 + }) 152 + .await 153 + } 154 + 155 + pub async fn update_user_password(opts: UpdateUserPasswordOpts, db: &DbConn) -> Result<()> { 156 + use crate::schema::pds::account::dsl as AccountSchema; 157 + 158 + db.run(move |conn| { 159 + update(AccountSchema::account) 160 + .filter(AccountSchema::did.eq(opts.did)) 161 + .set(AccountSchema::password.eq(opts.password_encrypted)) 162 + .execute(conn)?; 163 + Ok(()) 164 + }) 165 + .await 166 + } 167 + 168 + pub async fn delete_app_password(did: &str, name: &str, db: &DbConn) -> Result<()> { 169 + use crate::schema::pds::app_password::dsl as AppPasswordSchema; 170 + 171 + let did = did.to_owned(); 172 + let name = name.to_owned(); 173 + db.run(move |conn| { 174 + delete(AppPasswordSchema::app_password) 175 + .filter(AppPasswordSchema::did.eq(did)) 176 + .filter(AppPasswordSchema::name.eq(name)) 177 + .execute(conn)?; 178 + Ok(()) 179 + }) 180 + .await 181 + }
+36
src/account_manager/helpers/repo.rs
··· 1 + //! Based on https://github.com/blacksky-algorithms/rsky/blob/main/rsky-pds/src/account_manager/helpers/repo.rs 2 + //! blacksky-algorithms/rsky is licensed under the Apache License 2.0 3 + //! 4 + //! Modified for SQLite backend 5 + use crate::db::DbConn; 6 + use anyhow::Result; 7 + use diesel::*; 8 + use libipld::Cid; 9 + use rsky_common; 10 + 11 + pub async fn update_root(did: String, cid: Cid, rev: String, db: &DbConn) -> Result<()> { 12 + // @TODO balance risk of a race in the case of a long retry 13 + use crate::schema::pds::repo_root::dsl as RepoRootSchema; 14 + 15 + let now = rsky_common::now(); 16 + 17 + db.run(move |conn| { 18 + insert_into(RepoRootSchema::repo_root) 19 + .values(( 20 + RepoRootSchema::did.eq(did), 21 + RepoRootSchema::cid.eq(cid.to_string()), 22 + RepoRootSchema::rev.eq(rev.clone()), 23 + RepoRootSchema::indexedAt.eq(now), 24 + )) 25 + .on_conflict(RepoRootSchema::did) 26 + .do_update() 27 + .set(( 28 + RepoRootSchema::cid.eq(cid.to_string()), 29 + RepoRootSchema::rev.eq(rev), 30 + )) 31 + .execute(conn) 32 + }) 33 + .await?; 34 + 35 + Ok(()) 36 + }
+48 -12
src/account_manager/mod.rs
··· 1 + //! Based on https://github.com/blacksky-algorithms/rsky/blob/main/rsky-pds/src/account_manager/mod.rs 2 + //! blacksky-algorithms/rsky is licensed under the Apache License 2.0 3 + //! 4 + //! Modified for SQLite backend 1 5 use anyhow::Result; 2 6 use chrono::DateTime; 3 7 use chrono::offset::Utc as UtcOffset; 4 8 use cidv10::Cid; 5 9 use futures::try_join; 10 + use helpers::{account, auth, email_token, invite, password}; 6 11 use rsky_common::RFC3339_VARIANT; 7 12 use rsky_common::time::{HOUR, from_micros_to_str, from_str_to_micros}; 8 13 use rsky_lexicon::com::atproto::admin::StatusAttr; 9 14 use rsky_lexicon::com::atproto::server::{AccountCodes, CreateAppPasswordOutput}; 10 - use rsky_pds::account_manager::CreateAccountOpts; 11 15 use rsky_pds::account_manager::helpers::account::{ 12 16 AccountStatus, ActorAccount, AvailabilityFlags, GetAccountAdminStatusOutput, 13 17 }; ··· 17 21 use rsky_pds::account_manager::helpers::invite::CodeDetail; 18 22 use rsky_pds::account_manager::helpers::password::UpdateUserPasswordOpts; 19 23 use rsky_pds::account_manager::helpers::repo; 20 - use rsky_pds::account_manager::helpers::{account, auth, email_token, invite, password}; 24 + use rsky_pds::account_manager::{ 25 + ConfirmEmailOpts, CreateAccountOpts, DisableInviteCodesOpts, ResetPasswordOpts, 26 + UpdateAccountPasswordOpts, UpdateEmailOpts, 27 + }; 21 28 use rsky_pds::auth_verifier::AuthScope; 22 29 use rsky_pds::models::models::EmailTokenPurpose; 23 30 use secp256k1::{Keypair, Secp256k1, SecretKey}; 24 31 use std::collections::BTreeMap; 25 32 use std::env; 26 - use std::sync::Arc; 27 33 use std::time::SystemTime; 28 34 29 - use crate::db::DbConn; 35 + pub(crate) mod helpers { 36 + pub mod account; 37 + pub mod auth; 38 + pub mod email_token; 39 + pub mod invite; 40 + pub mod password; 41 + pub mod repo; 42 + } 30 43 31 - #[derive(Clone, Debug)] 44 + #[derive(Clone)] 32 45 pub struct AccountManager { 33 - pub db: deadpool_diesel::Connection<SqliteConnection>, 46 + pub db: deadpool_diesel::Pool< 47 + deadpool_diesel::Manager<diesel::SqliteConnection>, 48 + deadpool_diesel::sqlite::Object, 49 + >, 50 + } 51 + impl std::fmt::Debug for AccountManager { 52 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 53 + f.debug_struct("AccountManager").finish() 54 + } 34 55 } 35 56 36 - pub type AccountManagerCreator = 37 - Box<dyn Fn(deadpool_diesel::Connection<SqliteConnection>) -> AccountManager + Send + Sync>; 57 + pub type AccountManagerCreator = Box< 58 + dyn Fn( 59 + deadpool_diesel::Pool< 60 + deadpool_diesel::Manager<diesel::SqliteConnection>, 61 + deadpool_diesel::sqlite::Object, 62 + >, 63 + ) -> AccountManager 64 + + Send 65 + + Sync, 66 + >; 38 67 39 68 impl AccountManager { 40 - pub fn new(db: deadpool_diesel::Connection<SqliteConnection>) -> Self { 69 + pub fn new( 70 + db: deadpool_diesel::Pool< 71 + deadpool_diesel::Manager<diesel::SqliteConnection>, 72 + deadpool_diesel::sqlite::Object, 73 + >, 74 + ) -> Self { 41 75 Self { db } 42 76 } 43 77 44 78 pub fn creator() -> AccountManagerCreator { 45 79 Box::new( 46 - move |db: deadpool_diesel::Connection<SqliteConnection>| -> AccountManager { 47 - AccountManager::new(db) 48 - }, 80 + move |db: deadpool_diesel::Pool< 81 + deadpool_diesel::Manager<diesel::SqliteConnection>, 82 + deadpool_diesel::sqlite::Object, 83 + >| 84 + -> AccountManager { AccountManager::new(db) }, 49 85 ) 50 86 } 51 87
+31 -14
src/actor_endpoints.rs
··· 7 7 8 8 async fn put_preferences( 9 9 user: AuthenticatedUser, 10 - State(db): State<Db>, 10 + State(actor_pools): State<std::collections::HashMap<String, ActorPools>>, 11 11 Json(input): Json<actor::put_preferences::Input>, 12 12 ) -> Result<()> { 13 13 let did = user.did(); 14 14 let json_string = 15 15 serde_json::to_string(&input.preferences).context("failed to serialize preferences")?; 16 16 17 - // Use the db connection pool to execute the update 18 - let conn = &mut db.get().context("failed to get database connection")?; 19 - diesel::sql_query("UPDATE accounts SET private_prefs = ? WHERE did = ?") 20 - .bind::<diesel::sql_types::Text, _>(json_string) 21 - .bind::<diesel::sql_types::Text, _>(did) 22 - .execute(conn) 23 - .context("failed to update user preferences")?; 17 + let conn = &mut actor_pools 18 + .get(&did) 19 + .context("failed to get actor pool")? 20 + .repo 21 + .get() 22 + .await 23 + .expect("failed to get database connection"); 24 + conn.interact(move |conn| { 25 + diesel::update(accounts::table) 26 + .filter(accounts::did.eq(did)) 27 + .set(accounts::private_prefs.eq(json_string)) 28 + .execute(conn) 29 + .context("failed to update user preferences") 30 + }); 24 31 25 32 Ok(()) 26 33 } 27 34 28 35 async fn get_preferences( 29 36 user: AuthenticatedUser, 30 - State(db): State<Db>, 37 + State(actor_pools): State<std::collections::HashMap<String, ActorPools>>, 31 38 ) -> Result<Json<actor::get_preferences::Output>> { 32 39 let did = user.did(); 33 - let conn = &mut db.get().context("failed to get database connection")?; 40 + let conn = &mut actor_pools 41 + .get(&did) 42 + .context("failed to get actor pool")? 43 + .repo 44 + .get() 45 + .await 46 + .expect("failed to get database connection"); 34 47 35 48 #[derive(QueryableByName)] 36 49 struct Prefs { ··· 38 51 private_prefs: Option<String>, 39 52 } 40 53 41 - let result = diesel::sql_query("SELECT private_prefs FROM accounts WHERE did = ?") 42 - .bind::<diesel::sql_types::Text, _>(did) 43 - .get_result::<Prefs>(conn) 44 - .context("failed to fetch preferences")?; 54 + let result = conn 55 + .interact(move |conn| { 56 + diesel::sql_query("SELECT private_prefs FROM accounts WHERE did = ?") 57 + .bind::<diesel::sql_types::Text, _>(did) 58 + .get_result::<Prefs>(conn) 59 + }) 60 + .await 61 + .expect("failed to fetch preferences"); 45 62 46 63 if let Some(prefs_json) = result.private_prefs { 47 64 let prefs: actor::defs::Preferences =
+17 -8
src/auth.rs
··· 8 8 use diesel::prelude::*; 9 9 use sha2::{Digest as _, Sha256}; 10 10 11 - use crate::{AppState, Error, db::DbConn, error::ErrorMessage}; 11 + use crate::{AppState, Error, error::ErrorMessage}; 12 12 13 13 /// Request extractor for authenticated users. 14 14 /// If specified in an API endpoint, this guarantees the API can only be called ··· 130 130 131 131 // Extract subject (DID) 132 132 if let Some(did) = claims.get("sub").and_then(serde_json::Value::as_str) { 133 - // Convert SQLx query to Diesel query 134 133 use crate::schema::accounts::dsl as AccountSchema; 135 134 136 135 let _status = state 137 136 .db 138 - .run(move |conn| { 137 + .get() 138 + .await 139 + .expect("failed to get db connection") 140 + .interact(move |conn| { 139 141 AccountSchema::accounts 140 142 .filter(AccountSchema::did.eq(did.to_string())) 141 143 .select(AccountSchema::status) ··· 336 338 337 339 let timestamp = chrono::Utc::now().timestamp(); 338 340 339 - // Convert SQLx JTI check to Diesel 340 341 use crate::schema::oauth_used_jtis::dsl as JtiSchema; 341 342 342 343 // Check if JTI has been used before 343 344 let jti_string = jti.to_string(); 344 345 let jti_used = state 345 346 .db 346 - .run(move |conn| { 347 + .get() 348 + .await 349 + .expect("failed to get db connection") 350 + .interact(move |conn| { 347 351 JtiSchema::oauth_used_jtis 348 352 .filter(JtiSchema::jti.eq(jti_string)) 349 353 .count() ··· 371 375 let thumbprint_str = calculated_thumbprint.to_string(); 372 376 state 373 377 .db 374 - .run(move |conn| { 378 + .get() 379 + .await 380 + .expect("failed to get db connection") 381 + .interact(move |conn| { 375 382 diesel::insert_into(JtiSchema::oauth_used_jtis) 376 383 .values(( 377 384 JtiSchema::jti.eq(jti_str), ··· 386 393 387 394 // Extract subject (DID) from access token 388 395 if let Some(did) = claims.get("sub").and_then(|v| v.as_str) { 389 - // Convert SQLx query to Diesel 390 396 use crate::schema::accounts::dsl as AccountSchema; 391 397 392 398 let _status = state 393 399 .db 394 - .run(move |conn| { 400 + .get() 401 + .await 402 + .expect("failed to get db connection") 403 + .interact(move |conn| { 395 404 AccountSchema::accounts 396 405 .filter(AccountSchema::did.eq(did.to_string())) 397 406 .select(AccountSchema::status)
+81 -81
src/tests.rs
··· 174 174 175 175 /// Start the application in a background task. 176 176 async fn start_app(&self) -> Result<()> { 177 - // Get a reference to the config that can be moved into the task 178 - let config = self.config.clone(); 179 - let address = self.address; 177 + // // Get a reference to the config that can be moved into the task 178 + // let config = self.config.clone(); 179 + // let address = self.address; 180 180 181 - // Start the application in a background task 182 - let _handle = tokio::spawn(async move { 183 - // Set up the application 184 - use crate::*; 181 + // // Start the application in a background task 182 + // let _handle = tokio::spawn(async move { 183 + // // Set up the application 184 + // use crate::*; 185 185 186 - // Initialize metrics (noop in test mode) 187 - drop(metrics::setup(None)); 186 + // // Initialize metrics (noop in test mode) 187 + // drop(metrics::setup(None)); 188 188 189 - // Create client 190 - let simple_client = reqwest::Client::builder() 191 - .user_agent(APP_USER_AGENT) 192 - .build() 193 - .context("failed to build requester client")?; 194 - let client = reqwest_middleware::ClientBuilder::new(simple_client.clone()) 195 - .with(http_cache_reqwest::Cache(http_cache_reqwest::HttpCache { 196 - mode: CacheMode::Default, 197 - manager: MokaManager::default(), 198 - options: HttpCacheOptions::default(), 199 - })) 200 - .build(); 189 + // // Create client 190 + // let simple_client = reqwest::Client::builder() 191 + // .user_agent(APP_USER_AGENT) 192 + // .build() 193 + // .context("failed to build requester client")?; 194 + // let client = reqwest_middleware::ClientBuilder::new(simple_client.clone()) 195 + // .with(http_cache_reqwest::Cache(http_cache_reqwest::HttpCache { 196 + // mode: CacheMode::Default, 197 + // manager: MokaManager::default(), 198 + // options: HttpCacheOptions::default(), 199 + // })) 200 + // .build(); 201 201 202 - // Create a test keypair 203 - std::fs::create_dir_all(config.key.parent().context("should have parent")?)?; 204 - let (skey, rkey) = { 205 - let skey = Secp256k1Keypair::create(&mut rand::thread_rng()); 206 - let rkey = Secp256k1Keypair::create(&mut rand::thread_rng()); 202 + // // Create a test keypair 203 + // std::fs::create_dir_all(config.key.parent().context("should have parent")?)?; 204 + // let (skey, rkey) = { 205 + // let skey = Secp256k1Keypair::create(&mut rand::thread_rng()); 206 + // let rkey = Secp256k1Keypair::create(&mut rand::thread_rng()); 207 207 208 - let keys = KeyData { 209 - skey: skey.export(), 210 - rkey: rkey.export(), 211 - }; 208 + // let keys = KeyData { 209 + // skey: skey.export(), 210 + // rkey: rkey.export(), 211 + // }; 212 212 213 - let mut f = 214 - std::fs::File::create(&config.key).context("failed to create key file")?; 215 - serde_ipld_dagcbor::to_writer(&mut f, &keys) 216 - .context("failed to serialize crypto keys")?; 213 + // let mut f = 214 + // std::fs::File::create(&config.key).context("failed to create key file")?; 215 + // serde_ipld_dagcbor::to_writer(&mut f, &keys) 216 + // .context("failed to serialize crypto keys")?; 217 217 218 - (SigningKey(Arc::new(skey)), RotationKey(Arc::new(rkey))) 219 - }; 218 + // (SigningKey(Arc::new(skey)), RotationKey(Arc::new(rkey))) 219 + // }; 220 220 221 - // Set up database 222 - let opts = SqliteConnectOptions::from_str(&config.db) 223 - .context("failed to parse database options")? 224 - .create_if_missing(true); 225 - let db = SqliteDbConn::connect_with(opts).await?; 221 + // // Set up database 222 + // let opts = SqliteConnectOptions::from_str(&config.db) 223 + // .context("failed to parse database options")? 224 + // .create_if_missing(true); 225 + // let db = SqliteDbConn::connect_with(opts).await?; 226 226 227 - sqlx::migrate!() 228 - .run(&db) 229 - .await 230 - .context("failed to apply migrations")?; 227 + // sqlx::migrate!() 228 + // .run(&db) 229 + // .await 230 + // .context("failed to apply migrations")?; 231 231 232 - // Create firehose 233 - let (_fh, fhp) = firehose::spawn(client.clone(), config.clone()); 232 + // // Create firehose 233 + // let (_fh, fhp) = firehose::spawn(client.clone(), config.clone()); 234 234 235 - // Create the application state 236 - let app_state = AppState { 237 - cred: azure_identity::DefaultAzureCredential::new()?, 238 - config: config.clone(), 239 - db: db.clone(), 240 - client: client.clone(), 241 - simple_client, 242 - firehose: fhp, 243 - signing_key: skey, 244 - rotation_key: rkey, 245 - }; 235 + // // Create the application state 236 + // let app_state = AppState { 237 + // cred: azure_identity::DefaultAzureCredential::new()?, 238 + // config: config.clone(), 239 + // db: db.clone(), 240 + // client: client.clone(), 241 + // simple_client, 242 + // firehose: fhp, 243 + // signing_key: skey, 244 + // rotation_key: rkey, 245 + // }; 246 246 247 - // Create the router 248 - let app = Router::new() 249 - .route("/", get(index)) 250 - .merge(oauth::routes()) 251 - .nest( 252 - "/xrpc", 253 - endpoints::routes() 254 - .merge(actor_endpoints::routes()) 255 - .fallback(service_proxy), 256 - ) 257 - .layer(CorsLayer::permissive()) 258 - .layer(TraceLayer::new_for_http()) 259 - .with_state(app_state); 247 + // // Create the router 248 + // let app = Router::new() 249 + // .route("/", get(index)) 250 + // .merge(oauth::routes()) 251 + // .nest( 252 + // "/xrpc", 253 + // endpoints::routes() 254 + // .merge(actor_endpoints::routes()) 255 + // .fallback(service_proxy), 256 + // ) 257 + // .layer(CorsLayer::permissive()) 258 + // .layer(TraceLayer::new_for_http()) 259 + // .with_state(app_state); 260 260 261 - // Listen for connections 262 - let listener = TcpListener::bind(&address) 263 - .await 264 - .context("failed to bind address")?; 261 + // // Listen for connections 262 + // let listener = TcpListener::bind(&address) 263 + // .await 264 + // .context("failed to bind address")?; 265 265 266 - axum::serve(listener, app.into_make_service()) 267 - .await 268 - .context("failed to serve app") 269 - }); 266 + // axum::serve(listener, app.into_make_service()) 267 + // .await 268 + // .context("failed to serve app") 269 + // }); 270 270 271 - // Give the server a moment to start 272 - tokio::time::sleep(Duration::from_millis(500)).await; 271 + // // Give the server a moment to start 272 + // tokio::time::sleep(Duration::from_millis(500)).await; 273 273 274 274 Ok(()) 275 275 }