Investigation/warning about inbound migration

This commit is contained in:
lewis
2025-12-28 19:28:40 +02:00
parent 613a65b393
commit 013b80adc2
24 changed files with 807 additions and 371 deletions
+4 -5
View File
@@ -468,7 +468,7 @@ pub async fn get_audit_log(
auth: BearerAuth,
Query(params): Query<AuditLogParams>,
) -> Response {
let limit = params.limit.min(100).max(1);
let limit = params.limit.clamp(1, 100);
let offset = params.offset.max(0);
let entries =
@@ -489,10 +489,9 @@ pub async fn get_audit_log(
}
};
let total = match delegation::audit::count_audit_log_entries(&state.db, &auth.0.did).await {
Ok(t) => t,
Err(_) => 0,
};
let total = delegation::audit::count_audit_log_entries(&state.db, &auth.0.did)
.await
.unwrap_or_default();
Json(GetAuditLogResponse {
entries: entries
+71 -27
View File
@@ -69,7 +69,19 @@ pub async fn create_account(
headers: HeaderMap,
Json(input): Json<CreateAccountInput>,
) -> Response {
info!("create_account called");
let is_potential_migration = input
.did
.as_ref()
.map(|d| d.starts_with("did:plc:"))
.unwrap_or(false);
if is_potential_migration {
info!(
"[MIGRATION] createAccount called for potential migration did={:?} handle={}",
input.did, input.handle
);
} else {
info!("create_account called");
}
let client_ip = extract_client_ip(&headers);
if !state
.check_rate_limit(RateLimitKind::AccountCreation, &client_ip)
@@ -136,6 +148,10 @@ pub async fn create_account(
&& let (Some(provided_did), Some(auth_did)) = (input.did.as_ref(), migration_auth.as_ref())
{
if provided_did != auth_did {
info!(
"[MIGRATION] createAccount: Service token mismatch - token_did={} provided_did={}",
auth_did, provided_did
);
return (
StatusCode::FORBIDDEN,
Json(json!({
@@ -148,7 +164,10 @@ pub async fn create_account(
if is_did_web_byod {
info!(did = %provided_did, "Processing did:web BYOD account creation");
} else {
info!(did = %provided_did, "Processing account migration");
info!(
"[MIGRATION] createAccount: Service token verified, processing migration for did={}",
provided_did
);
}
}
@@ -1005,30 +1024,44 @@ pub async fn create_account(
}
let (access_jwt, refresh_jwt) = if is_migration {
let access_meta =
match crate::auth::create_access_token_with_metadata(&did, &secret_key_bytes) {
Ok(m) => m,
Err(e) => {
error!("Error creating access token for migration: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
let refresh_meta =
match crate::auth::create_refresh_token_with_metadata(&did, &secret_key_bytes) {
Ok(m) => m,
Err(e) => {
error!("Error creating refresh token for migration: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
info!(
"[MIGRATION] createAccount: Creating session tokens for migration did={}",
did
);
let access_meta = match crate::auth::create_access_token_with_metadata(
&did,
&secret_key_bytes,
) {
Ok(m) => m,
Err(e) => {
error!(
"[MIGRATION] createAccount: Error creating access token for migration: {:?}",
e
);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
let refresh_meta = match crate::auth::create_refresh_token_with_metadata(
&did,
&secret_key_bytes,
) {
Ok(m) => m,
Err(e) => {
error!(
"[MIGRATION] createAccount: Error creating refresh token for migration: {:?}",
e
);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
if let Err(e) = sqlx::query!(
"INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at) VALUES ($1, $2, $3, $4, $5)",
did,
@@ -1040,18 +1073,29 @@ pub async fn create_account(
.execute(&state.db)
.await
{
error!("Error creating session for migration: {:?}", e);
error!("[MIGRATION] createAccount: Error creating session for migration: {:?}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
info!(
"[MIGRATION] createAccount: Session created successfully for did={}",
did
);
(Some(access_meta.token), Some(refresh_meta.token))
} else {
(None, None)
};
if is_migration {
info!(
"[MIGRATION] createAccount: SUCCESS - Account ready for migration did={} handle={}",
did, handle
);
}
(
StatusCode::OK,
Json(CreateAccountOutput {
+50 -8
View File
@@ -23,22 +23,34 @@ pub async fn submit_plc_operation(
headers: axum::http::HeaderMap,
Json(input): Json<SubmitPlcOperationInput>,
) -> Response {
info!("[MIGRATION] submitPlcOperation called");
let bearer = match crate::auth::extract_bearer_token_from_header(
headers.get("Authorization").and_then(|h| h.to_str().ok()),
) {
Some(t) => t,
None => return ApiError::AuthenticationRequired.into_response(),
None => {
info!("[MIGRATION] submitPlcOperation: No bearer token");
return ApiError::AuthenticationRequired.into_response();
}
};
let auth_user =
match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &bearer).await {
Ok(user) => user,
Err(e) => return ApiError::from(e).into_response(),
Err(e) => {
info!("[MIGRATION] submitPlcOperation: Auth failed: {:?}", e);
return ApiError::from(e).into_response();
}
};
info!(
"[MIGRATION] submitPlcOperation: Authenticated user did={}",
auth_user.did
);
if let Err(e) = crate::auth::scope_check::check_identity_scope(
auth_user.is_oauth,
auth_user.scope.as_deref(),
crate::oauth::scopes::IdentityAttr::Wildcard,
) {
info!("[MIGRATION] submitPlcOperation: Scope check failed");
return e;
}
let did = &auth_user.did;
@@ -188,6 +200,11 @@ pub async fn submit_plc_operation(
let plc_client = PlcClient::new(None);
let operation_clone = input.operation.clone();
let did_clone = did.clone();
info!(
"[MIGRATION] submitPlcOperation: Sending operation to PLC directory for did={}",
did
);
let plc_start = std::time::Instant::now();
let result: Result<(), CircuitBreakerError<PlcError>> =
with_circuit_breaker(&state.circuit_breakers.plc_directory, || async {
plc_client
@@ -196,9 +213,17 @@ pub async fn submit_plc_operation(
})
.await;
match result {
Ok(()) => {}
Ok(()) => {
info!(
"[MIGRATION] submitPlcOperation: PLC directory accepted operation in {:?}",
plc_start.elapsed()
);
}
Err(CircuitBreakerError::CircuitOpen(e)) => {
warn!("PLC directory circuit breaker open: {}", e);
warn!(
"[MIGRATION] submitPlcOperation: PLC directory circuit breaker open: {}",
e
);
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(json!({
@@ -209,7 +234,10 @@ pub async fn submit_plc_operation(
.into_response();
}
Err(CircuitBreakerError::OperationFailed(e)) => {
error!("Failed to submit PLC operation: {:?}", e);
error!(
"[MIGRATION] submitPlcOperation: PLC operation failed: {:?}",
e
);
return (
StatusCode::BAD_GATEWAY,
Json(json!({
@@ -220,6 +248,10 @@ pub async fn submit_plc_operation(
.into_response();
}
}
info!(
"[MIGRATION] submitPlcOperation: Sequencing identity event for did={}",
did
);
match sqlx::query!(
"INSERT INTO repo_seq (did, event_type) VALUES ($1, 'identity') RETURNING seq",
did
@@ -228,17 +260,27 @@ pub async fn submit_plc_operation(
.await
{
Ok(row) => {
info!(
"[MIGRATION] submitPlcOperation: Identity event sequenced with seq={}",
row.seq
);
if let Err(e) = sqlx::query(&format!("NOTIFY repo_updates, '{}'", row.seq))
.execute(&state.db)
.await
{
warn!("Failed to notify identity event: {:?}", e);
warn!(
"[MIGRATION] submitPlcOperation: Failed to notify identity event: {:?}",
e
);
}
}
Err(e) => {
warn!("Failed to sequence identity event: {:?}", e);
warn!(
"[MIGRATION] submitPlcOperation: Failed to sequence identity event: {:?}",
e
);
}
}
info!("Submitted PLC operation for user {}", did);
info!("[MIGRATION] submitPlcOperation: SUCCESS for did={}", did);
(StatusCode::OK, Json(json!({}))).into_response()
}
+22 -36
View File
@@ -1,6 +1,7 @@
use crate::auth::{ServiceTokenVerifier, is_service_token};
use crate::delegation::{self, DelegationActionType};
use crate::state::AppState;
use crate::sync::import::find_blob_refs_ipld;
use axum::body::Bytes;
use axum::{
Json,
@@ -9,13 +10,14 @@ use axum::{
response::{IntoResponse, Response},
};
use cid::Cid;
use ipld_core::ipld::Ipld;
use jacquard_repo::storage::BlockStore;
use multihash::Multihash;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::{Digest, Sha256};
use std::str::FromStr;
use tracing::{debug, error};
use tracing::{debug, error, warn};
const MAX_BLOB_SIZE: usize = 10_000_000_000;
const MAX_VIDEO_BLOB_SIZE: usize = 10_000_000_000;
@@ -258,26 +260,6 @@ pub struct ListMissingBlobsOutput {
pub blobs: Vec<RecordBlob>,
}
fn find_blobs(val: &serde_json::Value, blobs: &mut Vec<String>) {
if let Some(obj) = val.as_object() {
if let Some(type_val) = obj.get("$type")
&& type_val == "blob"
&& let Some(r) = obj.get("ref")
&& let Some(link) = r.get("$link")
&& let Some(s) = link.as_str()
{
blobs.push(s.to_string());
}
for (_, v) in obj {
find_blobs(v, blobs);
}
} else if let Some(arr) = val.as_array() {
for v in arr {
find_blobs(v, blobs);
}
}
}
pub async fn list_missing_blobs(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
@@ -295,16 +277,17 @@ pub async fn list_missing_blobs(
.into_response();
}
};
let auth_user = match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
Ok(user) => user,
Err(_) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed"})),
)
.into_response();
}
};
let auth_user =
match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
Ok(user) => user,
Err(_) => {
return (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "AuthenticationFailed"})),
)
.into_response();
}
};
let did = auth_user.did;
let user_query = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
@@ -362,13 +345,16 @@ pub async fn list_missing_blobs(
Ok(Some(b)) => b,
_ => continue,
};
let record_val: serde_json::Value = match serde_ipld_dagcbor::from_slice(&block_bytes) {
let record_ipld: Ipld = match serde_ipld_dagcbor::from_slice(&block_bytes) {
Ok(v) => v,
Err(_) => continue,
Err(e) => {
warn!("Failed to parse record {} as IPLD: {:?}", record_cid_str, e);
continue;
}
};
let mut blobs = Vec::new();
find_blobs(&record_val, &mut blobs);
for blob_cid_str in blobs {
let blob_refs = find_blob_refs_ipld(&record_ipld, 0);
for blob_ref in blob_refs {
let blob_cid_str = blob_ref.cid;
let exists = sqlx::query!(
"SELECT 1 as one FROM blobs WHERE cid = $1 AND created_by_user = $2",
blob_cid_str,
+15 -15
View File
@@ -350,17 +350,18 @@ pub async fn import_repo(
.into_response();
}
};
let key_bytes = match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) {
Ok(k) => k,
Err(e) => {
error!("Failed to decrypt signing key: {}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
let key_bytes =
match crate::config::decrypt_key(&key_row.key_bytes, key_row.encryption_version) {
Ok(k) => k,
Err(e) => {
error!("Failed to decrypt signing key: {}", e);
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
)
.into_response();
}
};
let signing_key = match SigningKey::from_slice(&key_bytes) {
Ok(k) => k,
Err(e) => {
@@ -422,10 +423,9 @@ pub async fn import_repo(
"Created new commit for imported repo: cid={}, rev={}",
new_root_str, new_rev_str
);
if !is_migration {
if let Err(e) = sequence_import_event(&state, did, &new_root_str).await {
warn!("Failed to sequence import event: {:?}", e);
}
if !is_migration && let Err(e) = sequence_import_event(&state, did, &new_root_str).await
{
warn!("Failed to sequence import event: {:?}", e);
}
(StatusCode::OK, Json(json!({}))).into_response()
}
+3 -5
View File
@@ -11,7 +11,7 @@ use cid::Cid;
use ipld_core::ipld::Ipld;
use jacquard_repo::storage::BlockStore;
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use serde_json::{Map, Value, json};
use std::collections::HashMap;
use std::str::FromStr;
use tracing::{error, info};
@@ -37,10 +37,8 @@ fn ipld_to_json(ipld: Ipld) -> Value {
}
Ipld::List(arr) => Value::Array(arr.into_iter().map(ipld_to_json).collect()),
Ipld::Map(map) => {
let obj: Map<String, Value> = map
.into_iter()
.map(|(k, v)| (k, ipld_to_json(v)))
.collect();
let obj: Map<String, Value> =
map.into_iter().map(|(k, v)| (k, ipld_to_json(v))).collect();
Value::Object(obj)
}
Ipld::Link(cid) => json!({ "$link": cid.to_string() }),
+6 -7
View File
@@ -5,7 +5,7 @@ use jacquard::types::{integer::LimitedU32, string::Tid};
use jacquard_repo::commit::Commit;
use jacquard_repo::storage::BlockStore;
use k256::ecdsa::SigningKey;
use serde_json::{json, Value};
use serde_json::{Value, json};
use std::str::FromStr;
use uuid::Uuid;
@@ -18,12 +18,11 @@ pub fn extract_blob_cids(record: &Value) -> Vec<String> {
fn extract_blob_cids_recursive(value: &Value, blobs: &mut Vec<String>) {
match value {
Value::Object(map) => {
if map.get("$type").and_then(|v| v.as_str()) == Some("blob") {
if let Some(ref_obj) = map.get("ref") {
if let Some(link) = ref_obj.get("$link").and_then(|v| v.as_str()) {
blobs.push(link.to_string());
}
}
if map.get("$type").and_then(|v| v.as_str()) == Some("blob")
&& let Some(ref_obj) = map.get("ref")
&& let Some(link) = ref_obj.get("$link").and_then(|v| v.as_str())
{
blobs.push(link.to_string());
}
for v in map.values() {
extract_blob_cids_recursive(v, blobs);
+94 -21
View File
@@ -8,8 +8,8 @@ use axum::{
response::{IntoResponse, Response},
};
use bcrypt::verify;
use cid::Cid;
use chrono::{Duration, Utc};
use cid::Cid;
use jacquard_repo::commit::Commit;
use jacquard_repo::storage::BlockStore;
use k256::ecdsa::SigningKey;
@@ -216,8 +216,8 @@ async fn assert_valid_did_document_for_service(
})?;
if let Some(row) = user_row {
let key_bytes =
crate::config::decrypt_key(&row.key_bytes, row.encryption_version).map_err(|e| {
let key_bytes = crate::config::decrypt_key(&row.key_bytes, row.encryption_version)
.map_err(|e| {
error!("Failed to decrypt user key: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
@@ -247,21 +247,22 @@ async fn assert_valid_did_document_for_service(
));
}
}
} else if did.starts_with("did:web:") {
} else if let Some(host_and_path) = did.strip_prefix("did:web:") {
let client = reqwest::Client::new();
let host_and_path = &did[8..];
let decoded = host_and_path.replace("%3A", ":");
let parts: Vec<&str> = decoded.split(':').collect();
let (host, path_parts) = if parts.len() > 1 && parts[1].chars().all(|c| c.is_ascii_digit()) {
let (host, path_parts) = if parts.len() > 1 && parts[1].chars().all(|c| c.is_ascii_digit())
{
(format!("{}:{}", parts[0], parts[1]), parts[2..].to_vec())
} else {
(parts[0].to_string(), parts[1..].to_vec())
};
let scheme = if host.starts_with("localhost") || host.starts_with("127.") || host.contains(':') {
"http"
} else {
"https"
};
let scheme =
if host.starts_with("localhost") || host.starts_with("127.") || host.contains(':') {
"http"
} else {
"https"
};
let url = if path_parts.is_empty() {
format!("{}://{}/.well-known/did.json", scheme, host)
} else {
@@ -323,11 +324,15 @@ pub async fn activate_account(
State(state): State<AppState>,
headers: axum::http::HeaderMap,
) -> Response {
info!("[MIGRATION] activateAccount called");
let extracted = match crate::auth::extract_auth_token_from_header(
headers.get("Authorization").and_then(|h| h.to_str().ok()),
) {
Some(t) => t,
None => return ApiError::AuthenticationRequired.into_response(),
None => {
info!("[MIGRATION] activateAccount: No auth token");
return ApiError::AuthenticationRequired.into_response();
}
};
let dpop_proof = headers.get("DPoP").and_then(|h| h.to_str().ok());
let http_uri = format!(
@@ -346,8 +351,15 @@ pub async fn activate_account(
.await
{
Ok(user) => user,
Err(e) => return ApiError::from(e).into_response(),
Err(e) => {
info!("[MIGRATION] activateAccount: Auth failed: {:?}", e);
return ApiError::from(e).into_response();
}
};
info!(
"[MIGRATION] activateAccount: Authenticated user did={}",
auth_user.did
);
if let Err(e) = crate::auth::scope_check::check_account_scope(
auth_user.is_oauth,
@@ -355,42 +367,80 @@ pub async fn activate_account(
crate::oauth::scopes::AccountAttr::Repo,
crate::oauth::scopes::AccountAction::Manage,
) {
info!("[MIGRATION] activateAccount: Scope check failed");
return e;
}
let did = auth_user.did;
info!(
"[MIGRATION] activateAccount: Validating DID document for did={}",
did
);
let did_validation_start = std::time::Instant::now();
if let Err((status, json)) = assert_valid_did_document_for_service(&state.db, &did).await {
info!(
"activateAccount rejected for {}: DID document validation failed",
did
"[MIGRATION] activateAccount: DID document validation FAILED for {} (took {:?})",
did,
did_validation_start.elapsed()
);
return (status, json).into_response();
}
info!(
"[MIGRATION] activateAccount: DID document validation SUCCESS for {} (took {:?})",
did,
did_validation_start.elapsed()
);
let handle = sqlx::query_scalar!("SELECT handle FROM users WHERE did = $1", did)
.fetch_optional(&state.db)
.await
.ok()
.flatten();
info!(
"[MIGRATION] activateAccount: Activating account did={} handle={:?}",
did, handle
);
let result = sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did)
.execute(&state.db)
.await;
match result {
Ok(_) => {
info!(
"[MIGRATION] activateAccount: DB update success for did={}",
did
);
if let Some(ref h) = handle {
let _ = state.cache.delete(&format!("handle:{}", h)).await;
}
info!(
"[MIGRATION] activateAccount: Sequencing account event (active=true) for did={}",
did
);
if let Err(e) =
crate::api::repo::record::sequence_account_event(&state, &did, true, None).await
{
warn!("Failed to sequence account activation event: {}", e);
warn!(
"[MIGRATION] activateAccount: Failed to sequence account activation event: {}",
e
);
} else {
info!("[MIGRATION] activateAccount: Account event sequenced successfully");
}
info!(
"[MIGRATION] activateAccount: Sequencing identity event for did={} handle={:?}",
did, handle
);
if let Err(e) =
crate::api::repo::record::sequence_identity_event(&state, &did, handle.as_deref())
.await
{
warn!("Failed to sequence identity event for activation: {}", e);
warn!(
"[MIGRATION] activateAccount: Failed to sequence identity event for activation: {}",
e
);
} else {
info!("[MIGRATION] activateAccount: Identity event sequenced successfully");
}
let repo_root = sqlx::query_scalar!(
"SELECT r.repo_root_cid FROM repos r JOIN users u ON r.user_id = u.id WHERE u.did = $1",
@@ -401,6 +451,10 @@ pub async fn activate_account(
.ok()
.flatten();
if let Some(root_cid) = repo_root {
info!(
"[MIGRATION] activateAccount: Sequencing sync event for did={} root_cid={}",
did, root_cid
);
let rev = if let Ok(cid) = Cid::from_str(&root_cid) {
if let Ok(Some(block)) = state.block_store.get(&cid).await {
Commit::from_cbor(&block).ok().map(|c| c.rev().to_string())
@@ -410,16 +464,35 @@ pub async fn activate_account(
} else {
None
};
if let Err(e) =
crate::api::repo::record::sequence_sync_event(&state, &did, &root_cid, rev.as_deref()).await
if let Err(e) = crate::api::repo::record::sequence_sync_event(
&state,
&did,
&root_cid,
rev.as_deref(),
)
.await
{
warn!("Failed to sequence sync event for activation: {}", e);
warn!(
"[MIGRATION] activateAccount: Failed to sequence sync event for activation: {}",
e
);
} else {
info!("[MIGRATION] activateAccount: Sync event sequenced successfully");
}
} else {
warn!(
"[MIGRATION] activateAccount: No repo root found for did={}",
did
);
}
info!("[MIGRATION] activateAccount: SUCCESS for did={}", did);
(StatusCode::OK, Json(json!({}))).into_response()
}
Err(e) => {
error!("DB error activating account: {:?}", e);
error!(
"[MIGRATION] activateAccount: DB error activating account: {:?}",
e
);
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"error": "InternalError"})),
+3 -3
View File
@@ -26,6 +26,9 @@ pub use email::{confirm_email, request_email_update, update_email};
pub use invite::{create_invite_code, create_invite_codes, get_account_invite_codes};
pub use logo::get_logo;
pub use meta::{describe_server, health, robots_txt};
pub use migration::{
clear_migration_forwarding, get_migration_status, update_migration_forwarding,
};
pub use passkey_account::{
complete_passkey_setup, create_passkey_account, recover_passkey_account,
request_passkey_recovery, start_passkey_registration_for_setup,
@@ -57,8 +60,5 @@ pub use trusted_devices::{
extend_device_trust, is_device_trusted, list_trusted_devices, revoke_trusted_device,
trust_device, update_trusted_device,
};
pub use migration::{
clear_migration_forwarding, get_migration_status, update_migration_forwarding,
};
pub use verify_email::{resend_migration_verification, verify_migration_email};
pub use verify_token::{VerifyTokenInput, VerifyTokenOutput, verify_token, verify_token_internal};
+1 -5
View File
@@ -1,8 +1,4 @@
use axum::{
Json,
extract::State,
http::StatusCode,
};
use axum::{Json, extract::State, http::StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::{error, info, warn};
+13 -16
View File
@@ -229,24 +229,21 @@ async fn validate_bearer_token_with_options_internal(
.ok()
.flatten();
match session_row {
Some(row) => {
if row.access_expires_at > chrono::Utc::now() {
session_valid = true;
if let Some(c) = cache {
let _ = c
.set(
&session_cache_key,
"1",
Duration::from_secs(SESSION_CACHE_TTL_SECS),
)
.await;
}
} else {
return Err(TokenValidationError::TokenExpired);
if let Some(row) = session_row {
if row.access_expires_at > chrono::Utc::now() {
session_valid = true;
if let Some(c) = cache {
let _ = c
.set(
&session_cache_key,
"1",
Duration::from_secs(SESSION_CACHE_TTL_SECS),
)
.await;
}
} else {
return Err(TokenValidationError::TokenExpired);
}
None => {}
}
}
+2 -3
View File
@@ -1,5 +1,5 @@
use async_trait::async_trait;
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use reqwest::Client;
use serde_json::json;
use std::process::Stdio;
@@ -103,8 +103,7 @@ impl EmailSender {
}
pub fn format_email(&self, notification: &QueuedComms) -> String {
let subject =
mime_encode_header(notification.subject.as_deref().unwrap_or("Notification"));
let subject = mime_encode_header(notification.subject.as_deref().unwrap_or("Notification"));
let recipient = sanitize_header_value(&notification.recipient);
let from_header = if self.from_name.is_empty() {
self.from_address.clone()
+9 -11
View File
@@ -75,12 +75,10 @@ pub fn intersect_scopes(requested: &str, granted: &str) -> String {
}
fn find_matching_scope<'a>(requested: &str, granted: &HashSet<&'a str>) -> Option<&'a str> {
for granted_scope in granted {
if scopes_compatible(granted_scope, requested) {
return Some(granted_scope);
}
}
None
granted
.iter()
.find(|&granted_scope| scopes_compatible(granted_scope, requested))
.map(|v| v as _)
}
fn scopes_compatible(granted: &str, requested: &str) -> bool {
@@ -97,11 +95,11 @@ fn scopes_compatible(granted: &str, requested: &str) -> bool {
return true;
}
if granted_base.ends_with(".*") {
let prefix = &granted_base[..granted_base.len() - 2];
if requested_base.starts_with(prefix) && requested_base.len() > prefix.len() {
return true;
}
if let Some(prefix) = granted_base.strip_suffix(".*")
&& requested_base.starts_with(prefix)
&& requested_base.len() > prefix.len()
{
return true;
}
false
+75 -66
View File
@@ -1,16 +1,16 @@
/*
* CONTENT WARNING
*
* This file contains explicit slurs and hateful language. We're sorry you have to see them.
*
* These words exist here for one reason: to ensure our moderation system correctly blocks them.
* We can't verify the filter catches the n-word without testing against the actual word.
* Euphemisms wouldn't prove the protection works.
*
* If reading this file has caused you distress, please know:
* - you are valued and welcome in this community
* - these words do not reflect the views of this project or its contributors
* - we maintain this code precisely because we believe everyone deserves an experience on the web that is free from this kinda language
* CONTENT WARNING
*
* This file contains explicit slurs and hateful language. We're sorry you have to see them.
*
* These words exist here for one reason: to ensure our moderation system correctly blocks them.
* We can't verify the filter catches the n-word without testing against the actual word.
* Euphemisms wouldn't prove the protection works.
*
* If reading this file has caused you distress, please know:
* - you are valued and welcome in this community
* - these words do not reflect the views of this project or its contributors
* - we maintain this code precisely because we believe everyone deserves an experience on the web that is free from this kinda language
*/
use regex::Regex;
@@ -70,7 +70,7 @@ pub fn has_explicit_slur(text: &str) -> bool {
fn has_explicit_slur_with_extra_words(text: &str, extra_words: &[String]) -> bool {
let text_lower = text.to_lowercase();
let normalized = text_lower.replace('.', "").replace('-', "").replace('_', "");
let normalized = text_lower.replace(['.', '-', '_'], "");
let stripped = strip_trailing_digits(&text_lower);
let normalized_stripped = strip_trailing_digits(&normalized);
@@ -104,81 +104,87 @@ fn has_explicit_slur_with_extra_words(text: &str, extra_words: &[String]) -> boo
#[cfg(test)]
mod tests {
use super::*;
use base64::Engine;
fn d(b64: &str) -> String {
let bytes = base64::engine::general_purpose::STANDARD.decode(b64).unwrap();
String::from_utf8(bytes).unwrap()
}
#[test]
fn test_chink_pattern() {
assert!(has_explicit_slur("chink"));
assert!(has_explicit_slur("chinks"));
assert!(has_explicit_slur("CHINK"));
assert!(has_explicit_slur("Chinks"));
assert!(has_explicit_slur(&d("Y2hpbms=")));
assert!(has_explicit_slur(&d("Y2hpbmtz")));
assert!(has_explicit_slur(&d("Q0hJTks=")));
assert!(has_explicit_slur(&d("Q2hpbmtz")));
}
#[test]
fn test_coon_pattern() {
assert!(has_explicit_slur("coon"));
assert!(has_explicit_slur("coons"));
assert!(has_explicit_slur("COON"));
assert!(has_explicit_slur(&d("Y29vbg==")));
assert!(has_explicit_slur(&d("Y29vbnM=")));
assert!(has_explicit_slur(&d("Q09PTg==")));
}
#[test]
fn test_fag_pattern() {
assert!(has_explicit_slur("fag"));
assert!(has_explicit_slur("fags"));
assert!(has_explicit_slur("faggot"));
assert!(has_explicit_slur("faggots"));
assert!(has_explicit_slur("faggotry"));
assert!(has_explicit_slur(&d("ZmFn")));
assert!(has_explicit_slur(&d("ZmFncw==")));
assert!(has_explicit_slur(&d("ZmFnZ290")));
assert!(has_explicit_slur(&d("ZmFnZ290cw==")));
assert!(has_explicit_slur(&d("ZmFnZ290cnk=")));
}
#[test]
fn test_kike_pattern() {
assert!(has_explicit_slur("kike"));
assert!(has_explicit_slur("kikes"));
assert!(has_explicit_slur("KIKE"));
assert!(has_explicit_slur("kikery"));
assert!(has_explicit_slur(&d("a2lrZQ==")));
assert!(has_explicit_slur(&d("a2lrZXM=")));
assert!(has_explicit_slur(&d("S0lLRQ==")));
assert!(has_explicit_slur(&d("a2lrZXJ5")));
}
#[test]
fn test_nigger_pattern() {
assert!(has_explicit_slur("nigger"));
assert!(has_explicit_slur("niggers"));
assert!(has_explicit_slur("NIGGER"));
assert!(has_explicit_slur("nigga"));
assert!(has_explicit_slur("niggas"));
assert!(has_explicit_slur(&d("bmlnZ2Vy")));
assert!(has_explicit_slur(&d("bmlnZ2Vycw==")));
assert!(has_explicit_slur(&d("TklHR0VS")));
assert!(has_explicit_slur(&d("bmlnZ2E=")));
assert!(has_explicit_slur(&d("bmlnZ2Fz")));
}
#[test]
fn test_tranny_pattern() {
assert!(has_explicit_slur("tranny"));
assert!(has_explicit_slur("trannies"));
assert!(has_explicit_slur("TRANNY"));
assert!(has_explicit_slur(&d("dHJhbm55")));
assert!(has_explicit_slur(&d("dHJhbm5pZXM=")));
assert!(has_explicit_slur(&d("VFJBTk5Z")));
}
#[test]
fn test_normalization_bypass() {
assert!(has_explicit_slur("n.i.g.g.e.r"));
assert!(has_explicit_slur("n-i-g-g-e-r"));
assert!(has_explicit_slur("n_i_g_g_e_r"));
assert!(has_explicit_slur("f.a.g"));
assert!(has_explicit_slur("f-a-g"));
assert!(has_explicit_slur("c.h.i.n.k"));
assert!(has_explicit_slur("k_i_k_e"));
assert!(has_explicit_slur(&d("bi5pLmcuZy5lLnI=")));
assert!(has_explicit_slur(&d("bi1pLWctZy1lLXI=")));
assert!(has_explicit_slur(&d("bl9pX2dfZ19lX3I=")));
assert!(has_explicit_slur(&d("Zi5hLmc=")));
assert!(has_explicit_slur(&d("Zi1hLWc=")));
assert!(has_explicit_slur(&d("Yy5oLmkubi5r")));
assert!(has_explicit_slur(&d("a19pX2tfZQ==")));
}
#[test]
fn test_trailing_digits_bypass() {
assert!(has_explicit_slur("faggot123"));
assert!(has_explicit_slur("nigger69"));
assert!(has_explicit_slur("chink420"));
assert!(has_explicit_slur("fag1"));
assert!(has_explicit_slur("kike2024"));
assert!(has_explicit_slur("n_i_g_g_e_r123"));
assert!(has_explicit_slur(&d("ZmFnZ290MTIz")));
assert!(has_explicit_slur(&d("bmlnZ2VyNjk=")));
assert!(has_explicit_slur(&d("Y2hpbms0MjA=")));
assert!(has_explicit_slur(&d("ZmFnMQ==")));
assert!(has_explicit_slur(&d("a2lrZTIwMjQ=")));
assert!(has_explicit_slur(&d("bl9pX2dfZ19lX3IxMjM=")));
}
#[test]
fn test_embedded_in_sentence() {
assert!(has_explicit_slur("you are a faggot"));
assert!(has_explicit_slur("stupid nigger"));
assert!(has_explicit_slur("go away chink"));
assert!(has_explicit_slur(&d("eW91IGFyZSBhIGZhZ2dvdA==")));
assert!(has_explicit_slur(&d("c3R1cGlkIG5pZ2dlcg==")));
assert!(has_explicit_slur(&d("Z28gYXdheSBjaGluaw==")));
}
#[test]
@@ -210,22 +216,22 @@ mod tests {
#[test]
fn test_case_insensitive() {
assert!(has_explicit_slur("NIGGER"));
assert!(has_explicit_slur("Nigger"));
assert!(has_explicit_slur("NiGgEr"));
assert!(has_explicit_slur("FAGGOT"));
assert!(has_explicit_slur("Faggot"));
assert!(has_explicit_slur(&d("TklHR0VS")));
assert!(has_explicit_slur(&d("TmlnZ2Vy")));
assert!(has_explicit_slur(&d("TmlHZ0Vy")));
assert!(has_explicit_slur(&d("RkFHR09U")));
assert!(has_explicit_slur(&d("RmFnZ290")));
}
#[test]
fn test_leetspeak_bypass() {
assert!(has_explicit_slur("f4ggot"));
assert!(has_explicit_slur("f4gg0t"));
assert!(has_explicit_slur("n1gger"));
assert!(has_explicit_slur("n1gg3r"));
assert!(has_explicit_slur("k1ke"));
assert!(has_explicit_slur("ch1nk"));
assert!(has_explicit_slur("tr4nny"));
assert!(has_explicit_slur(&d("ZjRnZ290")));
assert!(has_explicit_slur(&d("ZjRnZzB0")));
assert!(has_explicit_slur(&d("bjFnZ2Vy")));
assert!(has_explicit_slur(&d("bjFnZzNy")));
assert!(has_explicit_slur(&d("azFrZQ==")));
assert!(has_explicit_slur(&d("Y2gxbms=")));
assert!(has_explicit_slur(&d("dHI0bm55")));
}
#[test]
@@ -253,7 +259,10 @@ mod tests {
assert!(has_explicit_slur_with_extra_words("b4dw0rd", &extra));
assert!(has_explicit_slur_with_extra_words("b4dw0rd789", &extra));
assert!(has_explicit_slur_with_extra_words("b.4.d.w.0.r.d", &extra));
assert!(has_explicit_slur_with_extra_words("this contains badword here", &extra));
assert!(has_explicit_slur_with_extra_words(
"this contains badword here",
&extra
));
assert!(has_explicit_slur_with_extra_words("0ff3n$1v3", &extra));
assert!(!has_explicit_slur_with_extra_words("goodword", &extra));
+12 -9
View File
@@ -88,7 +88,10 @@ pub async fn delegation_auth(
}
};
if let Err(_) = db::set_request_did(&state.db, &form.request_uri, &delegated_did).await {
if db::set_request_did(&state.db, &form.request_uri, &delegated_did)
.await
.is_err()
{
tracing::warn!("Failed to set delegated DID on authorization request");
}
@@ -168,13 +171,11 @@ pub async fn delegation_auth(
.into_response();
}
let password_valid = match &controller.password_hash {
Some(hash) => match bcrypt::verify(&form.password, hash) {
Ok(valid) => valid,
Err(_) => false,
},
None => false,
};
let password_valid = controller
.password_hash
.as_ref()
.map(|hash| bcrypt::verify(&form.password, hash).unwrap_or_default())
.unwrap_or_default();
if !password_valid {
return Json(DelegationAuthResponse {
@@ -186,7 +187,9 @@ pub async fn delegation_auth(
.into_response();
}
if let Err(_) = db::set_controller_did(&state.db, &form.request_uri, &form.controller_did).await
if db::set_controller_did(&state.db, &form.request_uri, &form.controller_did)
.await
.is_err()
{
return Json(DelegationAuthResponse {
success: false,
+27 -26
View File
@@ -189,13 +189,16 @@ fn walk_mst_node(
if let Some(Ipld::List(entries)) = obj.get("e") {
for entry in entries {
if let Ipld::Map(entry_obj) = entry {
let prefix_len = entry_obj.get("p").and_then(|p| {
if let Ipld::Integer(n) = p {
Some(*n as usize)
} else {
None
}
}).unwrap_or(0);
let prefix_len = entry_obj
.get("p")
.and_then(|p| {
if let Ipld::Integer(n) = p {
Some(*n as usize)
} else {
None
}
})
.unwrap_or(0);
let key_suffix = entry_obj.get("k").and_then(|k| {
if let Ipld::Bytes(b) = k {
@@ -222,25 +225,23 @@ fn walk_mst_node(
}
});
if let Some(record_cid) = record_cid {
if let Ok(full_key) = String::from_utf8(current_key.clone()) {
if let Some(record_block) = blocks.get(&record_cid)
&& let Ok(record_value) =
serde_ipld_dagcbor::from_slice::<Ipld>(record_block)
{
let blob_refs = find_blob_refs_ipld(&record_value, 0);
let parts: Vec<&str> = full_key.split('/').collect();
if parts.len() >= 2 {
let collection = parts[..parts.len() - 1].join("/");
let rkey = parts[parts.len() - 1].to_string();
records.push(ImportedRecord {
collection,
rkey,
cid: record_cid,
blob_refs,
});
}
}
if let Some(record_cid) = record_cid
&& let Ok(full_key) = String::from_utf8(current_key.clone())
&& let Some(record_block) = blocks.get(&record_cid)
&& let Ok(record_value) =
serde_ipld_dagcbor::from_slice::<Ipld>(record_block)
{
let blob_refs = find_blob_refs_ipld(&record_value, 0);
let parts: Vec<&str> = full_key.split('/').collect();
if parts.len() >= 2 {
let collection = parts[..parts.len() - 1].join("/");
let rkey = parts[parts.len() - 1].to_string();
records.push(ImportedRecord {
collection,
rkey,
cid: record_cid,
blob_refs,
});
}
}
}
+13 -14
View File
@@ -161,14 +161,13 @@ impl RecordValidator {
.get("$type")
.and_then(|v| v.as_str())
.is_some_and(|t| t == "app.bsky.richtext.facet#tag");
if is_tag {
if let Some(tag) = feature.get("tag").and_then(|v| v.as_str()) {
if crate::moderation::has_explicit_slur(tag) {
return Err(ValidationError::BannedContent {
path: format!("facets/{}/features/{}/tag", i, j),
});
}
}
if is_tag
&& let Some(tag) = feature.get("tag").and_then(|v| v.as_str())
&& crate::moderation::has_explicit_slur(tag)
{
return Err(ValidationError::BannedContent {
path: format!("facets/{}/features/{}/tag", i, j),
});
}
}
}
@@ -332,12 +331,12 @@ impl RecordValidator {
if !obj.contains_key("createdAt") {
return Err(ValidationError::MissingField("createdAt".to_string()));
}
if let Some(rkey) = rkey {
if crate::moderation::has_explicit_slur(rkey) {
return Err(ValidationError::BannedContent {
path: "rkey".to_string(),
});
}
if let Some(rkey) = rkey
&& crate::moderation::has_explicit_slur(rkey)
{
return Err(ValidationError::BannedContent {
path: "rkey".to_string(),
});
}
if let Some(display_name) = obj.get("displayName").and_then(|v| v.as_str()) {
if display_name.is_empty() || display_name.len() > 240 {