feat(tranquil-store): repository traits on MetastoreClient

Lewis: May this revision serve well! <lu5a@proton.me>
This commit is contained in:
Lewis
2026-03-27 21:24:26 -07:00
parent 8dd442dc64
commit 4fcb889942
29 changed files with 19220 additions and 89 deletions

22
Cargo.lock generated
View File

@@ -6979,6 +6979,26 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "tikv-jemalloc-sys"
version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd8aa5b2ab86a2cefa406d889139c162cbb230092f7d1d7cbc1716405d852a3b"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "tikv-jemallocator"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0359b4327f954e0567e69fb191cf1436617748813819c94b8cd4a431422d053a"
dependencies = [
"libc",
"tikv-jemalloc-sys",
]
[[package]]
name = "time"
version = "0.3.47"
@@ -7863,10 +7883,12 @@ dependencies = [
"sqlx",
"tempfile",
"thiserror 2.0.18",
"tikv-jemallocator",
"tokio",
"tracing",
"tranquil-db",
"tranquil-db-traits",
"tranquil-oauth",
"tranquil-repo",
"tranquil-types",
"uuid",

View File

@@ -127,7 +127,7 @@ pub async fn revoke_trusted_device(
state
.repos
.oauth
.revoke_device_trust(&input.device_id)
.revoke_device_trust(&input.device_id, &auth.did)
.await
.log_db_err("revoking device trust")?;
@@ -166,7 +166,7 @@ pub async fn update_trusted_device(
state
.repos
.oauth
.update_device_friendly_name(&input.device_id, input.friendly_name.as_deref())
.update_device_friendly_name(&input.device_id, &auth.did, input.friendly_name.as_deref())
.await
.log_db_err("updating device friendly name")?;
@@ -198,18 +198,22 @@ pub async fn is_device_trusted(
pub async fn trust_device(
oauth_repo: &dyn OAuthRepository,
device_id: &DeviceId,
did: &tranquil_types::Did,
) -> Result<(), tranquil_db_traits::DbError> {
let now = Utc::now();
let trusted_until = now + Duration::days(TRUST_DURATION_DAYS);
oauth_repo.trust_device(device_id, now, trusted_until).await
oauth_repo
.trust_device(device_id, did, now, trusted_until)
.await
}
pub async fn extend_device_trust(
oauth_repo: &dyn OAuthRepository,
device_id: &DeviceId,
did: &tranquil_types::Did,
) -> Result<(), tranquil_db_traits::DbError> {
let trusted_until = Utc::now() + Duration::days(TRUST_DURATION_DAYS);
oauth_repo
.extend_device_trust(device_id, trusted_until)
.extend_device_trust(device_id, did, trusted_until)
.await
}

View File

@@ -291,21 +291,24 @@ pub trait OAuthRepository: Send + Sync {
device_id: &DeviceId,
did: &Did,
) -> Result<bool, DbError>;
async fn revoke_device_trust(&self, device_id: &DeviceId) -> Result<(), DbError>;
async fn revoke_device_trust(&self, device_id: &DeviceId, did: &Did) -> Result<(), DbError>;
async fn update_device_friendly_name(
&self,
device_id: &DeviceId,
did: &Did,
friendly_name: Option<&str>,
) -> Result<(), DbError>;
async fn trust_device(
&self,
device_id: &DeviceId,
did: &Did,
trusted_at: DateTime<Utc>,
trusted_until: DateTime<Utc>,
) -> Result<(), DbError>;
async fn extend_device_trust(
&self,
device_id: &DeviceId,
did: &Did,
trusted_until: DateTime<Utc>,
) -> Result<(), DbError>;

View File

@@ -1194,7 +1194,7 @@ impl OAuthRepository for PostgresOAuthRepository {
Ok(exists.is_some())
}
async fn revoke_device_trust(&self, device_id: &DeviceId) -> Result<(), DbError> {
async fn revoke_device_trust(&self, device_id: &DeviceId, _did: &Did) -> Result<(), DbError> {
sqlx::query!(
"UPDATE oauth_device SET trusted_at = NULL, trusted_until = NULL WHERE id = $1",
device_id.as_str()
@@ -1208,6 +1208,7 @@ impl OAuthRepository for PostgresOAuthRepository {
async fn update_device_friendly_name(
&self,
device_id: &DeviceId,
_did: &Did,
friendly_name: Option<&str>,
) -> Result<(), DbError> {
sqlx::query!(
@@ -1224,6 +1225,7 @@ impl OAuthRepository for PostgresOAuthRepository {
async fn trust_device(
&self,
device_id: &DeviceId,
_did: &Did,
trusted_at: DateTime<Utc>,
trusted_until: DateTime<Utc>,
) -> Result<(), DbError> {
@@ -1242,6 +1244,7 @@ impl OAuthRepository for PostgresOAuthRepository {
async fn extend_device_trust(
&self,
device_id: &DeviceId,
_did: &Did,
trusted_until: DateTime<Utc>,
) -> Result<(), DbError> {
sqlx::query!(

View File

@@ -529,9 +529,12 @@ pub async fn authorize_post(
if device_is_trusted {
if let Some(ref dev_id) = device_cookie {
let _ =
tranquil_api::server::extend_device_trust(state.repos.oauth.as_ref(), dev_id)
.await;
let _ = tranquil_api::server::extend_device_trust(
state.repos.oauth.as_ref(),
dev_id,
&user.did,
)
.await;
}
} else {
if state
@@ -892,7 +895,8 @@ pub async fn authorize_select(
.into_response();
}
let _ =
tranquil_api::server::extend_device_trust(state.repos.oauth.as_ref(), &device_id).await;
tranquil_api::server::extend_device_trust(state.repos.oauth.as_ref(), &device_id, &did)
.await;
}
if user.two_factor_enabled {
let _ = state

View File

@@ -1074,9 +1074,12 @@ pub async fn authorize_passkey_finish(
if device_is_trusted {
if let Some(ref dev_id) = device_cookie {
let _ =
tranquil_api::server::extend_device_trust(state.repos.oauth.as_ref(), dev_id)
.await;
let _ = tranquil_api::server::extend_device_trust(
state.repos.oauth.as_ref(),
dev_id,
&did,
)
.await;
}
} else {
let user = match state.repos.user.get_2fa_status_by_did(&did).await {

View File

@@ -273,7 +273,8 @@ pub async fn authorize_2fa_post(
.upsert_account_device(&did, &trust_device_id)
.await;
let _ =
tranquil_api::server::trust_device(state.repos.oauth.as_ref(), &trust_device_id).await;
tranquil_api::server::trust_device(state.repos.oauth.as_ref(), &trust_device_id, &did)
.await;
}
let requested_scope_str = request_data
.parameters

View File

@@ -1 +1,72 @@
pub use tranquil_repo::{PostgresBlockStore, TrackingBlockStore};
pub use tranquil_repo::PostgresBlockStore;
pub type TrackingBlockStore = tranquil_repo::TrackingBlockStore<AnyBlockStore>;
use bytes::Bytes;
use cid::Cid;
use jacquard_repo::error::RepoError;
use jacquard_repo::repo::CommitData;
use jacquard_repo::storage::BlockStore;
use tranquil_store::blockstore::TranquilBlockStore;
#[derive(Clone)]
pub enum AnyBlockStore {
Postgres(PostgresBlockStore),
TranquilStore(TranquilBlockStore),
}
impl AnyBlockStore {
pub fn as_postgres(&self) -> Option<&PostgresBlockStore> {
match self {
Self::Postgres(s) => Some(s),
Self::TranquilStore(_) => None,
}
}
}
impl BlockStore for AnyBlockStore {
async fn get(&self, cid: &Cid) -> Result<Option<Bytes>, RepoError> {
match self {
Self::Postgres(s) => s.get(cid).await,
Self::TranquilStore(s) => s.get(cid).await,
}
}
async fn put(&self, data: &[u8]) -> Result<Cid, RepoError> {
match self {
Self::Postgres(s) => s.put(data).await,
Self::TranquilStore(s) => s.put(data).await,
}
}
async fn has(&self, cid: &Cid) -> Result<bool, RepoError> {
match self {
Self::Postgres(s) => s.has(cid).await,
Self::TranquilStore(s) => s.has(cid).await,
}
}
async fn put_many(
&self,
blocks: impl IntoIterator<Item = (Cid, Bytes)> + Send,
) -> Result<(), RepoError> {
match self {
Self::Postgres(s) => s.put_many(blocks).await,
Self::TranquilStore(s) => s.put_many(blocks).await,
}
}
async fn get_many(&self, cids: &[Cid]) -> Result<Vec<Option<Bytes>>, RepoError> {
match self {
Self::Postgres(s) => s.get_many(cids).await,
Self::TranquilStore(s) => s.get_many(cids).await,
}
}
async fn apply_commit(&self, commit: CommitData) -> Result<(), RepoError> {
match self {
Self::Postgres(s) => s.apply_commit(commit).await,
Self::TranquilStore(s) => s.apply_commit(commit).await,
}
}
}

View File

@@ -15,7 +15,7 @@ use tranquil_db_traits::{
};
use tranquil_types::{AtUri, CidLink, Did};
use crate::repo::PostgresBlockStore;
use crate::repo::AnyBlockStore;
use crate::storage::BlobStorage;
use crate::sync::car::encode_car_header;
@@ -44,7 +44,7 @@ impl std::fmt::Display for GenesisBackfillError {
async fn process_genesis_commit(
repo_repo: &dyn RepoRepository,
block_store: &PostgresBlockStore,
block_store: &AnyBlockStore,
row: BrokenGenesisCommit,
) -> Result<(Did, SequenceNumber), (SequenceNumber, GenesisBackfillError)> {
let commit_cid_str = row
@@ -69,7 +69,7 @@ async fn process_genesis_commit(
pub async fn backfill_genesis_commit_blocks(
repo_repo: Arc<dyn RepoRepository>,
block_store: PostgresBlockStore,
block_store: AnyBlockStore,
) {
let broken_genesis_commits = match repo_repo.get_broken_genesis_commits().await {
Ok(rows) => rows,
@@ -122,7 +122,7 @@ pub async fn backfill_genesis_commit_blocks(
async fn process_repo_rev(
repo_repo: &dyn RepoRepository,
block_store: &PostgresBlockStore,
block_store: &AnyBlockStore,
user_id: uuid::Uuid,
repo_root_cid: String,
) -> Result<uuid::Uuid, uuid::Uuid> {
@@ -147,10 +147,7 @@ async fn process_repo_rev(
Ok(user_id)
}
pub async fn backfill_repo_rev(
repo_repo: Arc<dyn RepoRepository>,
block_store: PostgresBlockStore,
) {
pub async fn backfill_repo_rev(repo_repo: Arc<dyn RepoRepository>, block_store: AnyBlockStore) {
let repos_missing_rev = match repo_repo.get_repos_without_rev().await {
Ok(rows) => rows,
Err(e) => {
@@ -197,7 +194,7 @@ pub async fn backfill_repo_rev(
async fn process_user_blocks(
repo_repo: &dyn RepoRepository,
block_store: &PostgresBlockStore,
block_store: &AnyBlockStore,
user_id: uuid::Uuid,
repo_root_cid: String,
repo_rev: Option<String>,
@@ -218,10 +215,7 @@ async fn process_user_blocks(
Ok((user_id, count))
}
pub async fn backfill_user_blocks(
repo_repo: Arc<dyn RepoRepository>,
block_store: PostgresBlockStore,
) {
pub async fn backfill_user_blocks(repo_repo: Arc<dyn RepoRepository>, block_store: AnyBlockStore) {
let users_without_blocks = match repo_repo.get_users_without_blocks().await {
Ok(rows) => rows,
Err(e) => {
@@ -271,7 +265,7 @@ pub async fn backfill_user_blocks(
}
pub async fn collect_current_repo_blocks(
block_store: &PostgresBlockStore,
block_store: &AnyBlockStore,
head_cid: &Cid,
) -> anyhow::Result<Vec<Vec<u8>>> {
let mut block_cids: Vec<Vec<u8>> = Vec::new();
@@ -324,7 +318,7 @@ pub async fn collect_current_repo_blocks(
async fn process_record_blobs(
repo_repo: &dyn RepoRepository,
block_store: &PostgresBlockStore,
block_store: &AnyBlockStore,
user_id: uuid::Uuid,
did: Did,
) -> Result<(uuid::Uuid, Did, usize), (uuid::Uuid, &'static str)> {
@@ -383,10 +377,7 @@ async fn process_record_blobs(
Ok((user_id, did, blob_refs_found))
}
pub async fn backfill_record_blobs(
repo_repo: Arc<dyn RepoRepository>,
block_store: PostgresBlockStore,
) {
pub async fn backfill_record_blobs(repo_repo: Arc<dyn RepoRepository>, block_store: AnyBlockStore) {
let users_needing_backfill = match repo_repo.get_users_needing_record_blobs_backfill(100).await
{
Ok(rows) => rows,
@@ -437,7 +428,7 @@ pub async fn start_scheduled_tasks(
blob_store: Arc<dyn BlobStorage>,
sso_repo: Arc<dyn SsoRepository>,
repo_repo: Arc<dyn RepoRepository>,
block_store: PostgresBlockStore,
block_store: AnyBlockStore,
shutdown: CancellationToken,
) {
let cfg = tranquil_config::get();
@@ -502,7 +493,9 @@ pub async fn start_scheduled_tasks(
}
}
_ = gc_ticker.tick() => {
if let Err(e) = run_block_gc(repo_repo.as_ref(), &block_store).await {
if let Some(pg) = block_store.as_postgres()
&& let Err(e) = run_block_gc(repo_repo.as_ref(), pg).await
{
error!("Block GC error: {e}");
}
}
@@ -514,7 +507,7 @@ const BLOCK_GC_BATCH_SIZE: i64 = 1000;
async fn run_block_gc(
repo_repo: &dyn RepoRepository,
block_store: &PostgresBlockStore,
block_store: &crate::repo::PostgresBlockStore,
) -> anyhow::Result<()> {
let mut total_deleted: u64 = 0;
@@ -632,11 +625,9 @@ async fn delete_account_data(
}
pub async fn generate_repo_car(
block_store: &PostgresBlockStore,
block_store: &AnyBlockStore,
head_cid: &Cid,
) -> anyhow::Result<Vec<u8>> {
use jacquard_repo::storage::BlockStore;
let block_cids_bytes = collect_current_repo_blocks(block_store, head_cid).await?;
let block_cids: Vec<Cid> = block_cids_bytes
.iter()
@@ -686,7 +677,7 @@ fn encode_car_block(cid: &Cid, block: &[u8]) -> Vec<u8> {
pub async fn generate_repo_car_from_user_blocks(
repo_repo: &dyn tranquil_db_traits::RepoRepository,
block_store: &PostgresBlockStore,
block_store: &AnyBlockStore,
user_id: uuid::Uuid,
_head_cid: &Cid,
) -> anyhow::Result<Vec<u8>> {

View File

@@ -33,7 +33,7 @@ pub fn init_rate_limit_override() {
#[derive(Clone)]
pub struct AppState {
pub repos: Arc<PostgresRepositories>,
pub block_store: PostgresBlockStore,
pub block_store: crate::repo::AnyBlockStore,
pub blob_store: Arc<dyn BlobStorage>,
pub firehose_tx: broadcast::Sender<SequencedEvent>,
pub rate_limiters: Arc<RateLimiters>,
@@ -266,12 +266,16 @@ impl AppState {
let mut repos = PostgresRepositories::new(db.clone());
let cfg = tranquil_config::get();
if cfg.storage.repo_backend() == tranquil_config::RepoBackend::TranquilStore {
wire_tranquil_store(&mut repos, &cfg.tranquil_store);
}
let block_store =
match cfg.storage.repo_backend() == tranquil_config::RepoBackend::TranquilStore {
true => {
let bs = wire_tranquil_store(&mut repos, &cfg.tranquil_store, shutdown.clone());
crate::repo::AnyBlockStore::TranquilStore(bs)
}
false => crate::repo::AnyBlockStore::Postgres(PostgresBlockStore::new(db)),
};
let repos = Arc::new(repos);
let block_store = PostgresBlockStore::new(db);
let blob_store = create_blob_storage().await;
let firehose_buffer_size = tranquil_config::get().firehose.buffer_size;
@@ -389,8 +393,10 @@ impl AppState {
fn wire_tranquil_store(
repos: &mut PostgresRepositories,
store_cfg: &tranquil_config::TranquilStoreConfig,
) {
shutdown: CancellationToken,
) -> tranquil_store::blockstore::TranquilBlockStore {
use tranquil_store::RealIO;
use tranquil_store::blockstore::{BlockStoreConfig, TranquilBlockStore};
use tranquil_store::eventlog::{EventLog, EventLogBridge, EventLogConfig};
use tranquil_store::metastore::client::MetastoreClient;
use tranquil_store::metastore::handler::HandlerPool;
@@ -404,9 +410,15 @@ fn wire_tranquil_store(
};
let metastore_dir = data_dir.join("metastore");
let segments_dir = data_dir.join("eventlog").join("segments");
let blockstore_data_dir = data_dir.join("blockstore").join("data");
let blockstore_index_dir = data_dir.join("blockstore").join("index");
std::fs::create_dir_all(&metastore_dir).expect("failed to create metastore directory");
std::fs::create_dir_all(&segments_dir).expect("failed to create eventlog segments directory");
std::fs::create_dir_all(&blockstore_data_dir)
.expect("failed to create blockstore data directory");
std::fs::create_dir_all(&blockstore_index_dir)
.expect("failed to create blockstore index directory");
let metastore_config = store_cfg
.memory_budget_mb
@@ -418,6 +430,14 @@ fn wire_tranquil_store(
let metastore =
Metastore::open(&metastore_dir, metastore_config).expect("failed to open metastore");
let blockstore = TranquilBlockStore::open(BlockStoreConfig {
data_dir: blockstore_data_dir,
index_dir: blockstore_index_dir,
max_file_size: tranquil_store::blockstore::DEFAULT_MAX_FILE_SIZE,
group_commit: Default::default(),
})
.expect("failed to open blockstore");
let event_log = EventLog::open(
EventLogConfig {
segments_dir,
@@ -441,13 +461,35 @@ fn wire_tranquil_store(
let notifier = bridge.notifier();
let pool = HandlerPool::spawn::<RealIO>(metastore, bridge, None, store_cfg.handler_threads);
let pool = Arc::new(HandlerPool::spawn::<RealIO>(
metastore,
bridge,
Some(blockstore.clone()),
store_cfg.handler_threads,
));
let client = MetastoreClient::<RealIO>::new(Arc::new(pool));
tokio::spawn({
let pool = Arc::clone(&pool);
async move {
shutdown.cancelled().await;
pool.close().await;
}
});
let client = MetastoreClient::<RealIO>::new(pool);
tracing::info!(data_dir = %store_cfg.data_dir, "tranquil-store data directory");
repos.repo = Arc::new(client.clone());
repos.backlink = Arc::new(client);
repos.backlink = Arc::new(client.clone());
repos.blob = Arc::new(client.clone());
repos.user = Arc::new(client.clone());
repos.session = Arc::new(client.clone());
repos.oauth = Arc::new(client.clone());
repos.infra = Arc::new(client.clone());
repos.delegation = Arc::new(client.clone());
repos.sso = Arc::new(client);
repos.event_notifier = Arc::new(notifier);
blockstore
}

View File

@@ -150,14 +150,14 @@ impl BlockStore for PostgresBlockStore {
}
#[derive(Clone)]
pub struct TrackingBlockStore {
inner: PostgresBlockStore,
pub struct TrackingBlockStore<S: BlockStore> {
inner: S,
written_cids: Arc<Mutex<Vec<Cid>>>,
read_cids: Arc<Mutex<HashSet<Cid>>>,
}
impl TrackingBlockStore {
pub fn new(store: PostgresBlockStore) -> Self {
impl<S: BlockStore + Sync> TrackingBlockStore<S> {
pub fn new(store: S) -> Self {
Self {
inner: store,
written_cids: Arc::new(Mutex::new(Vec::new())),
@@ -188,7 +188,7 @@ impl TrackingBlockStore {
}
}
impl BlockStore for TrackingBlockStore {
impl<S: BlockStore + Sync> BlockStore for TrackingBlockStore<S> {
async fn get(&self, cid: &Cid) -> Result<Option<Bytes>, RepoError> {
let result = self.inner.get(cid).await?;
if result.is_some() {

View File

@@ -22,6 +22,7 @@ chrono = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tranquil-db-traits = { workspace = true }
tranquil-oauth = { workspace = true }
tranquil-types = { workspace = true }
jacquard-repo = { workspace = true }
cid = { workspace = true }

File diff suppressed because it is too large Load Diff

View File

@@ -367,7 +367,7 @@ impl<S: StorageIO> CommitOps<S> {
&self,
limit: i64,
) -> Result<Vec<UserNeedingRecordBlobsBackfill>, MetastoreError> {
let limit_usize = usize::try_from(limit).unwrap_or(usize::MAX);
let limit_usize = usize::try_from(limit).unwrap_or(0);
self.scan_users_missing_prefix(
record_blobs_user_prefix,

View File

@@ -0,0 +1,412 @@
use std::sync::Arc;
use chrono::{DateTime, Utc};
use fjall::{Database, Keyspace};
use uuid::Uuid;
use super::MetastoreError;
use super::delegations::{
AuditLogValue, DelegationGrantValue, action_type_to_u8, audit_log_key, audit_log_prefix,
by_controller_key, by_controller_prefix, grant_key, grant_prefix, u8_to_action_type,
};
use super::keys::UserHash;
use super::scan::{count_prefix, point_lookup};
use super::user_hash::UserHashMap;
use tranquil_db_traits::DbScope;
use tranquil_db_traits::{
AuditLogEntry, ControllerInfo, DelegatedAccountInfo, DelegationActionType, DelegationGrant,
};
use tranquil_types::{Did, Handle};
pub struct DelegationOps {
db: Database,
indexes: Keyspace,
users: Keyspace,
user_hashes: Arc<UserHashMap>,
}
impl DelegationOps {
pub fn new(
db: Database,
indexes: Keyspace,
users: Keyspace,
user_hashes: Arc<UserHashMap>,
) -> Self {
Self {
db,
indexes,
users,
user_hashes,
}
}
fn resolve_handle_for_did(&self, did_str: &str) -> Option<Handle> {
let user_hash = UserHash::from_did(did_str);
let key = super::encoding::KeyBuilder::new()
.tag(super::keys::KeyTag::USER_PRIMARY)
.u64(user_hash.raw())
.build();
self.users
.get(key.as_slice())
.ok()
.flatten()
.and_then(|raw| super::users::UserValue::deserialize(&raw))
.and_then(|u| Handle::new(u.handle).ok())
}
fn value_to_grant(&self, v: &DelegationGrantValue) -> Result<DelegationGrant, MetastoreError> {
Ok(DelegationGrant {
id: v.id,
delegated_did: Did::new(v.delegated_did.clone())
.map_err(|_| MetastoreError::CorruptData("invalid delegated_did"))?,
controller_did: Did::new(v.controller_did.clone())
.map_err(|_| MetastoreError::CorruptData("invalid controller_did"))?,
granted_scopes: DbScope::new(&v.granted_scopes).unwrap_or_else(|_| DbScope::empty()),
granted_at: DateTime::from_timestamp_millis(v.granted_at_ms).unwrap_or_default(),
granted_by: Did::new(v.granted_by.clone())
.map_err(|_| MetastoreError::CorruptData("invalid granted_by"))?,
revoked_at: v.revoked_at_ms.and_then(DateTime::from_timestamp_millis),
revoked_by: v.revoked_by.as_ref().and_then(|d| Did::new(d.clone()).ok()),
})
}
pub fn is_delegated_account(&self, did: &Did) -> Result<bool, MetastoreError> {
let delegated_hash = UserHash::from_did(did.as_str());
let prefix = grant_prefix(delegated_hash);
let found = self
.indexes
.prefix(prefix.as_slice())
.map(|guard| {
let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?;
DelegationGrantValue::deserialize(&val_bytes)
.ok_or(MetastoreError::CorruptData("corrupt delegation grant"))
.map(|val| val.revoked_at_ms.is_none())
})
.find(|result| !matches!(result, Ok(false)));
match found {
Some(Ok(true)) => Ok(true),
Some(Err(e)) => Err(e),
_ => Ok(false),
}
}
pub fn create_delegation(
&self,
delegated_did: &Did,
controller_did: &Did,
granted_scopes: &DbScope,
granted_by: &Did,
) -> Result<Uuid, MetastoreError> {
let delegated_hash = UserHash::from_did(delegated_did.as_str());
let controller_hash = UserHash::from_did(controller_did.as_str());
let id = Uuid::new_v4();
let now_ms = Utc::now().timestamp_millis();
let value = DelegationGrantValue {
id,
delegated_did: delegated_did.to_string(),
controller_did: controller_did.to_string(),
granted_scopes: granted_scopes.as_str().to_owned(),
granted_at_ms: now_ms,
granted_by: granted_by.to_string(),
revoked_at_ms: None,
revoked_by: None,
};
let primary = grant_key(delegated_hash, controller_hash);
let reverse = by_controller_key(controller_hash, delegated_hash);
let mut batch = self.db.batch();
batch.insert(&self.indexes, primary.as_slice(), value.serialize());
batch.insert(&self.indexes, reverse.as_slice(), []);
batch.commit().map_err(MetastoreError::Fjall)?;
Ok(id)
}
pub fn revoke_delegation(
&self,
delegated_did: &Did,
controller_did: &Did,
revoked_by: &Did,
) -> Result<bool, MetastoreError> {
let delegated_hash = UserHash::from_did(delegated_did.as_str());
let controller_hash = UserHash::from_did(controller_did.as_str());
let primary = grant_key(delegated_hash, controller_hash);
let existing: Option<DelegationGrantValue> = point_lookup(
&self.indexes,
primary.as_slice(),
DelegationGrantValue::deserialize,
"corrupt delegation grant",
)?;
match existing {
Some(mut val) if val.revoked_at_ms.is_none() => {
val.revoked_at_ms = Some(Utc::now().timestamp_millis());
val.revoked_by = Some(revoked_by.to_string());
let reverse = by_controller_key(controller_hash, delegated_hash);
let mut batch = self.db.batch();
batch.insert(&self.indexes, primary.as_slice(), val.serialize());
batch.remove(&self.indexes, reverse.as_slice());
batch.commit().map_err(MetastoreError::Fjall)?;
Ok(true)
}
_ => Ok(false),
}
}
pub fn update_delegation_scopes(
&self,
delegated_did: &Did,
controller_did: &Did,
new_scopes: &DbScope,
) -> Result<bool, MetastoreError> {
let delegated_hash = UserHash::from_did(delegated_did.as_str());
let controller_hash = UserHash::from_did(controller_did.as_str());
let primary = grant_key(delegated_hash, controller_hash);
let existing: Option<DelegationGrantValue> = point_lookup(
&self.indexes,
primary.as_slice(),
DelegationGrantValue::deserialize,
"corrupt delegation grant",
)?;
match existing {
Some(mut val) if val.revoked_at_ms.is_none() => {
val.granted_scopes = new_scopes.as_str().to_owned();
self.indexes
.insert(primary.as_slice(), val.serialize())
.map_err(MetastoreError::Fjall)?;
Ok(true)
}
_ => Ok(false),
}
}
pub fn get_delegation(
&self,
delegated_did: &Did,
controller_did: &Did,
) -> Result<Option<DelegationGrant>, MetastoreError> {
let delegated_hash = UserHash::from_did(delegated_did.as_str());
let controller_hash = UserHash::from_did(controller_did.as_str());
let primary = grant_key(delegated_hash, controller_hash);
let val: Option<DelegationGrantValue> = point_lookup(
&self.indexes,
primary.as_slice(),
DelegationGrantValue::deserialize,
"corrupt delegation grant",
)?;
val.map(|v| self.value_to_grant(&v)).transpose()
}
pub fn get_delegations_for_account(
&self,
delegated_did: &Did,
) -> Result<Vec<ControllerInfo>, MetastoreError> {
let delegated_hash = UserHash::from_did(delegated_did.as_str());
let prefix = grant_prefix(delegated_hash);
self.indexes
.prefix(prefix.as_slice())
.try_fold(Vec::new(), |mut acc, guard| {
let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?;
let val = DelegationGrantValue::deserialize(&val_bytes)
.ok_or(MetastoreError::CorruptData("corrupt delegation grant"))?;
let is_active = val.revoked_at_ms.is_none();
let controller_did_parsed = Did::new(val.controller_did.clone())
.map_err(|_| MetastoreError::CorruptData("invalid controller_did"))?;
let handle = self.resolve_handle_for_did(&val.controller_did);
let is_local = self
.user_hashes
.get_uuid(&UserHash::from_did(&val.controller_did))
.is_some();
acc.push(ControllerInfo {
did: controller_did_parsed,
handle,
granted_scopes: DbScope::new(&val.granted_scopes)
.unwrap_or_else(|_| DbScope::empty()),
granted_at: DateTime::from_timestamp_millis(val.granted_at_ms)
.unwrap_or_default(),
is_active,
is_local,
});
Ok::<_, MetastoreError>(acc)
})
}
pub fn get_accounts_controlled_by(
&self,
controller_did: &Did,
) -> Result<Vec<DelegatedAccountInfo>, MetastoreError> {
let controller_hash = UserHash::from_did(controller_did.as_str());
let prefix = by_controller_prefix(controller_hash);
self.indexes
.prefix(prefix.as_slice())
.try_fold(Vec::new(), |mut acc, guard| {
let (key_bytes, _) = guard.into_inner().map_err(MetastoreError::Fjall)?;
let mut reader = super::encoding::KeyReader::new(&key_bytes);
let _tag = reader.tag();
let _ctrl_hash = reader.u64();
let deleg_hash_raw = reader
.u64()
.ok_or(MetastoreError::CorruptData("corrupt by_controller key"))?;
let deleg_hash = UserHash::from_raw(deleg_hash_raw);
let grant_pfx = grant_key(deleg_hash, controller_hash);
let grant_val: Option<DelegationGrantValue> = point_lookup(
&self.indexes,
grant_pfx.as_slice(),
DelegationGrantValue::deserialize,
"corrupt delegation grant",
)?;
if let Some(val) = grant_val.filter(|v| v.revoked_at_ms.is_none()) {
let delegated_did = Did::new(val.delegated_did.clone())
.map_err(|_| MetastoreError::CorruptData("invalid delegated_did"))?;
let handle = self
.resolve_handle_for_did(&val.delegated_did)
.unwrap_or_else(|| Handle::new("unknown.invalid").unwrap());
acc.push(DelegatedAccountInfo {
did: delegated_did,
handle,
granted_scopes: DbScope::new(&val.granted_scopes)
.unwrap_or_else(|_| DbScope::empty()),
granted_at: DateTime::from_timestamp_millis(val.granted_at_ms)
.unwrap_or_default(),
});
}
Ok::<_, MetastoreError>(acc)
})
}
pub fn count_active_controllers(&self, delegated_did: &Did) -> Result<i64, MetastoreError> {
let delegated_hash = UserHash::from_did(delegated_did.as_str());
let prefix = grant_prefix(delegated_hash);
self.indexes
.prefix(prefix.as_slice())
.try_fold(0i64, |acc, guard| {
let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?;
let val = DelegationGrantValue::deserialize(&val_bytes)
.ok_or(MetastoreError::CorruptData("corrupt delegation grant"))?;
Ok::<_, MetastoreError>(match val.revoked_at_ms.is_none() {
true => acc.saturating_add(1),
false => acc,
})
})
}
pub fn controls_any_accounts(&self, did: &Did) -> Result<bool, MetastoreError> {
let controller_hash = UserHash::from_did(did.as_str());
let prefix = by_controller_prefix(controller_hash);
match self.indexes.prefix(prefix.as_slice()).next() {
Some(guard) => {
guard.into_inner().map_err(MetastoreError::Fjall)?;
Ok(true)
}
None => Ok(false),
}
}
#[allow(clippy::too_many_arguments)]
pub fn log_delegation_action(
&self,
delegated_did: &Did,
actor_did: &Did,
controller_did: Option<&Did>,
action_type: DelegationActionType,
action_details: Option<serde_json::Value>,
ip_address: Option<&str>,
user_agent: Option<&str>,
) -> Result<Uuid, MetastoreError> {
let delegated_hash = UserHash::from_did(delegated_did.as_str());
let id = Uuid::new_v4();
let now_ms = Utc::now().timestamp_millis();
let value = AuditLogValue {
id,
delegated_did: delegated_did.to_string(),
actor_did: actor_did.to_string(),
controller_did: controller_did.map(|d| d.to_string()),
action_type: action_type_to_u8(action_type),
action_details: action_details.map(|v| serde_json::to_vec(&v).unwrap_or_default()),
ip_address: ip_address.map(str::to_owned),
user_agent: user_agent.map(str::to_owned),
created_at_ms: now_ms,
};
let key = audit_log_key(delegated_hash, now_ms, id);
self.indexes
.insert(key.as_slice(), value.serialize())
.map_err(MetastoreError::Fjall)?;
Ok(id)
}
pub fn get_audit_log_for_account(
&self,
delegated_did: &Did,
limit: i64,
offset: i64,
) -> Result<Vec<AuditLogEntry>, MetastoreError> {
let delegated_hash = UserHash::from_did(delegated_did.as_str());
let prefix = audit_log_prefix(delegated_hash);
let limit = usize::try_from(limit).unwrap_or(0);
let offset = usize::try_from(offset).unwrap_or(0);
self.indexes
.prefix(prefix.as_slice())
.skip(offset)
.take(limit)
.try_fold(Vec::new(), |mut acc, guard| {
let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?;
let val = AuditLogValue::deserialize(&val_bytes)
.ok_or(MetastoreError::CorruptData("corrupt audit log entry"))?;
acc.push(self.value_to_audit_entry(&val)?);
Ok::<_, MetastoreError>(acc)
})
}
pub fn count_audit_log_entries(&self, delegated_did: &Did) -> Result<i64, MetastoreError> {
let delegated_hash = UserHash::from_did(delegated_did.as_str());
let prefix = audit_log_prefix(delegated_hash);
count_prefix(&self.indexes, prefix.as_slice())
}
fn value_to_audit_entry(&self, v: &AuditLogValue) -> Result<AuditLogEntry, MetastoreError> {
Ok(AuditLogEntry {
id: v.id,
delegated_did: Did::new(v.delegated_did.clone())
.map_err(|_| MetastoreError::CorruptData("invalid delegated_did"))?,
actor_did: Did::new(v.actor_did.clone())
.map_err(|_| MetastoreError::CorruptData("invalid actor_did"))?,
controller_did: v
.controller_did
.as_ref()
.map(|d| Did::new(d.clone()))
.transpose()
.map_err(|_| MetastoreError::CorruptData("invalid controller_did"))?,
action_type: u8_to_action_type(v.action_type).ok_or(MetastoreError::CorruptData(
"unknown delegation action type",
))?,
action_details: v
.action_details
.as_ref()
.and_then(|b| serde_json::from_slice(b).ok()),
ip_address: v.ip_address.clone(),
user_agent: v.user_agent.clone(),
created_at: DateTime::from_timestamp_millis(v.created_at_ms).unwrap_or_default(),
})
}
}

View File

@@ -0,0 +1,201 @@
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use super::encoding::KeyBuilder;
use super::keys::{KeyTag, UserHash};
const GRANT_SCHEMA_VERSION: u8 = 1;
const AUDIT_LOG_SCHEMA_VERSION: u8 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DelegationGrantValue {
pub id: uuid::Uuid,
pub delegated_did: String,
pub controller_did: String,
pub granted_scopes: String,
pub granted_at_ms: i64,
pub granted_by: String,
pub revoked_at_ms: Option<i64>,
pub revoked_by: Option<String>,
}
impl DelegationGrantValue {
pub fn serialize(&self) -> Vec<u8> {
let payload =
postcard::to_allocvec(self).expect("DelegationGrantValue serialization cannot fail");
let mut buf = Vec::with_capacity(1 + payload.len());
buf.push(GRANT_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let (&version, payload) = bytes.split_first()?;
match version {
GRANT_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuditLogValue {
pub id: uuid::Uuid,
pub delegated_did: String,
pub actor_did: String,
pub controller_did: Option<String>,
pub action_type: u8,
pub action_details: Option<Vec<u8>>,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
pub created_at_ms: i64,
}
impl AuditLogValue {
pub fn serialize(&self) -> Vec<u8> {
let payload = postcard::to_allocvec(self).expect("AuditLogValue serialization cannot fail");
let mut buf = Vec::with_capacity(1 + payload.len());
buf.push(AUDIT_LOG_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let (&version, payload) = bytes.split_first()?;
match version {
AUDIT_LOG_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
pub fn action_type_to_u8(t: tranquil_db_traits::DelegationActionType) -> u8 {
match t {
tranquil_db_traits::DelegationActionType::GrantCreated => 0,
tranquil_db_traits::DelegationActionType::GrantRevoked => 1,
tranquil_db_traits::DelegationActionType::ScopesModified => 2,
tranquil_db_traits::DelegationActionType::TokenIssued => 3,
tranquil_db_traits::DelegationActionType::RepoWrite => 4,
tranquil_db_traits::DelegationActionType::BlobUpload => 5,
tranquil_db_traits::DelegationActionType::AccountAction => 6,
}
}
pub fn u8_to_action_type(v: u8) -> Option<tranquil_db_traits::DelegationActionType> {
match v {
0 => Some(tranquil_db_traits::DelegationActionType::GrantCreated),
1 => Some(tranquil_db_traits::DelegationActionType::GrantRevoked),
2 => Some(tranquil_db_traits::DelegationActionType::ScopesModified),
3 => Some(tranquil_db_traits::DelegationActionType::TokenIssued),
4 => Some(tranquil_db_traits::DelegationActionType::RepoWrite),
5 => Some(tranquil_db_traits::DelegationActionType::BlobUpload),
6 => Some(tranquil_db_traits::DelegationActionType::AccountAction),
_ => None,
}
}
pub fn grant_key(delegated_hash: UserHash, controller_hash: UserHash) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::DELEG_GRANT)
.u64(delegated_hash.raw())
.u64(controller_hash.raw())
.build()
}
pub fn grant_prefix(delegated_hash: UserHash) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::DELEG_GRANT)
.u64(delegated_hash.raw())
.build()
}
pub fn by_controller_key(
controller_hash: UserHash,
delegated_hash: UserHash,
) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::DELEG_BY_CONTROLLER)
.u64(controller_hash.raw())
.u64(delegated_hash.raw())
.build()
}
pub fn by_controller_prefix(controller_hash: UserHash) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::DELEG_BY_CONTROLLER)
.u64(controller_hash.raw())
.build()
}
pub fn audit_log_key(
delegated_hash: UserHash,
created_at_ms: i64,
id: uuid::Uuid,
) -> SmallVec<[u8; 128]> {
let reversed_ts = i64::MAX.saturating_sub(created_at_ms);
KeyBuilder::new()
.tag(KeyTag::DELEG_AUDIT_LOG)
.u64(delegated_hash.raw())
.i64(reversed_ts)
.bytes(id.as_bytes())
.build()
}
pub fn audit_log_prefix(delegated_hash: UserHash) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::DELEG_AUDIT_LOG)
.u64(delegated_hash.raw())
.build()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grant_value_roundtrip() {
let val = DelegationGrantValue {
id: uuid::Uuid::new_v4(),
delegated_did: "did:plc:deleg".to_owned(),
controller_did: "did:plc:ctrl".to_owned(),
granted_scopes: "atproto".to_owned(),
granted_at_ms: 1700000000000,
granted_by: "did:plc:grantor".to_owned(),
revoked_at_ms: None,
revoked_by: None,
};
let bytes = val.serialize();
assert_eq!(bytes[0], GRANT_SCHEMA_VERSION);
let decoded = DelegationGrantValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn audit_log_value_roundtrip() {
let val = AuditLogValue {
id: uuid::Uuid::new_v4(),
delegated_did: "did:plc:deleg".to_owned(),
actor_did: "did:plc:actor".to_owned(),
controller_did: Some("did:plc:ctrl".to_owned()),
action_type: 0,
action_details: None,
ip_address: Some("127.0.0.1".to_owned()),
user_agent: None,
created_at_ms: 1700000000000,
};
let bytes = val.serialize();
assert_eq!(bytes[0], AUDIT_LOG_SCHEMA_VERSION);
let decoded = AuditLogValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn grant_key_ordering_by_controller() {
let deleg = UserHash::from_did("did:plc:deleg");
let ctrl_a = UserHash::from_raw(100);
let ctrl_b = UserHash::from_raw(200);
let key_a = grant_key(deleg, ctrl_a);
let key_b = grant_key(deleg, ctrl_b);
assert!(key_a.as_slice() < key_b.as_slice());
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,730 @@
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use super::encoding::KeyBuilder;
use super::keys::KeyTag;
const COMMS_SCHEMA_VERSION: u8 = 1;
const INVITE_CODE_SCHEMA_VERSION: u8 = 1;
const INVITE_USE_SCHEMA_VERSION: u8 = 1;
const SIGNING_KEY_SCHEMA_VERSION: u8 = 1;
const DELETION_REQUEST_SCHEMA_VERSION: u8 = 1;
const REPORT_SCHEMA_VERSION: u8 = 1;
const NOTIFICATION_HISTORY_SCHEMA_VERSION: u8 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct QueuedCommsValue {
pub id: uuid::Uuid,
pub user_id: Option<uuid::Uuid>,
pub channel: u8,
pub comms_type: u8,
pub recipient: String,
pub subject: Option<String>,
pub body: String,
pub metadata: Option<Vec<u8>>,
pub status: u8,
pub error_message: Option<String>,
pub attempts: i32,
pub max_attempts: i32,
pub created_at_ms: i64,
pub scheduled_for_ms: i64,
pub sent_at_ms: Option<i64>,
}
impl QueuedCommsValue {
pub fn serialize(&self) -> Vec<u8> {
let payload =
postcard::to_allocvec(self).expect("QueuedCommsValue serialization cannot fail");
let mut buf = Vec::with_capacity(1 + payload.len());
buf.push(COMMS_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let (&version, payload) = bytes.split_first()?;
match version {
COMMS_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InviteCodeValue {
pub code: String,
pub available_uses: i32,
pub disabled: bool,
pub for_account: Option<String>,
pub created_by: Option<uuid::Uuid>,
pub created_at_ms: i64,
}
impl InviteCodeValue {
pub fn serialize(&self) -> Vec<u8> {
let payload =
postcard::to_allocvec(self).expect("InviteCodeValue serialization cannot fail");
let mut buf = Vec::with_capacity(1 + payload.len());
buf.push(INVITE_CODE_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let (&version, payload) = bytes.split_first()?;
match version {
INVITE_CODE_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InviteCodeUseValue {
pub used_by: uuid::Uuid,
pub used_at_ms: i64,
}
impl InviteCodeUseValue {
pub fn serialize(&self) -> Vec<u8> {
let payload =
postcard::to_allocvec(self).expect("InviteCodeUseValue serialization cannot fail");
let mut buf = Vec::with_capacity(1 + payload.len());
buf.push(INVITE_USE_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let (&version, payload) = bytes.split_first()?;
match version {
INVITE_USE_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SigningKeyValue {
pub id: uuid::Uuid,
pub did: Option<String>,
pub public_key_did_key: String,
pub private_key_bytes: Vec<u8>,
pub used: bool,
pub created_at_ms: i64,
pub expires_at_ms: i64,
}
impl SigningKeyValue {
pub fn serialize(&self) -> Vec<u8> {
let payload =
postcard::to_allocvec(self).expect("SigningKeyValue serialization cannot fail");
let mut buf = Vec::with_capacity(1 + payload.len());
buf.push(SIGNING_KEY_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let (&version, payload) = bytes.split_first()?;
match version {
SIGNING_KEY_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeletionRequestValue {
pub token: String,
pub did: String,
pub created_at_ms: i64,
pub expires_at_ms: i64,
}
impl DeletionRequestValue {
pub fn serialize(&self) -> Vec<u8> {
let payload =
postcard::to_allocvec(self).expect("DeletionRequestValue serialization cannot fail");
let mut buf = Vec::with_capacity(1 + payload.len());
buf.push(DELETION_REQUEST_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let (&version, payload) = bytes.split_first()?;
match version {
DELETION_REQUEST_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReportValue {
pub id: i64,
pub reason_type: String,
pub reason: Option<String>,
pub subject_json: Vec<u8>,
pub reported_by_did: String,
pub created_at_ms: i64,
}
impl ReportValue {
pub fn serialize(&self) -> Vec<u8> {
let payload = postcard::to_allocvec(self).expect("ReportValue serialization cannot fail");
let mut buf = Vec::with_capacity(1 + payload.len());
buf.push(REPORT_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let (&version, payload) = bytes.split_first()?;
match version {
REPORT_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NotificationHistoryValue {
pub id: uuid::Uuid,
pub channel: u8,
pub comms_type: u8,
pub recipient: String,
pub subject: Option<String>,
pub body: String,
pub status: u8,
pub created_at_ms: i64,
}
impl NotificationHistoryValue {
pub fn serialize(&self) -> Vec<u8> {
let payload = postcard::to_allocvec(self)
.expect("NotificationHistoryValue serialization cannot fail");
let mut buf = Vec::with_capacity(1 + payload.len());
buf.push(NOTIFICATION_HISTORY_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let (&version, payload) = bytes.split_first()?;
match version {
NOTIFICATION_HISTORY_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
pub fn channel_to_u8(ch: tranquil_db_traits::CommsChannel) -> u8 {
match ch {
tranquil_db_traits::CommsChannel::Email => 0,
tranquil_db_traits::CommsChannel::Discord => 1,
tranquil_db_traits::CommsChannel::Telegram => 2,
tranquil_db_traits::CommsChannel::Signal => 3,
}
}
pub fn u8_to_channel(v: u8) -> Option<tranquil_db_traits::CommsChannel> {
match v {
0 => Some(tranquil_db_traits::CommsChannel::Email),
1 => Some(tranquil_db_traits::CommsChannel::Discord),
2 => Some(tranquil_db_traits::CommsChannel::Telegram),
3 => Some(tranquil_db_traits::CommsChannel::Signal),
_ => None,
}
}
pub fn comms_type_to_u8(ct: tranquil_db_traits::CommsType) -> u8 {
match ct {
tranquil_db_traits::CommsType::Welcome => 0,
tranquil_db_traits::CommsType::EmailVerification => 1,
tranquil_db_traits::CommsType::PasswordReset => 2,
tranquil_db_traits::CommsType::EmailUpdate => 3,
tranquil_db_traits::CommsType::AccountDeletion => 4,
tranquil_db_traits::CommsType::AdminEmail => 5,
tranquil_db_traits::CommsType::PlcOperation => 6,
tranquil_db_traits::CommsType::TwoFactorCode => 7,
tranquil_db_traits::CommsType::PasskeyRecovery => 8,
tranquil_db_traits::CommsType::LegacyLoginAlert => 9,
tranquil_db_traits::CommsType::MigrationVerification => 10,
tranquil_db_traits::CommsType::ChannelVerification => 11,
tranquil_db_traits::CommsType::ChannelVerified => 12,
}
}
pub fn u8_to_comms_type(v: u8) -> Option<tranquil_db_traits::CommsType> {
match v {
0 => Some(tranquil_db_traits::CommsType::Welcome),
1 => Some(tranquil_db_traits::CommsType::EmailVerification),
2 => Some(tranquil_db_traits::CommsType::PasswordReset),
3 => Some(tranquil_db_traits::CommsType::EmailUpdate),
4 => Some(tranquil_db_traits::CommsType::AccountDeletion),
5 => Some(tranquil_db_traits::CommsType::AdminEmail),
6 => Some(tranquil_db_traits::CommsType::PlcOperation),
7 => Some(tranquil_db_traits::CommsType::TwoFactorCode),
8 => Some(tranquil_db_traits::CommsType::PasskeyRecovery),
9 => Some(tranquil_db_traits::CommsType::LegacyLoginAlert),
10 => Some(tranquil_db_traits::CommsType::MigrationVerification),
11 => Some(tranquil_db_traits::CommsType::ChannelVerification),
12 => Some(tranquil_db_traits::CommsType::ChannelVerified),
_ => None,
}
}
pub fn status_to_u8(s: tranquil_db_traits::CommsStatus) -> u8 {
match s {
tranquil_db_traits::CommsStatus::Pending => 0,
tranquil_db_traits::CommsStatus::Processing => 1,
tranquil_db_traits::CommsStatus::Sent => 2,
tranquil_db_traits::CommsStatus::Failed => 3,
}
}
pub fn u8_to_status(v: u8) -> Option<tranquil_db_traits::CommsStatus> {
match v {
0 => Some(tranquil_db_traits::CommsStatus::Pending),
1 => Some(tranquil_db_traits::CommsStatus::Processing),
2 => Some(tranquil_db_traits::CommsStatus::Sent),
3 => Some(tranquil_db_traits::CommsStatus::Failed),
_ => None,
}
}
pub fn comms_queue_key(id: uuid::Uuid) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::INFRA_COMMS_QUEUE)
.bytes(id.as_bytes())
.build()
}
pub fn comms_queue_prefix() -> SmallVec<[u8; 128]> {
KeyBuilder::new().tag(KeyTag::INFRA_COMMS_QUEUE).build()
}
pub fn invite_code_key(code: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::INFRA_INVITE_CODE)
.string(code)
.build()
}
pub fn invite_code_prefix() -> SmallVec<[u8; 128]> {
KeyBuilder::new().tag(KeyTag::INFRA_INVITE_CODE).build()
}
pub fn invite_use_key(code: &str, used_by: uuid::Uuid) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::INFRA_INVITE_USE)
.string(code)
.bytes(used_by.as_bytes())
.build()
}
pub fn invite_use_prefix(code: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::INFRA_INVITE_USE)
.string(code)
.build()
}
pub fn invite_by_account_key(did: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::INFRA_INVITE_BY_ACCOUNT)
.string(did)
.build()
}
pub fn invite_by_user_key(user_id: uuid::Uuid) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::INFRA_INVITE_BY_USER)
.bytes(user_id.as_bytes())
.build()
}
pub fn invite_by_user_prefix() -> SmallVec<[u8; 128]> {
KeyBuilder::new().tag(KeyTag::INFRA_INVITE_BY_USER).build()
}
pub fn signing_key_key(public_key_did_key: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::INFRA_SIGNING_KEY)
.string(public_key_did_key)
.build()
}
pub fn signing_key_by_id_key(key_id: uuid::Uuid) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::INFRA_SIGNING_KEY_BY_ID)
.bytes(key_id.as_bytes())
.build()
}
pub fn deletion_request_key(token: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::INFRA_DELETION_REQUEST)
.string(token)
.build()
}
pub fn deletion_by_did_key(did: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::INFRA_DELETION_BY_DID)
.string(did)
.build()
}
pub fn deletion_by_did_prefix() -> SmallVec<[u8; 128]> {
KeyBuilder::new().tag(KeyTag::INFRA_DELETION_BY_DID).build()
}
pub fn account_pref_key(user_id: uuid::Uuid, name: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::INFRA_ACCOUNT_PREF)
.bytes(user_id.as_bytes())
.string(name)
.build()
}
pub fn account_pref_prefix(user_id: uuid::Uuid) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::INFRA_ACCOUNT_PREF)
.bytes(user_id.as_bytes())
.build()
}
pub fn server_config_key(key: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::INFRA_SERVER_CONFIG)
.string(key)
.build()
}
pub fn report_key(id: i64) -> SmallVec<[u8; 128]> {
KeyBuilder::new().tag(KeyTag::INFRA_REPORT).i64(id).build()
}
pub fn plc_token_key(user_id: uuid::Uuid, token: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::INFRA_PLC_TOKEN)
.bytes(user_id.as_bytes())
.string(token)
.build()
}
pub fn plc_token_prefix(user_id: uuid::Uuid) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::INFRA_PLC_TOKEN)
.bytes(user_id.as_bytes())
.build()
}
pub fn comms_history_key(
user_id: uuid::Uuid,
created_at_ms: i64,
id: uuid::Uuid,
) -> SmallVec<[u8; 128]> {
let reversed_ts = i64::MAX.saturating_sub(created_at_ms);
KeyBuilder::new()
.tag(KeyTag::INFRA_COMMS_HISTORY)
.bytes(user_id.as_bytes())
.i64(reversed_ts)
.bytes(id.as_bytes())
.build()
}
pub fn comms_history_prefix(user_id: uuid::Uuid) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::INFRA_COMMS_HISTORY)
.bytes(user_id.as_bytes())
.build()
}
pub fn invite_code_used_by_key(user_id: uuid::Uuid) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::INFRA_INVITE_CODE_USED_BY)
.bytes(user_id.as_bytes())
.build()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metastore::encoding::KeyReader;
#[test]
fn queued_comms_value_roundtrip() {
let val = QueuedCommsValue {
id: uuid::Uuid::new_v4(),
user_id: Some(uuid::Uuid::new_v4()),
channel: 0,
comms_type: 1,
recipient: "user@example.com".to_owned(),
subject: Some("test".to_owned()),
body: "body text".to_owned(),
metadata: None,
status: 0,
error_message: None,
attempts: 0,
max_attempts: 3,
created_at_ms: 1700000000000,
scheduled_for_ms: 1700000000000,
sent_at_ms: None,
};
let bytes = val.serialize();
assert_eq!(bytes[0], COMMS_SCHEMA_VERSION);
let decoded = QueuedCommsValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn invite_code_value_roundtrip() {
let val = InviteCodeValue {
code: "abc-def-ghi".to_owned(),
available_uses: 5,
disabled: false,
for_account: Some("did:plc:test".to_owned()),
created_by: Some(uuid::Uuid::new_v4()),
created_at_ms: 1700000000000,
};
let bytes = val.serialize();
assert_eq!(bytes[0], INVITE_CODE_SCHEMA_VERSION);
let decoded = InviteCodeValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn invite_use_value_roundtrip() {
let val = InviteCodeUseValue {
used_by: uuid::Uuid::new_v4(),
used_at_ms: 1700000000000,
};
let bytes = val.serialize();
let decoded = InviteCodeUseValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn signing_key_value_roundtrip() {
let val = SigningKeyValue {
id: uuid::Uuid::new_v4(),
did: Some("did:plc:test".to_owned()),
public_key_did_key: "did:key:z123".to_owned(),
private_key_bytes: vec![1, 2, 3, 4],
used: false,
created_at_ms: 1700000000000,
expires_at_ms: 1700000600000,
};
let bytes = val.serialize();
let decoded = SigningKeyValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn deletion_request_value_roundtrip() {
let val = DeletionRequestValue {
token: "tok-abc".to_owned(),
did: "did:plc:test".to_owned(),
created_at_ms: 1700000000000,
expires_at_ms: 1700000600000,
};
let bytes = val.serialize();
let decoded = DeletionRequestValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn report_value_roundtrip() {
let val = ReportValue {
id: 42,
reason_type: "spam".to_owned(),
reason: Some("bad content".to_owned()),
subject_json: b"{}".to_vec(),
reported_by_did: "did:plc:reporter".to_owned(),
created_at_ms: 1700000000000,
};
let bytes = val.serialize();
let decoded = ReportValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn notification_history_value_roundtrip() {
let val = NotificationHistoryValue {
id: uuid::Uuid::new_v4(),
channel: 0,
comms_type: 1,
recipient: "user@example.com".to_owned(),
subject: None,
body: "notification body".to_owned(),
status: 2,
created_at_ms: 1700000000000,
};
let bytes = val.serialize();
let decoded = NotificationHistoryValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn comms_queue_key_roundtrip() {
let id = uuid::Uuid::new_v4();
let key = comms_queue_key(id);
let mut reader = KeyReader::new(&key);
assert_eq!(reader.tag(), Some(KeyTag::INFRA_COMMS_QUEUE.raw()));
let id_bytes = reader.bytes().unwrap();
assert_eq!(uuid::Uuid::from_slice(&id_bytes).unwrap(), id);
assert!(reader.is_empty());
}
#[test]
fn invite_code_key_roundtrip() {
let key = invite_code_key("abc-def");
let mut reader = KeyReader::new(&key);
assert_eq!(reader.tag(), Some(KeyTag::INFRA_INVITE_CODE.raw()));
assert_eq!(reader.string(), Some("abc-def".to_owned()));
assert!(reader.is_empty());
}
#[test]
fn invite_use_key_roundtrip() {
let user_id = uuid::Uuid::new_v4();
let key = invite_use_key("code1", user_id);
let mut reader = KeyReader::new(&key);
assert_eq!(reader.tag(), Some(KeyTag::INFRA_INVITE_USE.raw()));
assert_eq!(reader.string(), Some("code1".to_owned()));
let id_bytes = reader.bytes().unwrap();
assert_eq!(uuid::Uuid::from_slice(&id_bytes).unwrap(), user_id);
assert!(reader.is_empty());
}
#[test]
fn comms_history_newest_first_ordering() {
let user_id = uuid::Uuid::new_v4();
let id_a = uuid::Uuid::new_v4();
let id_b = uuid::Uuid::new_v4();
let key_old = comms_history_key(user_id, 1000, id_a);
let key_new = comms_history_key(user_id, 2000, id_b);
assert!(key_new.as_slice() < key_old.as_slice());
}
#[test]
fn account_pref_key_roundtrip() {
let user_id = uuid::Uuid::new_v4();
let key = account_pref_key(user_id, "app.bsky.actor.profile");
let mut reader = KeyReader::new(&key);
assert_eq!(reader.tag(), Some(KeyTag::INFRA_ACCOUNT_PREF.raw()));
let id_bytes = reader.bytes().unwrap();
assert_eq!(uuid::Uuid::from_slice(&id_bytes).unwrap(), user_id);
assert_eq!(reader.string(), Some("app.bsky.actor.profile".to_owned()));
assert!(reader.is_empty());
}
#[test]
fn plc_token_key_roundtrip() {
let user_id = uuid::Uuid::new_v4();
let key = plc_token_key(user_id, "tok123");
let mut reader = KeyReader::new(&key);
assert_eq!(reader.tag(), Some(KeyTag::INFRA_PLC_TOKEN.raw()));
let id_bytes = reader.bytes().unwrap();
assert_eq!(uuid::Uuid::from_slice(&id_bytes).unwrap(), user_id);
assert_eq!(reader.string(), Some("tok123".to_owned()));
assert!(reader.is_empty());
}
#[test]
fn deserialize_unknown_version_returns_none() {
let val = QueuedCommsValue {
id: uuid::Uuid::new_v4(),
user_id: None,
channel: 0,
comms_type: 0,
recipient: String::new(),
subject: None,
body: String::new(),
metadata: None,
status: 0,
error_message: None,
attempts: 0,
max_attempts: 3,
created_at_ms: 0,
scheduled_for_ms: 0,
sent_at_ms: None,
};
let mut bytes = val.serialize();
bytes[0] = 99;
assert!(QueuedCommsValue::deserialize(&bytes).is_none());
}
#[test]
fn channel_u8_roundtrip() {
use tranquil_db_traits::CommsChannel;
[
CommsChannel::Email,
CommsChannel::Discord,
CommsChannel::Telegram,
CommsChannel::Signal,
]
.iter()
.for_each(|&ch| {
assert_eq!(u8_to_channel(channel_to_u8(ch)), Some(ch));
});
}
#[test]
fn comms_type_u8_roundtrip() {
use tranquil_db_traits::CommsType;
[
CommsType::Welcome,
CommsType::EmailVerification,
CommsType::PasswordReset,
CommsType::EmailUpdate,
CommsType::AccountDeletion,
CommsType::AdminEmail,
CommsType::PlcOperation,
CommsType::TwoFactorCode,
CommsType::PasskeyRecovery,
CommsType::LegacyLoginAlert,
CommsType::MigrationVerification,
CommsType::ChannelVerification,
CommsType::ChannelVerified,
]
.iter()
.for_each(|&ct| {
assert_eq!(u8_to_comms_type(comms_type_to_u8(ct)), Some(ct));
});
}
#[test]
fn status_u8_roundtrip() {
use tranquil_db_traits::CommsStatus;
[
CommsStatus::Pending,
CommsStatus::Processing,
CommsStatus::Sent,
CommsStatus::Failed,
]
.iter()
.for_each(|&s| {
assert_eq!(u8_to_status(status_to_u8(s)), Some(s));
});
}
#[test]
fn u8_to_channel_invalid_returns_none() {
assert!(u8_to_channel(255).is_none());
}
#[test]
fn u8_to_comms_type_invalid_returns_none() {
assert!(u8_to_comms_type(255).is_none());
}
#[test]
fn u8_to_status_invalid_returns_none() {
assert!(u8_to_status(255).is_none());
}
}

View File

@@ -55,6 +55,76 @@ impl KeyTag {
pub const RECORD_BLOBS: Self = Self(0x30);
pub const BACKLINK_BY_USER: Self = Self(0x31);
pub const USER_PRIMARY: Self = Self(0x40);
pub const USER_BY_HANDLE: Self = Self(0x41);
pub const USER_BY_EMAIL: Self = Self(0x42);
pub const USER_PASSKEYS: Self = Self(0x43);
pub const USER_PASSKEY_BY_CRED: Self = Self(0x44);
pub const USER_TOTP: Self = Self(0x45);
pub const USER_BACKUP_CODES: Self = Self(0x46);
pub const USER_WEBAUTHN_CHALLENGE: Self = Self(0x47);
pub const USER_RESET_CODE: Self = Self(0x48);
pub const USER_RECOVERY_TOKEN: Self = Self(0x49);
pub const USER_DID_WEB_OVERRIDES: Self = Self(0x4A);
pub const USER_HANDLE_RESERVATION: Self = Self(0x4B);
pub const USER_COMMS_CHANNEL: Self = Self(0x4C);
pub const USER_TELEGRAM_LOOKUP: Self = Self(0x4D);
pub const USER_DISCORD_LOOKUP: Self = Self(0x4E);
pub const SESSION_PRIMARY: Self = Self(0x50);
pub const SESSION_BY_ACCESS: Self = Self(0x51);
pub const SESSION_BY_REFRESH: Self = Self(0x52);
pub const SESSION_USED_REFRESH: Self = Self(0x53);
pub const SESSION_APP_PASSWORD: Self = Self(0x54);
pub const SESSION_BY_DID: Self = Self(0x55);
pub const SESSION_LAST_REAUTH: Self = Self(0x56);
pub const SESSION_ID_COUNTER: Self = Self(0x57);
pub const OAUTH_TOKEN: Self = Self(0x60);
pub const OAUTH_TOKEN_BY_ID: Self = Self(0x61);
pub const OAUTH_TOKEN_BY_REFRESH: Self = Self(0x62);
pub const OAUTH_TOKEN_BY_PREV_REFRESH: Self = Self(0x63);
pub const OAUTH_USED_REFRESH: Self = Self(0x64);
pub const OAUTH_AUTH_REQUEST: Self = Self(0x65);
pub const OAUTH_AUTH_BY_CODE: Self = Self(0x66);
pub const OAUTH_DEVICE: Self = Self(0x67);
pub const OAUTH_ACCOUNT_DEVICE: Self = Self(0x68);
pub const OAUTH_DPOP_JTI: Self = Self(0x69);
pub const OAUTH_2FA_CHALLENGE: Self = Self(0x6A);
pub const OAUTH_2FA_BY_REQUEST: Self = Self(0x6B);
pub const OAUTH_SCOPE_PREFS: Self = Self(0x6C);
pub const OAUTH_AUTH_CLIENT: Self = Self(0x6D);
pub const OAUTH_DEVICE_TRUST: Self = Self(0x6E);
pub const OAUTH_TOKEN_FAMILY_COUNTER: Self = Self(0x6F);
pub const INFRA_COMMS_QUEUE: Self = Self(0x70);
pub const INFRA_INVITE_CODE: Self = Self(0x71);
pub const INFRA_INVITE_USE: Self = Self(0x72);
pub const INFRA_INVITE_BY_ACCOUNT: Self = Self(0x73);
pub const INFRA_INVITE_BY_USER: Self = Self(0x74);
pub const INFRA_SIGNING_KEY: Self = Self(0x75);
pub const INFRA_SIGNING_KEY_BY_ID: Self = Self(0x76);
pub const INFRA_DELETION_REQUEST: Self = Self(0x77);
pub const INFRA_DELETION_BY_DID: Self = Self(0x78);
pub const INFRA_ACCOUNT_PREF: Self = Self(0x79);
pub const INFRA_SERVER_CONFIG: Self = Self(0x7A);
pub const INFRA_REPORT: Self = Self(0x7B);
pub const INFRA_PLC_TOKEN: Self = Self(0x7C);
pub const INFRA_COMMS_HISTORY: Self = Self(0x7D);
pub const INFRA_INVITE_CODE_USED_BY: Self = Self(0x7E);
pub const DELEG_GRANT: Self = Self(0x80);
pub const DELEG_BY_CONTROLLER: Self = Self(0x81);
pub const DELEG_AUDIT_LOG: Self = Self(0x82);
pub const SSO_IDENTITY: Self = Self(0x90);
pub const SSO_BY_PROVIDER: Self = Self(0x91);
pub const SSO_AUTH_STATE: Self = Self(0x92);
pub const SSO_PENDING_REG: Self = Self(0x93);
pub const SSO_BY_ID: Self = Self(0x94);
pub const OAUTH_TOKEN_BY_FAMILY: Self = Self(0xA0);
pub const FORMAT_VERSION: Self = Self(0xFF);
pub const fn raw(self) -> u8 {
@@ -117,6 +187,69 @@ mod tests {
KeyTag::DID_EVENTS,
KeyTag::RECORD_BLOBS,
KeyTag::BACKLINK_BY_USER,
KeyTag::USER_PRIMARY,
KeyTag::USER_BY_HANDLE,
KeyTag::USER_BY_EMAIL,
KeyTag::USER_PASSKEYS,
KeyTag::USER_PASSKEY_BY_CRED,
KeyTag::USER_TOTP,
KeyTag::USER_BACKUP_CODES,
KeyTag::USER_WEBAUTHN_CHALLENGE,
KeyTag::USER_RESET_CODE,
KeyTag::USER_RECOVERY_TOKEN,
KeyTag::USER_DID_WEB_OVERRIDES,
KeyTag::USER_HANDLE_RESERVATION,
KeyTag::USER_COMMS_CHANNEL,
KeyTag::USER_TELEGRAM_LOOKUP,
KeyTag::USER_DISCORD_LOOKUP,
KeyTag::SESSION_PRIMARY,
KeyTag::SESSION_BY_ACCESS,
KeyTag::SESSION_BY_REFRESH,
KeyTag::SESSION_USED_REFRESH,
KeyTag::SESSION_APP_PASSWORD,
KeyTag::SESSION_BY_DID,
KeyTag::SESSION_LAST_REAUTH,
KeyTag::SESSION_ID_COUNTER,
KeyTag::OAUTH_TOKEN,
KeyTag::OAUTH_TOKEN_BY_ID,
KeyTag::OAUTH_TOKEN_BY_REFRESH,
KeyTag::OAUTH_TOKEN_BY_PREV_REFRESH,
KeyTag::OAUTH_USED_REFRESH,
KeyTag::OAUTH_AUTH_REQUEST,
KeyTag::OAUTH_AUTH_BY_CODE,
KeyTag::OAUTH_DEVICE,
KeyTag::OAUTH_ACCOUNT_DEVICE,
KeyTag::OAUTH_DPOP_JTI,
KeyTag::OAUTH_2FA_CHALLENGE,
KeyTag::OAUTH_2FA_BY_REQUEST,
KeyTag::OAUTH_SCOPE_PREFS,
KeyTag::OAUTH_AUTH_CLIENT,
KeyTag::OAUTH_DEVICE_TRUST,
KeyTag::OAUTH_TOKEN_FAMILY_COUNTER,
KeyTag::INFRA_COMMS_QUEUE,
KeyTag::INFRA_INVITE_CODE,
KeyTag::INFRA_INVITE_USE,
KeyTag::INFRA_INVITE_BY_ACCOUNT,
KeyTag::INFRA_INVITE_BY_USER,
KeyTag::INFRA_SIGNING_KEY,
KeyTag::INFRA_SIGNING_KEY_BY_ID,
KeyTag::INFRA_DELETION_REQUEST,
KeyTag::INFRA_DELETION_BY_DID,
KeyTag::INFRA_ACCOUNT_PREF,
KeyTag::INFRA_SERVER_CONFIG,
KeyTag::INFRA_REPORT,
KeyTag::INFRA_PLC_TOKEN,
KeyTag::INFRA_COMMS_HISTORY,
KeyTag::INFRA_INVITE_CODE_USED_BY,
KeyTag::DELEG_GRANT,
KeyTag::DELEG_BY_CONTROLLER,
KeyTag::DELEG_AUDIT_LOG,
KeyTag::SSO_IDENTITY,
KeyTag::SSO_BY_PROVIDER,
KeyTag::SSO_AUTH_STATE,
KeyTag::SSO_PENDING_REG,
KeyTag::SSO_BY_ID,
KeyTag::OAUTH_TOKEN_BY_FAMILY,
KeyTag::FORMAT_VERSION,
];
let mut raw: Vec<u8> = tags.iter().map(|t| t.raw()).collect();

View File

@@ -3,10 +3,16 @@ pub mod backlinks;
pub mod blob_ops;
pub mod blobs;
pub mod commit_ops;
pub mod delegation_ops;
pub mod delegations;
pub mod encoding;
pub mod event_keys;
pub mod event_ops;
pub mod infra_ops;
pub mod infra_schema;
pub mod keys;
pub mod oauth_ops;
pub mod oauth_schema;
pub mod partitions;
pub mod record_ops;
pub mod records;
@@ -14,9 +20,15 @@ pub mod recovery;
pub mod repo_meta;
pub mod repo_ops;
pub mod scan;
pub mod session_ops;
pub mod sessions;
pub mod sso_ops;
pub mod sso_schema;
pub mod user_block_ops;
pub mod user_blocks;
pub mod user_hash;
pub mod user_ops;
pub mod users;
use std::path::Path;
use std::sync::Arc;
@@ -145,6 +157,7 @@ pub struct Metastore {
db: Database,
partitions: [Keyspace; Partition::ALL.len()],
user_hashes: Arc<UserHashMap>,
counter_lock: Arc<parking_lot::Mutex<()>>,
}
impl Metastore {
@@ -185,6 +198,7 @@ impl Metastore {
db,
partitions,
user_hashes,
counter_lock: Arc::new(parking_lot::Mutex::new(())),
})
}
@@ -274,6 +288,61 @@ impl Metastore {
)
}
pub fn delegation_ops(&self) -> delegation_ops::DelegationOps {
delegation_ops::DelegationOps::new(
self.db.clone(),
self.partitions[Partition::Indexes.index()].clone(),
self.partitions[Partition::Users.index()].clone(),
Arc::clone(&self.user_hashes),
)
}
pub fn sso_ops(&self) -> sso_ops::SsoOps {
sso_ops::SsoOps::new(
self.db.clone(),
self.partitions[Partition::Indexes.index()].clone(),
)
}
pub fn session_ops(&self) -> session_ops::SessionOps {
session_ops::SessionOps::new(
self.db.clone(),
self.partitions[Partition::Auth.index()].clone(),
self.partitions[Partition::Users.index()].clone(),
Arc::clone(&self.user_hashes),
Arc::clone(&self.counter_lock),
)
}
pub fn infra_ops(&self) -> infra_ops::InfraOps {
infra_ops::InfraOps::new(
self.db.clone(),
self.partitions[Partition::Infra.index()].clone(),
self.partitions[Partition::RepoData.index()].clone(),
self.partitions[Partition::Users.index()].clone(),
Arc::clone(&self.user_hashes),
)
}
pub fn oauth_ops(&self) -> oauth_ops::OAuthOps {
oauth_ops::OAuthOps::new(
self.db.clone(),
self.partitions[Partition::Auth.index()].clone(),
self.partitions[Partition::Users.index()].clone(),
Arc::clone(&self.counter_lock),
)
}
pub fn user_ops(&self) -> user_ops::UserOps {
user_ops::UserOps::new(
self.db.clone(),
self.partitions[Partition::Users.index()].clone(),
self.partitions[Partition::RepoData.index()].clone(),
self.partitions[Partition::Auth.index()].clone(),
Arc::clone(&self.user_hashes),
)
}
pub fn commit_ops<S: crate::io::StorageIO>(
&self,
bridge: Arc<crate::eventlog::EventLogBridge<S>>,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,556 @@
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use super::encoding::KeyBuilder;
use super::keys::{KeyTag, UserHash};
const TOKEN_SCHEMA_VERSION: u8 = 1;
const REQUEST_SCHEMA_VERSION: u8 = 1;
const DEVICE_SCHEMA_VERSION: u8 = 1;
const ACCOUNT_DEVICE_SCHEMA_VERSION: u8 = 1;
const DPOP_JTI_SCHEMA_VERSION: u8 = 1;
const CHALLENGE_SCHEMA_VERSION: u8 = 1;
const SCOPE_PREFS_SCHEMA_VERSION: u8 = 1;
const AUTH_CLIENT_SCHEMA_VERSION: u8 = 1;
const DEVICE_TRUST_SCHEMA_VERSION: u8 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OAuthTokenValue {
pub family_id: i32,
pub did: String,
pub client_id: String,
pub token_id: String,
pub refresh_token: String,
pub previous_refresh_token: Option<String>,
pub scope: String,
pub expires_at_ms: i64,
pub created_at_ms: i64,
pub updated_at_ms: i64,
pub parameters_json: String,
pub controller_did: Option<String>,
}
impl OAuthTokenValue {
pub fn serialize_with_ttl(&self) -> Vec<u8> {
let ttl_bytes = u64::try_from(self.expires_at_ms).unwrap_or(0).to_be_bytes();
let payload =
postcard::to_allocvec(self).expect("OAuthTokenValue serialization cannot fail");
let mut buf = Vec::with_capacity(8 + 1 + payload.len());
buf.extend_from_slice(&ttl_bytes);
buf.push(TOKEN_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
if bytes.len() < 9 {
return None;
}
let (_ttl, rest) = bytes.split_at(8);
let (&version, payload) = rest.split_first()?;
match version {
TOKEN_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OAuthRequestValue {
pub client_id: String,
pub client_auth_json: Option<String>,
pub parameters_json: String,
pub expires_at_ms: i64,
pub did: Option<String>,
pub device_id: Option<String>,
pub code: Option<String>,
pub controller_did: Option<String>,
}
impl OAuthRequestValue {
pub fn serialize_with_ttl(&self) -> Vec<u8> {
let ttl_bytes = u64::try_from(self.expires_at_ms).unwrap_or(0).to_be_bytes();
let payload =
postcard::to_allocvec(self).expect("OAuthRequestValue serialization cannot fail");
let mut buf = Vec::with_capacity(8 + 1 + payload.len());
buf.extend_from_slice(&ttl_bytes);
buf.push(REQUEST_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
if bytes.len() < 9 {
return None;
}
let (_ttl, rest) = bytes.split_at(8);
let (&version, payload) = rest.split_first()?;
match version {
REQUEST_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OAuthDeviceValue {
pub session_id: String,
pub user_agent: Option<String>,
pub ip_address: String,
pub last_seen_at_ms: i64,
pub created_at_ms: i64,
}
impl OAuthDeviceValue {
pub fn serialize(&self) -> Vec<u8> {
let payload =
postcard::to_allocvec(self).expect("OAuthDeviceValue serialization cannot fail");
let mut buf = Vec::with_capacity(8 + 1 + payload.len());
buf.extend_from_slice(&0u64.to_be_bytes());
buf.push(DEVICE_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let rest = bytes.get(8..)?;
let (&version, payload) = rest.split_first()?;
match version {
DEVICE_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AccountDeviceValue {
pub last_used_at_ms: i64,
}
impl AccountDeviceValue {
pub fn serialize_with_ttl(&self) -> Vec<u8> {
let ttl_bytes = 0u64.to_be_bytes();
let payload =
postcard::to_allocvec(self).expect("AccountDeviceValue serialization cannot fail");
let mut buf = Vec::with_capacity(8 + 1 + payload.len());
buf.extend_from_slice(&ttl_bytes);
buf.push(ACCOUNT_DEVICE_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
if bytes.len() < 9 {
return None;
}
let (_ttl, rest) = bytes.split_at(8);
let (&version, payload) = rest.split_first()?;
match version {
ACCOUNT_DEVICE_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DpopJtiValue {
pub recorded_at_ms: i64,
}
const DPOP_JTI_WINDOW_MS: i64 = 300_000;
impl DpopJtiValue {
pub fn serialize_with_ttl(&self) -> Vec<u8> {
let expires_at_ms = self.recorded_at_ms.saturating_add(DPOP_JTI_WINDOW_MS);
let ttl_bytes = u64::try_from(expires_at_ms).unwrap_or(0).to_be_bytes();
let payload = postcard::to_allocvec(self).expect("DpopJtiValue serialization cannot fail");
let mut buf = Vec::with_capacity(8 + 1 + payload.len());
buf.extend_from_slice(&ttl_bytes);
buf.push(DPOP_JTI_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
if bytes.len() < 9 {
return None;
}
let (_ttl, rest) = bytes.split_at(8);
let (&version, payload) = rest.split_first()?;
match version {
DPOP_JTI_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
pub fn ttl_ms(bytes: &[u8]) -> Option<u64> {
if bytes.len() < 8 {
return None;
}
let ttl_bytes: [u8; 8] = bytes[..8].try_into().ok()?;
Some(u64::from_be_bytes(ttl_bytes))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TwoFactorChallengeValue {
pub id: [u8; 16],
pub did: String,
pub request_uri: String,
pub code: String,
pub attempts: i32,
pub created_at_ms: i64,
pub expires_at_ms: i64,
}
impl TwoFactorChallengeValue {
pub fn serialize_with_ttl(&self) -> Vec<u8> {
let ttl_bytes = u64::try_from(self.expires_at_ms).unwrap_or(0).to_be_bytes();
let payload =
postcard::to_allocvec(self).expect("TwoFactorChallengeValue serialization cannot fail");
let mut buf = Vec::with_capacity(8 + 1 + payload.len());
buf.extend_from_slice(&ttl_bytes);
buf.push(CHALLENGE_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
if bytes.len() < 9 {
return None;
}
let (_ttl, rest) = bytes.split_at(8);
let (&version, payload) = rest.split_first()?;
match version {
CHALLENGE_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
pub fn ttl_ms(bytes: &[u8]) -> Option<u64> {
if bytes.len() < 8 {
return None;
}
let ttl_bytes: [u8; 8] = bytes[..8].try_into().ok()?;
Some(u64::from_be_bytes(ttl_bytes))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScopePrefsValue {
pub prefs_json: String,
}
impl ScopePrefsValue {
pub fn serialize(&self) -> Vec<u8> {
let payload =
postcard::to_allocvec(self).expect("ScopePrefsValue serialization cannot fail");
let mut buf = Vec::with_capacity(8 + 1 + payload.len());
buf.extend_from_slice(&0u64.to_be_bytes());
buf.push(SCOPE_PREFS_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let rest = bytes.get(8..)?;
let (&version, payload) = rest.split_first()?;
match version {
SCOPE_PREFS_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthorizedClientValue {
pub data_json: String,
}
impl AuthorizedClientValue {
pub fn serialize(&self) -> Vec<u8> {
let payload =
postcard::to_allocvec(self).expect("AuthorizedClientValue serialization cannot fail");
let mut buf = Vec::with_capacity(8 + 1 + payload.len());
buf.extend_from_slice(&0u64.to_be_bytes());
buf.push(AUTH_CLIENT_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let rest = bytes.get(8..)?;
let (&version, payload) = rest.split_first()?;
match version {
AUTH_CLIENT_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeviceTrustValue {
pub device_id: String,
pub did: String,
pub user_agent: Option<String>,
pub friendly_name: Option<String>,
pub trusted_at_ms: Option<i64>,
pub trusted_until_ms: Option<i64>,
pub last_seen_at_ms: i64,
}
impl DeviceTrustValue {
pub fn serialize(&self) -> Vec<u8> {
let payload =
postcard::to_allocvec(self).expect("DeviceTrustValue serialization cannot fail");
let mut buf = Vec::with_capacity(8 + 1 + payload.len());
buf.extend_from_slice(&0u64.to_be_bytes());
buf.push(DEVICE_TRUST_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let rest = bytes.get(8..)?;
let (&version, payload) = rest.split_first()?;
match version {
DEVICE_TRUST_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UsedRefreshValue {
pub family_id: i32,
}
impl UsedRefreshValue {
pub fn serialize_with_ttl(&self, expires_at_ms: i64) -> Vec<u8> {
let ttl_bytes = u64::try_from(expires_at_ms).unwrap_or(0).to_be_bytes();
let payload =
postcard::to_allocvec(self).expect("UsedRefreshValue serialization cannot fail");
let mut buf = Vec::with_capacity(8 + 1 + payload.len());
buf.extend_from_slice(&ttl_bytes);
buf.push(TOKEN_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
if bytes.len() < 9 {
return None;
}
let (_ttl, rest) = bytes.split_at(8);
let (&version, payload) = rest.split_first()?;
match version {
TOKEN_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
pub fn oauth_token_key(user_hash: UserHash, family_id: i32) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_TOKEN)
.u64(user_hash.raw())
.raw(&family_id.to_be_bytes())
.build()
}
pub fn oauth_token_user_prefix(user_hash: UserHash) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_TOKEN)
.u64(user_hash.raw())
.build()
}
pub fn oauth_token_by_id_key(token_id: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_TOKEN_BY_ID)
.string(token_id)
.build()
}
pub fn oauth_token_by_refresh_key(refresh_token: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_TOKEN_BY_REFRESH)
.string(refresh_token)
.build()
}
pub fn oauth_token_by_prev_refresh_key(refresh_token: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_TOKEN_BY_PREV_REFRESH)
.string(refresh_token)
.build()
}
pub fn oauth_used_refresh_key(refresh_token: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_USED_REFRESH)
.string(refresh_token)
.build()
}
pub fn oauth_auth_request_key(request_id: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_AUTH_REQUEST)
.string(request_id)
.build()
}
pub fn oauth_auth_request_prefix() -> SmallVec<[u8; 128]> {
KeyBuilder::new().tag(KeyTag::OAUTH_AUTH_REQUEST).build()
}
pub fn oauth_auth_by_code_key(code: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_AUTH_BY_CODE)
.string(code)
.build()
}
pub fn oauth_device_key(device_id: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_DEVICE)
.string(device_id)
.build()
}
pub fn oauth_account_device_key(user_hash: UserHash, device_id: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_ACCOUNT_DEVICE)
.u64(user_hash.raw())
.string(device_id)
.build()
}
pub fn oauth_account_device_prefix(user_hash: UserHash) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_ACCOUNT_DEVICE)
.u64(user_hash.raw())
.build()
}
pub fn oauth_dpop_jti_key(jti: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_DPOP_JTI)
.string(jti)
.build()
}
pub fn oauth_dpop_jti_prefix() -> SmallVec<[u8; 128]> {
KeyBuilder::new().tag(KeyTag::OAUTH_DPOP_JTI).build()
}
pub fn oauth_2fa_challenge_key(id: &[u8; 16]) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_2FA_CHALLENGE)
.raw(id)
.build()
}
pub fn oauth_2fa_challenge_prefix() -> SmallVec<[u8; 128]> {
KeyBuilder::new().tag(KeyTag::OAUTH_2FA_CHALLENGE).build()
}
pub fn oauth_2fa_by_request_key(request_uri: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_2FA_BY_REQUEST)
.string(request_uri)
.build()
}
pub fn oauth_scope_prefs_key(user_hash: UserHash, client_id: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_SCOPE_PREFS)
.u64(user_hash.raw())
.string(client_id)
.build()
}
pub fn oauth_auth_client_key(user_hash: UserHash, client_id: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_AUTH_CLIENT)
.u64(user_hash.raw())
.string(client_id)
.build()
}
pub fn oauth_device_trust_key(user_hash: UserHash, device_id: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_DEVICE_TRUST)
.u64(user_hash.raw())
.string(device_id)
.build()
}
pub fn oauth_device_trust_prefix(user_hash: UserHash) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_DEVICE_TRUST)
.u64(user_hash.raw())
.build()
}
pub fn oauth_token_family_counter_key() -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_TOKEN_FAMILY_COUNTER)
.build()
}
pub fn serialize_family_counter(counter: i32) -> Vec<u8> {
let mut buf = Vec::with_capacity(12);
buf.extend_from_slice(&0u64.to_be_bytes());
buf.extend_from_slice(&counter.to_be_bytes());
buf
}
pub fn deserialize_family_counter(bytes: &[u8]) -> Option<i32> {
match bytes.len() {
4 => Some(i32::from_be_bytes(bytes.try_into().ok()?)),
n if n >= 12 => {
let arr: [u8; 4] = bytes[8..12].try_into().ok()?;
Some(i32::from_be_bytes(arr))
}
_ => None,
}
}
pub fn oauth_token_by_family_key(family_id: i32) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::OAUTH_TOKEN_BY_FAMILY)
.raw(&family_id.to_be_bytes())
.build()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TokenIndexValue {
pub user_hash: u64,
pub family_id: i32,
}
impl TokenIndexValue {
pub fn serialize_with_ttl(&self, expires_at_ms: i64) -> Vec<u8> {
let ttl_bytes = u64::try_from(expires_at_ms).unwrap_or(0).to_be_bytes();
let payload =
postcard::to_allocvec(self).expect("TokenIndexValue serialization cannot fail");
let mut buf = Vec::with_capacity(8 + 1 + payload.len());
buf.extend_from_slice(&ttl_bytes);
buf.push(TOKEN_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
if bytes.len() < 9 {
return None;
}
let (_ttl, rest) = bytes.split_at(8);
let (&version, payload) = rest.split_first()?;
match version {
TOKEN_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}

View File

@@ -0,0 +1,919 @@
use std::sync::Arc;
use chrono::{DateTime, Utc};
use fjall::{Database, Keyspace};
use uuid::Uuid;
use super::MetastoreError;
use super::keys::UserHash;
use super::scan::point_lookup;
use super::sessions::{
AppPasswordValue, SessionIndexValue, SessionTokenValue, deserialize_id_counter_value,
deserialize_last_reauth_value, deserialize_used_refresh_value, login_type_to_u8,
privilege_to_u8, serialize_by_did_value, serialize_id_counter_value,
serialize_last_reauth_value, serialize_used_refresh_value, session_app_password_key,
session_app_password_prefix, session_by_access_key, session_by_did_key, session_by_did_prefix,
session_by_refresh_key, session_id_counter_key, session_last_reauth_key, session_primary_key,
session_used_refresh_key, u8_to_login_type, u8_to_privilege,
};
use super::user_hash::UserHashMap;
use super::users::UserValue;
use tranquil_db_traits::{
AppPasswordCreate, AppPasswordRecord, LoginType, RefreshSessionResult, SessionForRefresh,
SessionId, SessionListItem, SessionMfaStatus, SessionRefreshData, SessionToken,
SessionTokenCreate,
};
use tranquil_types::Did;
pub struct SessionOps {
db: Database,
auth: Keyspace,
users: Keyspace,
user_hashes: Arc<UserHashMap>,
counter_lock: Arc<parking_lot::Mutex<()>>,
}
impl SessionOps {
pub fn new(
db: Database,
auth: Keyspace,
users: Keyspace,
user_hashes: Arc<UserHashMap>,
counter_lock: Arc<parking_lot::Mutex<()>>,
) -> Self {
Self {
db,
auth,
users,
user_hashes,
counter_lock,
}
}
fn resolve_user_hash_from_did(&self, did: &str) -> UserHash {
UserHash::from_did(did)
}
fn resolve_user_hash_from_uuid(&self, user_id: Uuid) -> Result<UserHash, MetastoreError> {
self.user_hashes
.get(&user_id)
.ok_or(MetastoreError::InvalidInput("unknown user_id"))
}
fn next_session_id(&self) -> Result<i32, MetastoreError> {
let _guard = self.counter_lock.lock();
let counter_key = session_id_counter_key();
let current = self
.auth
.get(counter_key.as_slice())
.map_err(MetastoreError::Fjall)?
.and_then(|raw| deserialize_id_counter_value(&raw))
.unwrap_or(0);
let next = current.saturating_add(1);
self.auth
.insert(counter_key.as_slice(), serialize_id_counter_value(next))
.map_err(MetastoreError::Fjall)?;
Ok(next)
}
fn value_to_session_token(
&self,
v: &SessionTokenValue,
) -> Result<SessionToken, MetastoreError> {
Ok(SessionToken {
id: SessionId::new(v.id),
did: Did::new(v.did.clone())
.map_err(|_| MetastoreError::CorruptData("invalid session did"))?,
access_jti: v.access_jti.clone(),
refresh_jti: v.refresh_jti.clone(),
access_expires_at: DateTime::from_timestamp_millis(v.access_expires_at_ms)
.unwrap_or_default(),
refresh_expires_at: DateTime::from_timestamp_millis(v.refresh_expires_at_ms)
.unwrap_or_default(),
login_type: u8_to_login_type(v.login_type).unwrap_or(LoginType::Modern),
mfa_verified: v.mfa_verified,
scope: v.scope.clone(),
controller_did: v
.controller_did
.as_ref()
.and_then(|d| Did::new(d.clone()).ok()),
app_password_name: v.app_password_name.clone(),
created_at: DateTime::from_timestamp_millis(v.created_at_ms).unwrap_or_default(),
updated_at: DateTime::from_timestamp_millis(v.updated_at_ms).unwrap_or_default(),
})
}
fn value_to_app_password(
&self,
v: &AppPasswordValue,
) -> Result<AppPasswordRecord, MetastoreError> {
Ok(AppPasswordRecord {
id: v.id,
user_id: v.user_id,
name: v.name.clone(),
password_hash: v.password_hash.clone(),
created_at: DateTime::from_timestamp_millis(v.created_at_ms).unwrap_or_default(),
privilege: u8_to_privilege(v.privilege)
.unwrap_or(tranquil_db_traits::AppPasswordPrivilege::Standard),
scopes: v.scopes.clone(),
created_by_controller_did: v
.created_by_controller_did
.as_ref()
.and_then(|d| Did::new(d.clone()).ok()),
})
}
fn load_session_by_id(
&self,
session_id: i32,
) -> Result<Option<SessionTokenValue>, MetastoreError> {
let key = session_primary_key(session_id);
point_lookup(
&self.auth,
key.as_slice(),
SessionTokenValue::deserialize,
"corrupt session token",
)
}
fn load_user_value(&self, user_hash: UserHash) -> Result<Option<UserValue>, MetastoreError> {
let key = super::encoding::KeyBuilder::new()
.tag(super::keys::KeyTag::USER_PRIMARY)
.u64(user_hash.raw())
.build();
point_lookup(
&self.users,
key.as_slice(),
UserValue::deserialize,
"corrupt user value",
)
}
fn delete_session_indexes(
&self,
batch: &mut fjall::OwnedWriteBatch,
session: &SessionTokenValue,
) {
let user_hash = self.resolve_user_hash_from_did(&session.did);
batch.remove(&self.auth, session_primary_key(session.id).as_slice());
batch.remove(
&self.auth,
session_by_access_key(&session.access_jti).as_slice(),
);
batch.remove(
&self.auth,
session_by_refresh_key(&session.refresh_jti).as_slice(),
);
batch.remove(
&self.auth,
session_by_did_key(user_hash, session.id).as_slice(),
);
}
fn collect_sessions_for_did(
&self,
user_hash: UserHash,
) -> Result<Vec<SessionTokenValue>, MetastoreError> {
let prefix = session_by_did_prefix(user_hash);
self.auth
.prefix(prefix.as_slice())
.try_fold(Vec::new(), |mut acc, guard| {
let (key_bytes, _) = guard.into_inner().map_err(MetastoreError::Fjall)?;
let mut reader = super::encoding::KeyReader::new(&key_bytes);
let _tag = reader.tag();
let _hash = reader.u64();
let sid_bytes: [u8; 4] = reader
.remaining()
.try_into()
.map_err(|_| MetastoreError::CorruptData("session_by_did key truncated"))?;
let sid = i32::from_be_bytes(sid_bytes);
match self.load_session_by_id(sid)? {
Some(session) => {
acc.push(session);
Ok::<_, MetastoreError>(acc)
}
None => Ok(acc),
}
})
}
pub fn create_session(&self, data: &SessionTokenCreate) -> Result<SessionId, MetastoreError> {
let user_hash = self.resolve_user_hash_from_did(data.did.as_str());
let session_id = self.next_session_id()?;
let now_ms = Utc::now().timestamp_millis();
let value = SessionTokenValue {
id: session_id,
did: data.did.to_string(),
access_jti: data.access_jti.clone(),
refresh_jti: data.refresh_jti.clone(),
access_expires_at_ms: data.access_expires_at.timestamp_millis(),
refresh_expires_at_ms: data.refresh_expires_at.timestamp_millis(),
login_type: login_type_to_u8(data.login_type),
mfa_verified: data.mfa_verified,
scope: data.scope.clone(),
controller_did: data.controller_did.as_ref().map(|d| d.to_string()),
app_password_name: data.app_password_name.clone(),
created_at_ms: now_ms,
updated_at_ms: now_ms,
};
let access_index = SessionIndexValue {
user_hash: user_hash.raw(),
session_id,
};
let refresh_index = SessionIndexValue {
user_hash: user_hash.raw(),
session_id,
};
let mut batch = self.db.batch();
batch.insert(
&self.auth,
session_primary_key(session_id).as_slice(),
value.serialize(),
);
batch.insert(
&self.auth,
session_by_access_key(&data.access_jti).as_slice(),
access_index.serialize(value.refresh_expires_at_ms),
);
batch.insert(
&self.auth,
session_by_refresh_key(&data.refresh_jti).as_slice(),
refresh_index.serialize(value.refresh_expires_at_ms),
);
batch.insert(
&self.auth,
session_by_did_key(user_hash, session_id).as_slice(),
serialize_by_did_value(value.refresh_expires_at_ms),
);
batch.commit().map_err(MetastoreError::Fjall)?;
Ok(SessionId::new(session_id))
}
pub fn get_session_by_access_jti(
&self,
access_jti: &str,
) -> Result<Option<SessionToken>, MetastoreError> {
let index_key = session_by_access_key(access_jti);
let index_val: Option<SessionIndexValue> = point_lookup(
&self.auth,
index_key.as_slice(),
SessionIndexValue::deserialize,
"corrupt session access index",
)?;
match index_val {
Some(idx) => {
let session = self.load_session_by_id(idx.session_id)?;
session.map(|v| self.value_to_session_token(&v)).transpose()
}
None => Ok(None),
}
}
pub fn get_session_for_refresh(
&self,
refresh_jti: &str,
) -> Result<Option<SessionForRefresh>, MetastoreError> {
let index_key = session_by_refresh_key(refresh_jti);
let index_val: Option<SessionIndexValue> = point_lookup(
&self.auth,
index_key.as_slice(),
SessionIndexValue::deserialize,
"corrupt session refresh index",
)?;
let idx = match index_val {
Some(idx) => idx,
None => return Ok(None),
};
let session = match self.load_session_by_id(idx.session_id)? {
Some(s) => s,
None => return Ok(None),
};
let now_ms = Utc::now().timestamp_millis();
if session.refresh_expires_at_ms <= now_ms {
return Ok(None);
}
let user_hash = self.resolve_user_hash_from_did(&session.did);
let user_value = self.load_user_value(user_hash)?;
match user_value {
Some(user) => Ok(Some(SessionForRefresh {
id: SessionId::new(session.id),
did: Did::new(session.did)
.map_err(|_| MetastoreError::CorruptData("invalid session did"))?,
scope: session.scope,
controller_did: session.controller_did.and_then(|d| Did::new(d).ok()),
key_bytes: user.key_bytes,
encryption_version: user.encryption_version,
})),
None => Ok(None),
}
}
pub fn update_session_tokens(
&self,
session_id: SessionId,
new_access_jti: &str,
new_refresh_jti: &str,
new_access_expires_at: DateTime<Utc>,
new_refresh_expires_at: DateTime<Utc>,
) -> Result<(), MetastoreError> {
let mut session = self
.load_session_by_id(session_id.as_i32())?
.ok_or(MetastoreError::InvalidInput("session not found"))?;
let user_hash = self.resolve_user_hash_from_did(&session.did);
let old_access_jti = session.access_jti.clone();
let old_refresh_jti = session.refresh_jti.clone();
session.access_jti = new_access_jti.to_owned();
session.refresh_jti = new_refresh_jti.to_owned();
session.access_expires_at_ms = new_access_expires_at.timestamp_millis();
session.refresh_expires_at_ms = new_refresh_expires_at.timestamp_millis();
session.updated_at_ms = Utc::now().timestamp_millis();
let new_access_index = SessionIndexValue {
user_hash: user_hash.raw(),
session_id: session.id,
};
let new_refresh_index = SessionIndexValue {
user_hash: user_hash.raw(),
session_id: session.id,
};
let mut batch = self.db.batch();
batch.remove(
&self.auth,
session_by_access_key(&old_access_jti).as_slice(),
);
batch.remove(
&self.auth,
session_by_refresh_key(&old_refresh_jti).as_slice(),
);
batch.insert(
&self.auth,
session_primary_key(session.id).as_slice(),
session.serialize(),
);
batch.insert(
&self.auth,
session_by_access_key(new_access_jti).as_slice(),
new_access_index.serialize(session.refresh_expires_at_ms),
);
batch.insert(
&self.auth,
session_by_refresh_key(new_refresh_jti).as_slice(),
new_refresh_index.serialize(session.refresh_expires_at_ms),
);
batch.insert(
&self.auth,
session_by_did_key(user_hash, session.id).as_slice(),
serialize_by_did_value(session.refresh_expires_at_ms),
);
batch.commit().map_err(MetastoreError::Fjall)?;
Ok(())
}
pub fn delete_session_by_access_jti(&self, access_jti: &str) -> Result<u64, MetastoreError> {
let index_key = session_by_access_key(access_jti);
let index_val: Option<SessionIndexValue> = point_lookup(
&self.auth,
index_key.as_slice(),
SessionIndexValue::deserialize,
"corrupt session access index",
)?;
let idx = match index_val {
Some(idx) => idx,
None => return Ok(0),
};
let session = match self.load_session_by_id(idx.session_id)? {
Some(s) => s,
None => return Ok(0),
};
let mut batch = self.db.batch();
self.delete_session_indexes(&mut batch, &session);
batch.commit().map_err(MetastoreError::Fjall)?;
Ok(1)
}
pub fn delete_session_by_id(&self, session_id: SessionId) -> Result<u64, MetastoreError> {
let session = match self.load_session_by_id(session_id.as_i32())? {
Some(s) => s,
None => return Ok(0),
};
let mut batch = self.db.batch();
self.delete_session_indexes(&mut batch, &session);
batch.commit().map_err(MetastoreError::Fjall)?;
Ok(1)
}
pub fn delete_sessions_by_did(&self, did: &Did) -> Result<u64, MetastoreError> {
let user_hash = self.resolve_user_hash_from_did(did.as_str());
let sessions = self.collect_sessions_for_did(user_hash)?;
let count = u64::try_from(sessions.len()).unwrap_or(u64::MAX);
match count {
0 => Ok(0),
_ => {
let mut batch = self.db.batch();
sessions.iter().for_each(|session| {
self.delete_session_indexes(&mut batch, session);
});
batch.commit().map_err(MetastoreError::Fjall)?;
Ok(count)
}
}
}
pub fn delete_sessions_by_did_except_jti(
&self,
did: &Did,
except_jti: &str,
) -> Result<u64, MetastoreError> {
let user_hash = self.resolve_user_hash_from_did(did.as_str());
let sessions = self.collect_sessions_for_did(user_hash)?;
let to_delete: Vec<_> = sessions
.iter()
.filter(|s| s.access_jti != except_jti)
.collect();
let count = u64::try_from(to_delete.len()).unwrap_or(u64::MAX);
match count {
0 => Ok(0),
_ => {
let mut batch = self.db.batch();
to_delete.iter().for_each(|session| {
self.delete_session_indexes(&mut batch, session);
});
batch.commit().map_err(MetastoreError::Fjall)?;
Ok(count)
}
}
}
pub fn list_sessions_by_did(&self, did: &Did) -> Result<Vec<SessionListItem>, MetastoreError> {
let user_hash = self.resolve_user_hash_from_did(did.as_str());
let now_ms = Utc::now().timestamp_millis();
let mut sessions = self.collect_sessions_for_did(user_hash)?;
sessions.retain(|s| s.refresh_expires_at_ms > now_ms);
sessions.sort_by_key(|s| std::cmp::Reverse(s.created_at_ms));
Ok(sessions
.iter()
.map(|s| SessionListItem {
id: SessionId::new(s.id),
access_jti: s.access_jti.clone(),
created_at: DateTime::from_timestamp_millis(s.created_at_ms).unwrap_or_default(),
refresh_expires_at: DateTime::from_timestamp_millis(s.refresh_expires_at_ms)
.unwrap_or_default(),
})
.collect())
}
pub fn get_session_access_jti_by_id(
&self,
session_id: SessionId,
did: &Did,
) -> Result<Option<String>, MetastoreError> {
self.load_session_by_id(session_id.as_i32())?
.filter(|s| s.did == did.as_str())
.map(|s| Ok(s.access_jti))
.transpose()
}
pub fn delete_sessions_by_app_password(
&self,
did: &Did,
app_password_name: &str,
) -> Result<u64, MetastoreError> {
let user_hash = self.resolve_user_hash_from_did(did.as_str());
let sessions = self.collect_sessions_for_did(user_hash)?;
let to_delete: Vec<_> = sessions
.iter()
.filter(|s| s.app_password_name.as_deref() == Some(app_password_name))
.collect();
let count = u64::try_from(to_delete.len()).unwrap_or(u64::MAX);
match count {
0 => Ok(0),
_ => {
let mut batch = self.db.batch();
to_delete.iter().for_each(|session| {
self.delete_session_indexes(&mut batch, session);
});
batch.commit().map_err(MetastoreError::Fjall)?;
Ok(count)
}
}
}
pub fn get_session_jtis_by_app_password(
&self,
did: &Did,
app_password_name: &str,
) -> Result<Vec<String>, MetastoreError> {
let user_hash = self.resolve_user_hash_from_did(did.as_str());
let sessions = self.collect_sessions_for_did(user_hash)?;
Ok(sessions
.iter()
.filter(|s| s.app_password_name.as_deref() == Some(app_password_name))
.map(|s| s.access_jti.clone())
.collect())
}
pub fn check_refresh_token_used(
&self,
refresh_jti: &str,
) -> Result<Option<SessionId>, MetastoreError> {
let key = session_used_refresh_key(refresh_jti);
match self
.auth
.get(key.as_slice())
.map_err(MetastoreError::Fjall)?
{
Some(raw) => Ok(deserialize_used_refresh_value(&raw).map(SessionId::new)),
None => Ok(None),
}
}
pub fn mark_refresh_token_used(
&self,
refresh_jti: &str,
session_id: SessionId,
) -> Result<bool, MetastoreError> {
let key = session_used_refresh_key(refresh_jti);
let existing = self
.auth
.get(key.as_slice())
.map_err(MetastoreError::Fjall)?;
match existing {
Some(_) => Ok(false),
None => {
let session = self.load_session_by_id(session_id.as_i32())?;
let expires_at_ms = session
.map(|s| s.refresh_expires_at_ms)
.unwrap_or(Utc::now().timestamp_millis().saturating_add(86_400_000));
self.auth
.insert(
key.as_slice(),
serialize_used_refresh_value(expires_at_ms, session_id.as_i32()),
)
.map_err(MetastoreError::Fjall)?;
Ok(true)
}
}
}
pub fn list_app_passwords(
&self,
user_id: Uuid,
) -> Result<Vec<AppPasswordRecord>, MetastoreError> {
let user_hash = self.resolve_user_hash_from_uuid(user_id)?;
let prefix = session_app_password_prefix(user_hash);
let mut records: Vec<AppPasswordRecord> =
self.auth
.prefix(prefix.as_slice())
.try_fold(Vec::new(), |mut acc, guard| {
let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?;
let val = AppPasswordValue::deserialize(&val_bytes)
.ok_or(MetastoreError::CorruptData("corrupt app password"))?;
acc.push(self.value_to_app_password(&val)?);
Ok::<_, MetastoreError>(acc)
})?;
records.sort_by_key(|r| std::cmp::Reverse(r.created_at));
Ok(records)
}
pub fn get_app_passwords_for_login(
&self,
user_id: Uuid,
) -> Result<Vec<AppPasswordRecord>, MetastoreError> {
let mut passwords = self.list_app_passwords(user_id)?;
passwords.truncate(20);
Ok(passwords)
}
pub fn get_app_password_by_name(
&self,
user_id: Uuid,
name: &str,
) -> Result<Option<AppPasswordRecord>, MetastoreError> {
let user_hash = self.resolve_user_hash_from_uuid(user_id)?;
let key = session_app_password_key(user_hash, name);
let val: Option<AppPasswordValue> = point_lookup(
&self.auth,
key.as_slice(),
AppPasswordValue::deserialize,
"corrupt app password",
)?;
val.map(|v| self.value_to_app_password(&v)).transpose()
}
pub fn create_app_password(&self, data: &AppPasswordCreate) -> Result<Uuid, MetastoreError> {
let user_hash = self.resolve_user_hash_from_uuid(data.user_id)?;
let id = Uuid::new_v4();
let now_ms = Utc::now().timestamp_millis();
let value = AppPasswordValue {
id,
user_id: data.user_id,
name: data.name.clone(),
password_hash: data.password_hash.clone(),
created_at_ms: now_ms,
privilege: privilege_to_u8(data.privilege),
scopes: data.scopes.clone(),
created_by_controller_did: data
.created_by_controller_did
.as_ref()
.map(|d| d.to_string()),
};
let key = session_app_password_key(user_hash, &data.name);
self.auth
.insert(key.as_slice(), value.serialize())
.map_err(MetastoreError::Fjall)?;
Ok(id)
}
pub fn delete_app_password(&self, user_id: Uuid, name: &str) -> Result<u64, MetastoreError> {
let user_hash = self.resolve_user_hash_from_uuid(user_id)?;
let key = session_app_password_key(user_hash, name);
let exists = self
.auth
.get(key.as_slice())
.map_err(MetastoreError::Fjall)?
.is_some();
match exists {
true => {
self.auth
.remove(key.as_slice())
.map_err(MetastoreError::Fjall)?;
Ok(1)
}
false => Ok(0),
}
}
pub fn delete_app_passwords_by_controller(
&self,
did: &Did,
controller_did: &Did,
) -> Result<u64, MetastoreError> {
let user_hash = self.resolve_user_hash_from_did(did.as_str());
let user_uuid = self
.user_hashes
.get_uuid(&user_hash)
.ok_or(MetastoreError::InvalidInput("unknown did"))?;
let _ = user_uuid;
let prefix = session_app_password_prefix(user_hash);
let controller_str = controller_did.to_string();
let keys_to_remove: Vec<_> = self
.auth
.prefix(prefix.as_slice())
.filter_map(|guard| {
let (key_bytes, val_bytes) = guard.into_inner().ok()?;
let val = AppPasswordValue::deserialize(&val_bytes)?;
match val.created_by_controller_did.as_deref() == Some(controller_str.as_str()) {
true => Some(key_bytes.to_vec()),
false => None,
}
})
.collect();
let count = u64::try_from(keys_to_remove.len()).unwrap_or(u64::MAX);
match count {
0 => Ok(0),
_ => {
let mut batch = self.db.batch();
keys_to_remove.iter().for_each(|key| {
batch.remove(&self.auth, key);
});
batch.commit().map_err(MetastoreError::Fjall)?;
Ok(count)
}
}
}
pub fn get_last_reauth_at(&self, did: &Did) -> Result<Option<DateTime<Utc>>, MetastoreError> {
let user_hash = self.resolve_user_hash_from_did(did.as_str());
let key = session_last_reauth_key(user_hash);
match self
.auth
.get(key.as_slice())
.map_err(MetastoreError::Fjall)?
{
Some(raw) => {
Ok(deserialize_last_reauth_value(&raw).and_then(DateTime::from_timestamp_millis))
}
None => Ok(None),
}
}
pub fn update_last_reauth(&self, did: &Did) -> Result<DateTime<Utc>, MetastoreError> {
let user_hash = self.resolve_user_hash_from_did(did.as_str());
let now = Utc::now();
let key = session_last_reauth_key(user_hash);
self.auth
.insert(
key.as_slice(),
serialize_last_reauth_value(now.timestamp_millis()),
)
.map_err(MetastoreError::Fjall)?;
Ok(now)
}
pub fn get_session_mfa_status(
&self,
did: &Did,
) -> Result<Option<SessionMfaStatus>, MetastoreError> {
let user_hash = self.resolve_user_hash_from_did(did.as_str());
let mut sessions = self.collect_sessions_for_did(user_hash)?;
sessions.sort_by_key(|s| std::cmp::Reverse(s.created_at_ms));
let latest = match sessions.first() {
Some(s) => s,
None => return Ok(None),
};
let last_reauth_at = self.get_last_reauth_at(did)?;
Ok(Some(SessionMfaStatus {
login_type: u8_to_login_type(latest.login_type).unwrap_or(LoginType::Modern),
mfa_verified: latest.mfa_verified,
last_reauth_at,
}))
}
pub fn update_mfa_verified(&self, did: &Did) -> Result<(), MetastoreError> {
let user_hash = self.resolve_user_hash_from_did(did.as_str());
let sessions = self.collect_sessions_for_did(user_hash)?;
let now = Utc::now();
let now_ms = now.timestamp_millis();
let mut batch = self.db.batch();
sessions.iter().for_each(|session| {
let mut updated = session.clone();
updated.mfa_verified = true;
updated.updated_at_ms = now_ms;
batch.insert(
&self.auth,
session_primary_key(updated.id).as_slice(),
updated.serialize(),
);
});
let reauth_key = session_last_reauth_key(user_hash);
batch.insert(
&self.auth,
reauth_key.as_slice(),
serialize_last_reauth_value(now_ms),
);
batch.commit().map_err(MetastoreError::Fjall)?;
Ok(())
}
pub fn get_app_password_hashes_by_did(&self, did: &Did) -> Result<Vec<String>, MetastoreError> {
let user_hash = self.resolve_user_hash_from_did(did.as_str());
match self.user_hashes.get_uuid(&user_hash) {
Some(_) => {}
None => return Ok(Vec::new()),
};
let prefix = session_app_password_prefix(user_hash);
self.auth
.prefix(prefix.as_slice())
.try_fold(Vec::new(), |mut acc, guard| {
let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?;
let val = AppPasswordValue::deserialize(&val_bytes)
.ok_or(MetastoreError::CorruptData("corrupt app password"))?;
acc.push(val.password_hash);
Ok::<_, MetastoreError>(acc)
})
}
pub fn refresh_session_atomic(
&self,
data: &SessionRefreshData,
) -> Result<RefreshSessionResult, MetastoreError> {
let used_key = session_used_refresh_key(&data.old_refresh_jti);
let already_used = self
.auth
.get(used_key.as_slice())
.map_err(MetastoreError::Fjall)?;
if already_used.is_some() {
let mut batch = self.db.batch();
let session = self.load_session_by_id(data.session_id.as_i32())?;
if let Some(s) = session {
self.delete_session_indexes(&mut batch, &s);
}
batch.commit().map_err(MetastoreError::Fjall)?;
return Ok(RefreshSessionResult::TokenAlreadyUsed);
}
let mut session = match self.load_session_by_id(data.session_id.as_i32())? {
Some(s) => s,
None => return Ok(RefreshSessionResult::ConcurrentRefresh),
};
if session.refresh_jti != data.old_refresh_jti {
return Ok(RefreshSessionResult::ConcurrentRefresh);
}
let user_hash = self.resolve_user_hash_from_did(&session.did);
let old_access_jti = session.access_jti.clone();
let old_refresh_jti = session.refresh_jti.clone();
session.access_jti = data.new_access_jti.clone();
session.refresh_jti = data.new_refresh_jti.clone();
session.access_expires_at_ms = data.new_access_expires_at.timestamp_millis();
session.refresh_expires_at_ms = data.new_refresh_expires_at.timestamp_millis();
session.updated_at_ms = Utc::now().timestamp_millis();
let new_access_index = SessionIndexValue {
user_hash: user_hash.raw(),
session_id: session.id,
};
let new_refresh_index = SessionIndexValue {
user_hash: user_hash.raw(),
session_id: session.id,
};
let mut batch = self.db.batch();
batch.insert(
&self.auth,
used_key.as_slice(),
serialize_used_refresh_value(session.refresh_expires_at_ms, session.id),
);
batch.remove(
&self.auth,
session_by_access_key(&old_access_jti).as_slice(),
);
batch.remove(
&self.auth,
session_by_refresh_key(&old_refresh_jti).as_slice(),
);
batch.insert(
&self.auth,
session_primary_key(session.id).as_slice(),
session.serialize(),
);
batch.insert(
&self.auth,
session_by_access_key(&data.new_access_jti).as_slice(),
new_access_index.serialize(session.refresh_expires_at_ms),
);
batch.insert(
&self.auth,
session_by_refresh_key(&data.new_refresh_jti).as_slice(),
new_refresh_index.serialize(session.refresh_expires_at_ms),
);
batch.insert(
&self.auth,
session_by_did_key(user_hash, session.id).as_slice(),
serialize_by_did_value(session.refresh_expires_at_ms),
);
batch.commit().map_err(MetastoreError::Fjall)?;
Ok(RefreshSessionResult::Success)
}
}

View File

@@ -0,0 +1,459 @@
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use super::encoding::KeyBuilder;
use super::keys::{KeyTag, UserHash};
const SESSION_SCHEMA_VERSION: u8 = 1;
const APP_PASSWORD_SCHEMA_VERSION: u8 = 1;
const SESSION_INDEX_SCHEMA_VERSION: u8 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionTokenValue {
pub id: i32,
pub did: String,
pub access_jti: String,
pub refresh_jti: String,
pub access_expires_at_ms: i64,
pub refresh_expires_at_ms: i64,
pub login_type: u8,
pub mfa_verified: bool,
pub scope: Option<String>,
pub controller_did: Option<String>,
pub app_password_name: Option<String>,
pub created_at_ms: i64,
pub updated_at_ms: i64,
}
impl SessionTokenValue {
pub fn serialize(&self) -> Vec<u8> {
let ttl_bytes = u64::try_from(self.refresh_expires_at_ms)
.unwrap_or(0)
.to_be_bytes();
let payload =
postcard::to_allocvec(self).expect("SessionTokenValue serialization cannot fail");
let mut buf = Vec::with_capacity(8 + 1 + payload.len());
buf.extend_from_slice(&ttl_bytes);
buf.push(SESSION_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let rest = bytes.get(8..)?;
let (&version, payload) = rest.split_first()?;
match version {
SESSION_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppPasswordValue {
pub id: uuid::Uuid,
pub user_id: uuid::Uuid,
pub name: String,
pub password_hash: String,
pub created_at_ms: i64,
pub privilege: u8,
pub scopes: Option<String>,
pub created_by_controller_did: Option<String>,
}
impl AppPasswordValue {
pub fn serialize(&self) -> Vec<u8> {
let payload =
postcard::to_allocvec(self).expect("AppPasswordValue serialization cannot fail");
let mut buf = Vec::with_capacity(8 + 1 + payload.len());
buf.extend_from_slice(&0u64.to_be_bytes());
buf.push(APP_PASSWORD_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let rest = bytes.get(8..)?;
let (&version, payload) = rest.split_first()?;
match version {
APP_PASSWORD_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SessionIndexValue {
pub user_hash: u64,
pub session_id: i32,
}
impl SessionIndexValue {
pub fn serialize(&self, expires_at_ms: i64) -> Vec<u8> {
let ttl_bytes = u64::try_from(expires_at_ms).unwrap_or(0).to_be_bytes();
let payload =
postcard::to_allocvec(self).expect("SessionIndexValue serialization cannot fail");
let mut buf = Vec::with_capacity(8 + 1 + payload.len());
buf.extend_from_slice(&ttl_bytes);
buf.push(SESSION_INDEX_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let rest = bytes.get(8..)?;
let (&version, payload) = rest.split_first()?;
match version {
SESSION_INDEX_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
pub fn login_type_to_u8(t: tranquil_db_traits::LoginType) -> u8 {
match t {
tranquil_db_traits::LoginType::Modern => 0,
tranquil_db_traits::LoginType::Legacy => 1,
}
}
pub fn u8_to_login_type(v: u8) -> Option<tranquil_db_traits::LoginType> {
match v {
0 => Some(tranquil_db_traits::LoginType::Modern),
1 => Some(tranquil_db_traits::LoginType::Legacy),
_ => None,
}
}
pub fn privilege_to_u8(p: tranquil_db_traits::AppPasswordPrivilege) -> u8 {
match p {
tranquil_db_traits::AppPasswordPrivilege::Standard => 0,
tranquil_db_traits::AppPasswordPrivilege::Privileged => 1,
}
}
pub fn u8_to_privilege(v: u8) -> Option<tranquil_db_traits::AppPasswordPrivilege> {
match v {
0 => Some(tranquil_db_traits::AppPasswordPrivilege::Standard),
1 => Some(tranquil_db_traits::AppPasswordPrivilege::Privileged),
_ => None,
}
}
pub fn session_primary_key(session_id: i32) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::SESSION_PRIMARY)
.raw(&session_id.to_be_bytes())
.build()
}
pub fn session_by_access_key(access_jti: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::SESSION_BY_ACCESS)
.string(access_jti)
.build()
}
pub fn session_by_refresh_key(refresh_jti: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::SESSION_BY_REFRESH)
.string(refresh_jti)
.build()
}
pub fn session_used_refresh_key(refresh_jti: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::SESSION_USED_REFRESH)
.string(refresh_jti)
.build()
}
pub fn session_app_password_key(user_hash: UserHash, name: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::SESSION_APP_PASSWORD)
.u64(user_hash.raw())
.string(name)
.build()
}
pub fn session_app_password_prefix(user_hash: UserHash) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::SESSION_APP_PASSWORD)
.u64(user_hash.raw())
.build()
}
pub fn session_by_did_key(user_hash: UserHash, session_id: i32) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::SESSION_BY_DID)
.u64(user_hash.raw())
.raw(&session_id.to_be_bytes())
.build()
}
pub fn session_by_did_prefix(user_hash: UserHash) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::SESSION_BY_DID)
.u64(user_hash.raw())
.build()
}
pub fn session_last_reauth_key(user_hash: UserHash) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::SESSION_LAST_REAUTH)
.u64(user_hash.raw())
.build()
}
pub fn session_id_counter_key() -> SmallVec<[u8; 128]> {
KeyBuilder::new().tag(KeyTag::SESSION_ID_COUNTER).build()
}
fn serialize_ttl_i32(expires_at_ms: i64, session_id: i32) -> Vec<u8> {
let mut buf = Vec::with_capacity(12);
buf.extend_from_slice(&u64::try_from(expires_at_ms).unwrap_or(0).to_be_bytes());
buf.extend_from_slice(&session_id.to_be_bytes());
buf
}
fn deserialize_ttl_i32(bytes: &[u8]) -> Option<i32> {
let rest = bytes.get(8..)?;
let arr: [u8; 4] = rest.try_into().ok()?;
Some(i32::from_be_bytes(arr))
}
pub fn serialize_used_refresh_value(expires_at_ms: i64, session_id: i32) -> Vec<u8> {
serialize_ttl_i32(expires_at_ms, session_id)
}
pub fn deserialize_used_refresh_value(bytes: &[u8]) -> Option<i32> {
deserialize_ttl_i32(bytes)
}
fn serialize_ttl_i64(timestamp_ms: i64) -> Vec<u8> {
let mut buf = Vec::with_capacity(16);
buf.extend_from_slice(&0u64.to_be_bytes());
buf.extend_from_slice(&timestamp_ms.to_be_bytes());
buf
}
fn deserialize_ttl_i64(bytes: &[u8]) -> Option<i64> {
let rest = bytes.get(8..)?;
let arr: [u8; 8] = rest.try_into().ok()?;
Some(i64::from_be_bytes(arr))
}
pub fn serialize_last_reauth_value(timestamp_ms: i64) -> Vec<u8> {
serialize_ttl_i64(timestamp_ms)
}
pub fn deserialize_last_reauth_value(bytes: &[u8]) -> Option<i64> {
deserialize_ttl_i64(bytes)
}
pub fn serialize_by_did_value(expires_at_ms: i64) -> Vec<u8> {
u64::try_from(expires_at_ms)
.unwrap_or(0)
.to_be_bytes()
.to_vec()
}
pub fn serialize_id_counter_value(counter: i32) -> Vec<u8> {
let mut buf = Vec::with_capacity(12);
buf.extend_from_slice(&0u64.to_be_bytes());
buf.extend_from_slice(&counter.to_be_bytes());
buf
}
pub fn deserialize_id_counter_value(bytes: &[u8]) -> Option<i32> {
deserialize_ttl_i32(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn session_token_value_roundtrip() {
let val = SessionTokenValue {
id: 42,
did: "did:plc:test".to_owned(),
access_jti: "access_abc".to_owned(),
refresh_jti: "refresh_xyz".to_owned(),
access_expires_at_ms: 1700000060000,
refresh_expires_at_ms: 1700000600000,
login_type: 0,
mfa_verified: true,
scope: Some("atproto".to_owned()),
controller_did: None,
app_password_name: None,
created_at_ms: 1700000000000,
updated_at_ms: 1700000000000,
};
let bytes = val.serialize();
let ttl = u64::from_be_bytes(bytes[..8].try_into().unwrap());
assert_eq!(ttl, u64::try_from(val.refresh_expires_at_ms).unwrap_or(0));
assert_eq!(bytes[8], SESSION_SCHEMA_VERSION);
let decoded = SessionTokenValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn app_password_value_roundtrip() {
let val = AppPasswordValue {
id: uuid::Uuid::new_v4(),
user_id: uuid::Uuid::new_v4(),
name: "test-app".to_owned(),
password_hash: "hashed".to_owned(),
created_at_ms: 1700000000000,
privilege: 0,
scopes: None,
created_by_controller_did: None,
};
let bytes = val.serialize();
let ttl = u64::from_be_bytes(bytes[..8].try_into().unwrap());
assert_eq!(ttl, 0);
assert_eq!(bytes[8], APP_PASSWORD_SCHEMA_VERSION);
let decoded = AppPasswordValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn session_index_value_roundtrip() {
let val = SessionIndexValue {
user_hash: 0xDEAD_BEEF,
session_id: 99,
};
let expires_at_ms = 1700000600000i64;
let bytes = val.serialize(expires_at_ms);
let ttl = u64::from_be_bytes(bytes[..8].try_into().unwrap());
assert_eq!(ttl, u64::try_from(expires_at_ms).unwrap_or(0));
let decoded = SessionIndexValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn used_refresh_value_roundtrip() {
let session_id = 7;
let expires_at_ms = 1700000600000i64;
let bytes = serialize_used_refresh_value(expires_at_ms, session_id);
let ttl = u64::from_be_bytes(bytes[..8].try_into().unwrap());
assert_eq!(ttl, u64::try_from(expires_at_ms).unwrap_or(0));
assert_eq!(deserialize_used_refresh_value(&bytes), Some(session_id));
}
#[test]
fn last_reauth_value_roundtrip() {
let ts = 1700000000000i64;
let bytes = serialize_last_reauth_value(ts);
let ttl = u64::from_be_bytes(bytes[..8].try_into().unwrap());
assert_eq!(ttl, 0);
assert_eq!(deserialize_last_reauth_value(&bytes), Some(ts));
}
#[test]
fn id_counter_value_roundtrip() {
let counter = 123;
let bytes = serialize_id_counter_value(counter);
let ttl = u64::from_be_bytes(bytes[..8].try_into().unwrap());
assert_eq!(ttl, 0);
assert_eq!(deserialize_id_counter_value(&bytes), Some(counter));
}
#[test]
fn session_primary_key_roundtrip() {
use super::super::encoding::KeyReader;
let key = session_primary_key(42);
let mut reader = KeyReader::new(&key);
assert_eq!(reader.tag(), Some(KeyTag::SESSION_PRIMARY.raw()));
let remaining = reader.remaining();
let arr: [u8; 4] = remaining.try_into().unwrap();
assert_eq!(i32::from_be_bytes(arr), 42);
}
#[test]
fn session_by_access_key_roundtrip() {
use super::super::encoding::KeyReader;
let key = session_by_access_key("jti_abc");
let mut reader = KeyReader::new(&key);
assert_eq!(reader.tag(), Some(KeyTag::SESSION_BY_ACCESS.raw()));
assert_eq!(reader.string(), Some("jti_abc".to_owned()));
assert!(reader.is_empty());
}
#[test]
fn session_by_did_prefix_is_prefix_of_key() {
let uh = UserHash::from_did("did:plc:test");
let prefix = session_by_did_prefix(uh);
let key = session_by_did_key(uh, 5);
assert!(key.starts_with(prefix.as_slice()));
}
#[test]
fn app_password_prefix_is_prefix_of_key() {
let uh = UserHash::from_did("did:plc:test");
let prefix = session_app_password_prefix(uh);
let key = session_app_password_key(uh, "my-app");
assert!(key.starts_with(prefix.as_slice()));
}
#[test]
fn deserialize_unknown_version_returns_none() {
let val = SessionTokenValue {
id: 1,
did: String::new(),
access_jti: String::new(),
refresh_jti: String::new(),
access_expires_at_ms: 0,
refresh_expires_at_ms: 0,
login_type: 0,
mfa_verified: false,
scope: None,
controller_did: None,
app_password_name: None,
created_at_ms: 0,
updated_at_ms: 0,
};
let mut bytes = val.serialize();
bytes[8] = 99;
assert!(SessionTokenValue::deserialize(&bytes).is_none());
}
#[test]
fn deserialize_too_short_returns_none() {
assert!(SessionTokenValue::deserialize(&[0; 8]).is_none());
assert!(SessionTokenValue::deserialize(&[0; 7]).is_none());
assert!(AppPasswordValue::deserialize(&[0; 8]).is_none());
assert!(SessionIndexValue::deserialize(&[0; 8]).is_none());
}
#[test]
fn login_type_conversion_roundtrip() {
assert_eq!(
u8_to_login_type(login_type_to_u8(tranquil_db_traits::LoginType::Modern)),
Some(tranquil_db_traits::LoginType::Modern)
);
assert_eq!(
u8_to_login_type(login_type_to_u8(tranquil_db_traits::LoginType::Legacy)),
Some(tranquil_db_traits::LoginType::Legacy)
);
assert_eq!(u8_to_login_type(99), None);
}
#[test]
fn privilege_conversion_roundtrip() {
assert_eq!(
u8_to_privilege(privilege_to_u8(
tranquil_db_traits::AppPasswordPrivilege::Standard
)),
Some(tranquil_db_traits::AppPasswordPrivilege::Standard)
);
assert_eq!(
u8_to_privilege(privilege_to_u8(
tranquil_db_traits::AppPasswordPrivilege::Privileged
)),
Some(tranquil_db_traits::AppPasswordPrivilege::Privileged)
);
assert_eq!(u8_to_privilege(99), None);
}
}

View File

@@ -0,0 +1,482 @@
use chrono::{DateTime, Utc};
use fjall::{Database, Keyspace};
use uuid::Uuid;
use super::MetastoreError;
use super::keys::UserHash;
use super::scan::point_lookup;
use super::sso_schema::{
ExternalIdentityValue, PendingRegistrationValue, SsoAuthStateValue, auth_state_key,
auth_state_prefix, by_id_key, by_provider_key, identity_key, identity_user_prefix,
pending_reg_key, pending_reg_prefix, provider_to_u8, u8_to_provider,
};
use tranquil_db_traits::{
ExternalEmail, ExternalIdentity, ExternalUserId, ExternalUsername, SsoAction, SsoAuthState,
SsoPendingRegistration, SsoProviderType,
};
use tranquil_types::Did;
pub struct SsoOps {
db: Database,
indexes: Keyspace,
}
impl SsoOps {
pub fn new(db: Database, indexes: Keyspace) -> Self {
Self { db, indexes }
}
fn value_to_identity(v: &ExternalIdentityValue) -> Result<ExternalIdentity, MetastoreError> {
Ok(ExternalIdentity {
id: v.id,
did: Did::new(v.did.clone())
.map_err(|_| MetastoreError::CorruptData("invalid did in sso identity"))?,
provider: u8_to_provider(v.provider)
.ok_or(MetastoreError::CorruptData("unknown sso provider"))?,
provider_user_id: ExternalUserId::new(v.provider_user_id.clone()),
provider_username: v
.provider_username
.as_ref()
.map(|s| ExternalUsername::new(s.clone())),
provider_email: v
.provider_email
.as_ref()
.map(|s| ExternalEmail::new(s.clone())),
created_at: DateTime::from_timestamp_millis(v.created_at_ms).unwrap_or_default(),
updated_at: DateTime::from_timestamp_millis(v.updated_at_ms).unwrap_or_default(),
last_login_at: v.last_login_at_ms.and_then(DateTime::from_timestamp_millis),
})
}
fn value_to_auth_state(v: &SsoAuthStateValue) -> Result<SsoAuthState, MetastoreError> {
Ok(SsoAuthState {
state: v.state.clone(),
request_uri: v.request_uri.clone(),
provider: u8_to_provider(v.provider)
.ok_or(MetastoreError::CorruptData("unknown sso provider"))?,
action: SsoAction::parse(&v.action)
.ok_or(MetastoreError::CorruptData("unknown sso action"))?,
nonce: v.nonce.clone(),
code_verifier: v.code_verifier.clone(),
did: v
.did
.as_ref()
.map(|d| Did::new(d.clone()))
.transpose()
.map_err(|_| MetastoreError::CorruptData("invalid did in sso auth state"))?,
created_at: DateTime::from_timestamp_millis(v.created_at_ms).unwrap_or_default(),
expires_at: DateTime::from_timestamp_millis(v.expires_at_ms).unwrap_or_default(),
})
}
fn value_to_pending_reg(
v: &PendingRegistrationValue,
) -> Result<SsoPendingRegistration, MetastoreError> {
Ok(SsoPendingRegistration {
token: v.token.clone(),
request_uri: v.request_uri.clone(),
provider: u8_to_provider(v.provider)
.ok_or(MetastoreError::CorruptData("unknown sso provider"))?,
provider_user_id: ExternalUserId::new(v.provider_user_id.clone()),
provider_username: v
.provider_username
.as_ref()
.map(|s| ExternalUsername::new(s.clone())),
provider_email: v
.provider_email
.as_ref()
.map(|s| ExternalEmail::new(s.clone())),
provider_email_verified: v.provider_email_verified,
created_at: DateTime::from_timestamp_millis(v.created_at_ms).unwrap_or_default(),
expires_at: DateTime::from_timestamp_millis(v.expires_at_ms).unwrap_or_default(),
})
}
pub fn create_external_identity(
&self,
did: &Did,
provider: SsoProviderType,
provider_user_id: &str,
provider_username: Option<&str>,
provider_email: Option<&str>,
) -> Result<Uuid, MetastoreError> {
let user_hash = UserHash::from_did(did.as_str());
let prov_u8 = provider_to_u8(provider);
let id = Uuid::new_v4();
let now_ms = Utc::now().timestamp_millis();
let value = ExternalIdentityValue {
id,
did: did.to_string(),
provider: prov_u8,
provider_user_id: provider_user_id.to_owned(),
provider_username: provider_username.map(str::to_owned),
provider_email: provider_email.map(str::to_owned),
created_at_ms: now_ms,
updated_at_ms: now_ms,
last_login_at_ms: None,
};
let primary = identity_key(user_hash, prov_u8, provider_user_id);
let provider_index = by_provider_key(prov_u8, provider_user_id);
let id_index = by_id_key(id);
let provider_index_val = {
let mut buf = Vec::with_capacity(8 + 16);
buf.extend_from_slice(&user_hash.raw().to_be_bytes());
buf.extend_from_slice(id.as_bytes());
buf
};
let id_index_val = {
let puid_bytes = provider_user_id.as_bytes();
let mut buf = Vec::with_capacity(8 + 1 + puid_bytes.len());
buf.extend_from_slice(&user_hash.raw().to_be_bytes());
buf.push(prov_u8);
buf.extend_from_slice(puid_bytes);
buf
};
let mut batch = self.db.batch();
batch.insert(&self.indexes, primary.as_slice(), value.serialize());
batch.insert(&self.indexes, provider_index.as_slice(), provider_index_val);
batch.insert(&self.indexes, id_index.as_slice(), id_index_val);
batch.commit().map_err(MetastoreError::Fjall)?;
Ok(id)
}
pub fn get_external_identity_by_provider(
&self,
provider: SsoProviderType,
provider_user_id: &str,
) -> Result<Option<ExternalIdentity>, MetastoreError> {
let prov_u8 = provider_to_u8(provider);
let index = by_provider_key(prov_u8, provider_user_id);
let index_val = match self
.indexes
.get(index.as_slice())
.map_err(MetastoreError::Fjall)?
{
Some(v) => v,
None => return Ok(None),
};
let (user_hash_raw, _id_bytes) = index_val.as_ref().split_at(8);
let user_hash =
UserHash::from_raw(u64::from_be_bytes(user_hash_raw.try_into().map_err(
|_| MetastoreError::CorruptData("corrupt sso by_provider index"),
)?));
let primary = identity_key(user_hash, prov_u8, provider_user_id);
let val: Option<ExternalIdentityValue> = point_lookup(
&self.indexes,
primary.as_slice(),
ExternalIdentityValue::deserialize,
"corrupt sso identity",
)?;
val.map(|v| Self::value_to_identity(&v)).transpose()
}
pub fn get_external_identities_by_did(
&self,
did: &Did,
) -> Result<Vec<ExternalIdentity>, MetastoreError> {
let user_hash = UserHash::from_did(did.as_str());
let prefix = identity_user_prefix(user_hash);
self.indexes
.prefix(prefix.as_slice())
.try_fold(Vec::new(), |mut acc, guard| {
let (_, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?;
let val = ExternalIdentityValue::deserialize(&val_bytes)
.ok_or(MetastoreError::CorruptData("corrupt sso identity"))?;
acc.push(Self::value_to_identity(&val)?);
Ok::<_, MetastoreError>(acc)
})
}
pub fn update_external_identity_login(
&self,
id: Uuid,
provider_username: Option<&str>,
provider_email: Option<&str>,
) -> Result<(), MetastoreError> {
let id_idx = by_id_key(id);
let id_val = match self
.indexes
.get(id_idx.as_slice())
.map_err(MetastoreError::Fjall)?
{
Some(v) => v,
None => return Ok(()),
};
let raw = id_val.as_ref();
if raw.len() < 9 {
return Err(MetastoreError::CorruptData("corrupt sso by_id index"));
}
let user_hash = UserHash::from_raw(u64::from_be_bytes(
raw[..8]
.try_into()
.map_err(|_| MetastoreError::CorruptData("corrupt sso by_id index"))?,
));
let provider = raw[8];
let provider_user_id = std::str::from_utf8(&raw[9..])
.map_err(|_| MetastoreError::CorruptData("corrupt sso by_id index"))?;
let primary = identity_key(user_hash, provider, provider_user_id);
let existing: Option<ExternalIdentityValue> = point_lookup(
&self.indexes,
primary.as_slice(),
ExternalIdentityValue::deserialize,
"corrupt sso identity",
)?;
match existing {
Some(mut val) => {
val.provider_username = provider_username.map(str::to_owned);
val.provider_email = provider_email.map(str::to_owned);
let now_ms = Utc::now().timestamp_millis();
val.last_login_at_ms = Some(now_ms);
val.updated_at_ms = now_ms;
self.indexes
.insert(primary.as_slice(), val.serialize())
.map_err(MetastoreError::Fjall)?;
Ok(())
}
None => Ok(()),
}
}
pub fn delete_external_identity(&self, id: Uuid, _did: &Did) -> Result<bool, MetastoreError> {
let id_idx = by_id_key(id);
let id_val = match self
.indexes
.get(id_idx.as_slice())
.map_err(MetastoreError::Fjall)?
{
Some(v) => v,
None => return Ok(false),
};
let raw = id_val.as_ref();
if raw.len() < 9 {
return Err(MetastoreError::CorruptData("corrupt sso by_id index"));
}
let user_hash = UserHash::from_raw(u64::from_be_bytes(
raw[..8]
.try_into()
.map_err(|_| MetastoreError::CorruptData("corrupt sso by_id index"))?,
));
let provider = raw[8];
let provider_user_id = std::str::from_utf8(&raw[9..])
.map_err(|_| MetastoreError::CorruptData("corrupt sso by_id index"))?;
let primary = identity_key(user_hash, provider, provider_user_id);
let provider_idx = by_provider_key(provider, provider_user_id);
let mut batch = self.db.batch();
batch.remove(&self.indexes, primary.as_slice());
batch.remove(&self.indexes, provider_idx.as_slice());
batch.remove(&self.indexes, id_idx.as_slice());
batch.commit().map_err(MetastoreError::Fjall)?;
Ok(true)
}
#[allow(clippy::too_many_arguments)]
pub fn create_sso_auth_state(
&self,
state: &str,
request_uri: &str,
provider: SsoProviderType,
action: SsoAction,
nonce: Option<&str>,
code_verifier: Option<&str>,
did: Option<&Did>,
) -> Result<(), MetastoreError> {
let now_ms = Utc::now().timestamp_millis();
let expires_at_ms = now_ms.saturating_add(600_000);
let value = SsoAuthStateValue {
state: state.to_owned(),
request_uri: request_uri.to_owned(),
provider: provider_to_u8(provider),
action: action.as_str().to_owned(),
nonce: nonce.map(str::to_owned),
code_verifier: code_verifier.map(str::to_owned),
did: did.map(|d| d.to_string()),
created_at_ms: now_ms,
expires_at_ms,
};
let key = auth_state_key(state);
self.indexes
.insert(key.as_slice(), value.serialize())
.map_err(MetastoreError::Fjall)?;
Ok(())
}
pub fn consume_sso_auth_state(
&self,
state: &str,
) -> Result<Option<SsoAuthState>, MetastoreError> {
let key = auth_state_key(state);
let val: Option<SsoAuthStateValue> = point_lookup(
&self.indexes,
key.as_slice(),
SsoAuthStateValue::deserialize,
"corrupt sso auth state",
)?;
match val {
Some(v) => {
self.indexes
.remove(key.as_slice())
.map_err(MetastoreError::Fjall)?;
let now_ms = Utc::now().timestamp_millis();
match v.expires_at_ms < now_ms {
true => Ok(None),
false => Self::value_to_auth_state(&v).map(Some),
}
}
None => Ok(None),
}
}
pub fn cleanup_expired_sso_auth_states(&self) -> Result<u64, MetastoreError> {
let prefix = auth_state_prefix();
let now_ms = Utc::now().timestamp_millis();
let mut count = 0u64;
let mut batch = self.db.batch();
self.indexes
.prefix(prefix.as_slice())
.try_for_each(|guard| {
let (key_bytes, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?;
let val = SsoAuthStateValue::deserialize(&val_bytes)
.ok_or(MetastoreError::CorruptData("corrupt sso auth state"))?;
if val.expires_at_ms < now_ms {
batch.remove(&self.indexes, key_bytes.as_ref());
count = count.saturating_add(1);
}
Ok::<_, MetastoreError>(())
})?;
batch.commit().map_err(MetastoreError::Fjall)?;
Ok(count)
}
#[allow(clippy::too_many_arguments)]
pub fn create_pending_registration(
&self,
token: &str,
request_uri: &str,
provider: SsoProviderType,
provider_user_id: &str,
provider_username: Option<&str>,
provider_email: Option<&str>,
provider_email_verified: bool,
) -> Result<(), MetastoreError> {
let now_ms = Utc::now().timestamp_millis();
let expires_at_ms = now_ms.saturating_add(600_000);
let value = PendingRegistrationValue {
token: token.to_owned(),
request_uri: request_uri.to_owned(),
provider: provider_to_u8(provider),
provider_user_id: provider_user_id.to_owned(),
provider_username: provider_username.map(str::to_owned),
provider_email: provider_email.map(str::to_owned),
provider_email_verified,
created_at_ms: now_ms,
expires_at_ms,
};
let key = pending_reg_key(token);
self.indexes
.insert(key.as_slice(), value.serialize())
.map_err(MetastoreError::Fjall)?;
Ok(())
}
pub fn get_pending_registration(
&self,
token: &str,
) -> Result<Option<SsoPendingRegistration>, MetastoreError> {
let key = pending_reg_key(token);
let val: Option<PendingRegistrationValue> = point_lookup(
&self.indexes,
key.as_slice(),
PendingRegistrationValue::deserialize,
"corrupt sso pending registration",
)?;
match val {
Some(v) => {
let now_ms = Utc::now().timestamp_millis();
match v.expires_at_ms < now_ms {
true => Ok(None),
false => Self::value_to_pending_reg(&v).map(Some),
}
}
None => Ok(None),
}
}
pub fn consume_pending_registration(
&self,
token: &str,
) -> Result<Option<SsoPendingRegistration>, MetastoreError> {
let key = pending_reg_key(token);
let val: Option<PendingRegistrationValue> = point_lookup(
&self.indexes,
key.as_slice(),
PendingRegistrationValue::deserialize,
"corrupt sso pending registration",
)?;
match val {
Some(v) => {
self.indexes
.remove(key.as_slice())
.map_err(MetastoreError::Fjall)?;
let now_ms = Utc::now().timestamp_millis();
match v.expires_at_ms < now_ms {
true => Ok(None),
false => Self::value_to_pending_reg(&v).map(Some),
}
}
None => Ok(None),
}
}
pub fn cleanup_expired_pending_registrations(&self) -> Result<u64, MetastoreError> {
let prefix = pending_reg_prefix();
let now_ms = Utc::now().timestamp_millis();
let mut count = 0u64;
let mut batch = self.db.batch();
self.indexes
.prefix(prefix.as_slice())
.try_for_each(|guard| {
let (key_bytes, val_bytes) = guard.into_inner().map_err(MetastoreError::Fjall)?;
let val = PendingRegistrationValue::deserialize(&val_bytes).ok_or(
MetastoreError::CorruptData("corrupt sso pending registration"),
)?;
if val.expires_at_ms < now_ms {
batch.remove(&self.indexes, key_bytes.as_ref());
count = count.saturating_add(1);
}
Ok::<_, MetastoreError>(())
})?;
batch.commit().map_err(MetastoreError::Fjall)?;
Ok(count)
}
}

View File

@@ -0,0 +1,227 @@
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use super::encoding::KeyBuilder;
use super::keys::{KeyTag, UserHash};
const IDENTITY_SCHEMA_VERSION: u8 = 1;
const AUTH_STATE_SCHEMA_VERSION: u8 = 1;
const PENDING_REG_SCHEMA_VERSION: u8 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExternalIdentityValue {
pub id: uuid::Uuid,
pub did: String,
pub provider: u8,
pub provider_user_id: String,
pub provider_username: Option<String>,
pub provider_email: Option<String>,
pub created_at_ms: i64,
pub updated_at_ms: i64,
pub last_login_at_ms: Option<i64>,
}
impl ExternalIdentityValue {
pub fn serialize(&self) -> Vec<u8> {
let payload =
postcard::to_allocvec(self).expect("ExternalIdentityValue serialization cannot fail");
let mut buf = Vec::with_capacity(1 + payload.len());
buf.push(IDENTITY_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let (&version, payload) = bytes.split_first()?;
match version {
IDENTITY_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SsoAuthStateValue {
pub state: String,
pub request_uri: String,
pub provider: u8,
pub action: String,
pub nonce: Option<String>,
pub code_verifier: Option<String>,
pub did: Option<String>,
pub created_at_ms: i64,
pub expires_at_ms: i64,
}
impl SsoAuthStateValue {
pub fn serialize(&self) -> Vec<u8> {
let payload =
postcard::to_allocvec(self).expect("SsoAuthStateValue serialization cannot fail");
let mut buf = Vec::with_capacity(1 + payload.len());
buf.push(AUTH_STATE_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let (&version, payload) = bytes.split_first()?;
match version {
AUTH_STATE_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PendingRegistrationValue {
pub token: String,
pub request_uri: String,
pub provider: u8,
pub provider_user_id: String,
pub provider_username: Option<String>,
pub provider_email: Option<String>,
pub provider_email_verified: bool,
pub created_at_ms: i64,
pub expires_at_ms: i64,
}
impl PendingRegistrationValue {
pub fn serialize(&self) -> Vec<u8> {
let payload = postcard::to_allocvec(self)
.expect("PendingRegistrationValue serialization cannot fail");
let mut buf = Vec::with_capacity(1 + payload.len());
buf.push(PENDING_REG_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let (&version, payload) = bytes.split_first()?;
match version {
PENDING_REG_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
pub fn provider_to_u8(p: tranquil_db_traits::SsoProviderType) -> u8 {
match p {
tranquil_db_traits::SsoProviderType::Github => 0,
tranquil_db_traits::SsoProviderType::Discord => 1,
tranquil_db_traits::SsoProviderType::Google => 2,
tranquil_db_traits::SsoProviderType::Gitlab => 3,
tranquil_db_traits::SsoProviderType::Oidc => 4,
tranquil_db_traits::SsoProviderType::Apple => 5,
}
}
pub fn u8_to_provider(v: u8) -> Option<tranquil_db_traits::SsoProviderType> {
match v {
0 => Some(tranquil_db_traits::SsoProviderType::Github),
1 => Some(tranquil_db_traits::SsoProviderType::Discord),
2 => Some(tranquil_db_traits::SsoProviderType::Google),
3 => Some(tranquil_db_traits::SsoProviderType::Gitlab),
4 => Some(tranquil_db_traits::SsoProviderType::Oidc),
5 => Some(tranquil_db_traits::SsoProviderType::Apple),
_ => None,
}
}
pub fn identity_key(
user_hash: UserHash,
provider: u8,
provider_user_id: &str,
) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::SSO_IDENTITY)
.u64(user_hash.raw())
.raw(&[provider])
.string(provider_user_id)
.build()
}
pub fn identity_user_prefix(user_hash: UserHash) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::SSO_IDENTITY)
.u64(user_hash.raw())
.build()
}
pub fn by_provider_key(provider: u8, provider_user_id: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::SSO_BY_PROVIDER)
.raw(&[provider])
.string(provider_user_id)
.build()
}
pub fn by_id_key(id: uuid::Uuid) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::SSO_BY_ID)
.fixed(id.as_bytes())
.build()
}
pub fn auth_state_key(state: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::SSO_AUTH_STATE)
.string(state)
.build()
}
pub fn auth_state_prefix() -> SmallVec<[u8; 128]> {
KeyBuilder::new().tag(KeyTag::SSO_AUTH_STATE).build()
}
pub fn pending_reg_key(token: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::SSO_PENDING_REG)
.string(token)
.build()
}
pub fn pending_reg_prefix() -> SmallVec<[u8; 128]> {
KeyBuilder::new().tag(KeyTag::SSO_PENDING_REG).build()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identity_value_roundtrip() {
let val = ExternalIdentityValue {
id: uuid::Uuid::new_v4(),
did: "did:plc:test".to_owned(),
provider: 0,
provider_user_id: "12345".to_owned(),
provider_username: Some("user".to_owned()),
provider_email: Some("user@example.com".to_owned()),
created_at_ms: 1700000000000,
updated_at_ms: 1700000000000,
last_login_at_ms: None,
};
let bytes = val.serialize();
assert_eq!(bytes[0], IDENTITY_SCHEMA_VERSION);
let decoded = ExternalIdentityValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn auth_state_value_roundtrip() {
let val = SsoAuthStateValue {
state: "random-state".to_owned(),
request_uri: "urn:ietf:params:oauth:request_uri:abc".to_owned(),
provider: 0,
action: "login".to_owned(),
nonce: None,
code_verifier: None,
did: None,
created_at_ms: 1700000000000,
expires_at_ms: 1700000600000,
};
let bytes = val.serialize();
let decoded = SsoAuthStateValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,829 @@
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use super::encoding::KeyBuilder;
use super::keys::{KeyTag, UserHash};
const USER_SCHEMA_VERSION: u8 = 1;
const PASSKEY_SCHEMA_VERSION: u8 = 1;
const TOTP_SCHEMA_VERSION: u8 = 1;
const BACKUP_CODE_SCHEMA_VERSION: u8 = 1;
const WEBAUTHN_CHALLENGE_SCHEMA_VERSION: u8 = 1;
const RESET_CODE_SCHEMA_VERSION: u8 = 1;
const RECOVERY_TOKEN_SCHEMA_VERSION: u8 = 1;
const DID_WEB_OVERRIDES_SCHEMA_VERSION: u8 = 1;
const HANDLE_RESERVATION_SCHEMA_VERSION: u8 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UserValue {
pub id: uuid::Uuid,
pub did: String,
pub handle: String,
pub email: Option<String>,
pub email_verified: bool,
pub password_hash: Option<String>,
pub created_at_ms: i64,
pub deactivated_at_ms: Option<i64>,
pub takedown_ref: Option<String>,
pub is_admin: bool,
pub preferred_comms_channel: Option<u8>,
pub key_bytes: Vec<u8>,
pub encryption_version: i32,
pub account_type: u8,
pub password_required: bool,
pub two_factor_enabled: bool,
pub email_2fa_enabled: bool,
pub totp_enabled: bool,
pub allow_legacy_login: bool,
pub preferred_locale: Option<String>,
pub invites_disabled: bool,
pub migrated_to_pds: Option<String>,
pub migrated_at_ms: Option<i64>,
pub discord_username: Option<String>,
pub discord_id: Option<String>,
pub discord_verified: bool,
pub telegram_username: Option<String>,
pub telegram_chat_id: Option<i64>,
pub telegram_verified: bool,
pub signal_username: Option<String>,
pub signal_verified: bool,
pub delete_after_ms: Option<i64>,
}
impl UserValue {
pub fn serialize(&self) -> Vec<u8> {
let payload = postcard::to_allocvec(self).expect("UserValue serialization cannot fail");
let mut buf = Vec::with_capacity(1 + payload.len());
buf.push(USER_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let (&version, payload) = bytes.split_first()?;
match version {
USER_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
pub fn is_active(&self) -> bool {
self.deactivated_at_ms.is_none() && self.takedown_ref.is_none()
}
pub fn channel_verification(&self) -> u8 {
let mut flags = 0u8;
if self.email_verified {
flags |= 1;
}
if self.discord_verified {
flags |= 2;
}
if self.telegram_verified {
flags |= 4;
}
if self.signal_verified {
flags |= 8;
}
flags
}
}
pub fn account_type_to_u8(t: tranquil_db_traits::AccountType) -> u8 {
match t {
tranquil_db_traits::AccountType::Personal => 0,
tranquil_db_traits::AccountType::Delegated => 1,
}
}
pub fn u8_to_account_type(v: u8) -> Option<tranquil_db_traits::AccountType> {
match v {
0 => Some(tranquil_db_traits::AccountType::Personal),
1 => Some(tranquil_db_traits::AccountType::Delegated),
_ => None,
}
}
pub fn challenge_type_to_u8(t: tranquil_db_traits::WebauthnChallengeType) -> u8 {
match t {
tranquil_db_traits::WebauthnChallengeType::Registration => 0,
tranquil_db_traits::WebauthnChallengeType::Authentication => 1,
}
}
pub fn u8_to_challenge_type(v: u8) -> Option<tranquil_db_traits::WebauthnChallengeType> {
match v {
0 => Some(tranquil_db_traits::WebauthnChallengeType::Registration),
1 => Some(tranquil_db_traits::WebauthnChallengeType::Authentication),
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PasskeyValue {
pub id: uuid::Uuid,
pub did: String,
pub credential_id: Vec<u8>,
pub public_key: Vec<u8>,
pub sign_count: i32,
pub created_at_ms: i64,
pub last_used_at_ms: Option<i64>,
pub friendly_name: Option<String>,
pub aaguid: Option<Vec<u8>>,
pub transports: Option<Vec<String>>,
}
impl PasskeyValue {
pub fn serialize(&self) -> Vec<u8> {
let payload = postcard::to_allocvec(self).expect("PasskeyValue serialization cannot fail");
let mut buf = Vec::with_capacity(1 + payload.len());
buf.push(PASSKEY_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let (&version, payload) = bytes.split_first()?;
match version {
PASSKEY_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TotpValue {
pub secret_encrypted: Vec<u8>,
pub encryption_version: i32,
pub verified: bool,
pub last_used_at_ms: Option<i64>,
}
impl TotpValue {
pub fn serialize(&self) -> Vec<u8> {
let payload = postcard::to_allocvec(self).expect("TotpValue serialization cannot fail");
let mut buf = Vec::with_capacity(1 + payload.len());
buf.push(TOTP_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let (&version, payload) = bytes.split_first()?;
match version {
TOTP_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BackupCodeValue {
pub id: uuid::Uuid,
pub code_hash: String,
pub used: bool,
}
impl BackupCodeValue {
pub fn serialize(&self) -> Vec<u8> {
let payload =
postcard::to_allocvec(self).expect("BackupCodeValue serialization cannot fail");
let mut buf = Vec::with_capacity(1 + payload.len());
buf.push(BACKUP_CODE_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let (&version, payload) = bytes.split_first()?;
match version {
BACKUP_CODE_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WebauthnChallengeValue {
pub id: uuid::Uuid,
pub challenge_type: u8,
pub state_json: String,
pub created_at_ms: i64,
}
impl WebauthnChallengeValue {
pub fn serialize_with_ttl(&self) -> Vec<u8> {
let expires_at_ms = self.created_at_ms.saturating_add(300_000);
let ttl_bytes = u64::try_from(expires_at_ms).unwrap_or(0).to_be_bytes();
let payload =
postcard::to_allocvec(self).expect("WebauthnChallengeValue serialization cannot fail");
let mut buf = Vec::with_capacity(8 + 1 + payload.len());
buf.extend_from_slice(&ttl_bytes);
buf.push(WEBAUTHN_CHALLENGE_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let rest = bytes.get(8..)?;
let (&version, payload) = rest.split_first()?;
match version {
WEBAUTHN_CHALLENGE_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResetCodeValue {
pub user_hash: u64,
pub user_id: uuid::Uuid,
pub preferred_comms_channel: Option<u8>,
pub code: String,
pub expires_at_ms: i64,
}
impl ResetCodeValue {
pub fn serialize_with_ttl(&self) -> Vec<u8> {
let ttl_bytes = u64::try_from(self.expires_at_ms).unwrap_or(0).to_be_bytes();
let payload =
postcard::to_allocvec(self).expect("ResetCodeValue serialization cannot fail");
let mut buf = Vec::with_capacity(8 + 1 + payload.len());
buf.extend_from_slice(&ttl_bytes);
buf.push(RESET_CODE_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let rest = bytes.get(8..)?;
let (&version, payload) = rest.split_first()?;
match version {
RESET_CODE_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecoveryTokenValue {
pub token_hash: String,
pub expires_at_ms: i64,
}
impl RecoveryTokenValue {
pub fn serialize(&self) -> Vec<u8> {
let payload =
postcard::to_allocvec(self).expect("RecoveryTokenValue serialization cannot fail");
let mut buf = Vec::with_capacity(1 + payload.len());
buf.push(RECOVERY_TOKEN_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let (&version, payload) = bytes.split_first()?;
match version {
RECOVERY_TOKEN_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DidWebOverridesValue {
pub verification_methods_json: Option<String>,
pub also_known_as: Option<Vec<String>>,
}
impl DidWebOverridesValue {
pub fn serialize(&self) -> Vec<u8> {
let payload =
postcard::to_allocvec(self).expect("DidWebOverridesValue serialization cannot fail");
let mut buf = Vec::with_capacity(1 + payload.len());
buf.push(DID_WEB_OVERRIDES_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let (&version, payload) = bytes.split_first()?;
match version {
DID_WEB_OVERRIDES_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HandleReservationValue {
pub reserved_by: String,
pub created_at_ms: i64,
pub expires_at_ms: i64,
}
impl HandleReservationValue {
pub fn serialize_with_ttl(&self) -> Vec<u8> {
let ttl_bytes = u64::try_from(self.expires_at_ms).unwrap_or(0).to_be_bytes();
let payload =
postcard::to_allocvec(self).expect("HandleReservationValue serialization cannot fail");
let mut buf = Vec::with_capacity(8 + 1 + payload.len());
buf.extend_from_slice(&ttl_bytes);
buf.push(HANDLE_RESERVATION_SCHEMA_VERSION);
buf.extend_from_slice(&payload);
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
let rest = bytes.get(8..)?;
let (&version, payload) = rest.split_first()?;
match version {
HANDLE_RESERVATION_SCHEMA_VERSION => postcard::from_bytes(payload).ok(),
_ => None,
}
}
}
pub fn user_primary_key(user_hash: UserHash) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::USER_PRIMARY)
.u64(user_hash.raw())
.build()
}
pub fn user_primary_prefix() -> SmallVec<[u8; 128]> {
KeyBuilder::new().tag(KeyTag::USER_PRIMARY).build()
}
pub fn user_by_handle_key(handle: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::USER_BY_HANDLE)
.string(handle)
.build()
}
pub fn user_by_email_key(email: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::USER_BY_EMAIL)
.string(email)
.build()
}
pub fn passkey_key(user_hash: UserHash, passkey_id: uuid::Uuid) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::USER_PASSKEYS)
.u64(user_hash.raw())
.bytes(passkey_id.as_bytes())
.build()
}
pub fn passkey_prefix(user_hash: UserHash) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::USER_PASSKEYS)
.u64(user_hash.raw())
.build()
}
pub fn passkey_by_cred_key(credential_id: &[u8]) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::USER_PASSKEY_BY_CRED)
.bytes(credential_id)
.build()
}
pub fn totp_key(user_hash: UserHash) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::USER_TOTP)
.u64(user_hash.raw())
.build()
}
pub fn backup_code_key(user_hash: UserHash, code_id: uuid::Uuid) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::USER_BACKUP_CODES)
.u64(user_hash.raw())
.bytes(code_id.as_bytes())
.build()
}
pub fn backup_code_prefix(user_hash: UserHash) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::USER_BACKUP_CODES)
.u64(user_hash.raw())
.build()
}
pub fn webauthn_challenge_key(user_hash: UserHash, challenge_type: u8) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::USER_WEBAUTHN_CHALLENGE)
.u64(user_hash.raw())
.raw(&[challenge_type])
.build()
}
pub fn reset_code_key(code: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::USER_RESET_CODE)
.string(code)
.build()
}
pub fn recovery_token_key(user_hash: UserHash) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::USER_RECOVERY_TOKEN)
.u64(user_hash.raw())
.build()
}
pub fn did_web_overrides_key(user_hash: UserHash) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::USER_DID_WEB_OVERRIDES)
.u64(user_hash.raw())
.build()
}
pub fn handle_reservation_key(handle: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::USER_HANDLE_RESERVATION)
.string(handle)
.build()
}
pub fn handle_reservation_prefix() -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::USER_HANDLE_RESERVATION)
.build()
}
pub fn telegram_lookup_key(telegram_username: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::USER_TELEGRAM_LOOKUP)
.string(telegram_username)
.build()
}
pub fn discord_lookup_key(discord_username: &str) -> SmallVec<[u8; 128]> {
KeyBuilder::new()
.tag(KeyTag::USER_DISCORD_LOOKUP)
.string(discord_username)
.build()
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PasskeyIndexValue {
pub user_hash: u64,
pub passkey_id: uuid::Uuid,
}
impl PasskeyIndexValue {
pub fn serialize(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(24);
buf.extend_from_slice(&self.user_hash.to_be_bytes());
buf.extend_from_slice(self.passkey_id.as_bytes());
buf
}
pub fn deserialize(bytes: &[u8]) -> Option<Self> {
(bytes.len() == 24).then(|| {
let user_hash = u64::from_be_bytes(bytes[..8].try_into().unwrap());
let passkey_id = uuid::Uuid::from_slice(&bytes[8..24]).unwrap();
Self {
user_hash,
passkey_id,
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn user_value_roundtrip() {
let val = UserValue {
id: uuid::Uuid::new_v4(),
did: "did:plc:test".to_owned(),
handle: "test.example.com".to_owned(),
email: Some("test@example.com".to_owned()),
email_verified: true,
password_hash: Some("hashed".to_owned()),
created_at_ms: 1700000000000,
deactivated_at_ms: None,
takedown_ref: None,
is_admin: false,
preferred_comms_channel: None,
key_bytes: vec![1, 2, 3],
encryption_version: 1,
account_type: 0,
password_required: true,
two_factor_enabled: false,
email_2fa_enabled: false,
totp_enabled: false,
allow_legacy_login: false,
preferred_locale: None,
invites_disabled: false,
migrated_to_pds: None,
migrated_at_ms: None,
discord_username: None,
discord_id: None,
discord_verified: false,
telegram_username: None,
telegram_chat_id: None,
telegram_verified: false,
signal_username: None,
signal_verified: false,
delete_after_ms: None,
};
let bytes = val.serialize();
assert_eq!(bytes[0], USER_SCHEMA_VERSION);
let decoded = UserValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn passkey_value_roundtrip() {
let val = PasskeyValue {
id: uuid::Uuid::new_v4(),
did: "did:plc:test".to_owned(),
credential_id: vec![1, 2, 3, 4],
public_key: vec![5, 6, 7, 8],
sign_count: 42,
created_at_ms: 1700000000000,
last_used_at_ms: None,
friendly_name: Some("my key".to_owned()),
aaguid: None,
transports: Some(vec!["usb".to_owned()]),
};
let bytes = val.serialize();
assert_eq!(bytes[0], PASSKEY_SCHEMA_VERSION);
let decoded = PasskeyValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn totp_value_roundtrip() {
let val = TotpValue {
secret_encrypted: vec![10, 20, 30],
encryption_version: 1,
verified: true,
last_used_at_ms: Some(1700000000000),
};
let bytes = val.serialize();
let decoded = TotpValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn backup_code_value_roundtrip() {
let val = BackupCodeValue {
id: uuid::Uuid::new_v4(),
code_hash: "hash123".to_owned(),
used: false,
};
let bytes = val.serialize();
let decoded = BackupCodeValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn webauthn_challenge_value_roundtrip() {
let val = WebauthnChallengeValue {
id: uuid::Uuid::new_v4(),
challenge_type: 0,
state_json: r#"{"challenge":"abc"}"#.to_owned(),
created_at_ms: 1700000000000,
};
let bytes = val.serialize_with_ttl();
let ttl = u64::from_be_bytes(bytes[..8].try_into().unwrap());
assert_eq!(ttl, 1700000300000);
let decoded = WebauthnChallengeValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn reset_code_value_roundtrip() {
let val = ResetCodeValue {
user_hash: 0xDEAD,
user_id: uuid::Uuid::new_v4(),
preferred_comms_channel: Some(0),
code: "abc123".to_owned(),
expires_at_ms: 1700000600000,
};
let bytes = val.serialize_with_ttl();
let ttl = u64::from_be_bytes(bytes[..8].try_into().unwrap());
assert_eq!(ttl, 1700000600000);
let decoded = ResetCodeValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn recovery_token_value_roundtrip() {
let val = RecoveryTokenValue {
token_hash: "tokenhash".to_owned(),
expires_at_ms: 1700000600000,
};
let bytes = val.serialize();
let decoded = RecoveryTokenValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn did_web_overrides_value_roundtrip() {
let val = DidWebOverridesValue {
verification_methods_json: Some(r#"[{"id":"key-1"}]"#.to_owned()),
also_known_as: Some(vec!["at://user.example.com".to_owned()]),
};
let bytes = val.serialize();
let decoded = DidWebOverridesValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn handle_reservation_value_roundtrip() {
let val = HandleReservationValue {
reserved_by: "signup-flow".to_owned(),
created_at_ms: 1700000000000,
expires_at_ms: 1700000600000,
};
let bytes = val.serialize_with_ttl();
let ttl = u64::from_be_bytes(bytes[..8].try_into().unwrap());
assert_eq!(ttl, 1700000600000);
let decoded = HandleReservationValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn passkey_index_value_roundtrip() {
let id = uuid::Uuid::new_v4();
let val = PasskeyIndexValue {
user_hash: 0xCAFE_BABE,
passkey_id: id,
};
let bytes = val.serialize();
let decoded = PasskeyIndexValue::deserialize(&bytes).unwrap();
assert_eq!(val, decoded);
}
#[test]
fn key_functions_produce_distinct_prefixes() {
let uh = UserHash::from_did("did:plc:test");
let id = uuid::Uuid::new_v4();
let keys: Vec<SmallVec<[u8; 128]>> = vec![
user_primary_key(uh),
user_by_handle_key("test.handle"),
user_by_email_key("test@email.com"),
passkey_key(uh, id),
passkey_by_cred_key(&[1, 2, 3]),
totp_key(uh),
backup_code_key(uh, id),
webauthn_challenge_key(uh, 0),
reset_code_key("code123"),
recovery_token_key(uh),
did_web_overrides_key(uh),
handle_reservation_key("reserved.handle"),
telegram_lookup_key("tg_user"),
discord_lookup_key("dc_user"),
];
let tags: Vec<u8> = keys.iter().map(|k| k[0]).collect();
let mut unique_tags = tags.clone();
unique_tags.sort();
unique_tags.dedup();
assert!(unique_tags.len() >= 12);
}
#[test]
fn passkey_prefix_is_prefix_of_key() {
let uh = UserHash::from_did("did:plc:test");
let prefix = passkey_prefix(uh);
let key = passkey_key(uh, uuid::Uuid::new_v4());
assert!(key.starts_with(prefix.as_slice()));
}
#[test]
fn backup_code_prefix_is_prefix_of_key() {
let uh = UserHash::from_did("did:plc:test");
let prefix = backup_code_prefix(uh);
let key = backup_code_key(uh, uuid::Uuid::new_v4());
assert!(key.starts_with(prefix.as_slice()));
}
#[test]
fn account_type_roundtrip() {
assert_eq!(
u8_to_account_type(account_type_to_u8(
tranquil_db_traits::AccountType::Personal
)),
Some(tranquil_db_traits::AccountType::Personal)
);
assert_eq!(
u8_to_account_type(account_type_to_u8(
tranquil_db_traits::AccountType::Delegated
)),
Some(tranquil_db_traits::AccountType::Delegated)
);
assert_eq!(u8_to_account_type(99), None);
}
#[test]
fn challenge_type_roundtrip() {
use tranquil_db_traits::WebauthnChallengeType;
assert_eq!(
u8_to_challenge_type(challenge_type_to_u8(WebauthnChallengeType::Registration)),
Some(WebauthnChallengeType::Registration)
);
assert_eq!(
u8_to_challenge_type(challenge_type_to_u8(WebauthnChallengeType::Authentication)),
Some(WebauthnChallengeType::Authentication)
);
assert_eq!(u8_to_challenge_type(99), None);
}
#[test]
fn channel_verification_flags() {
let mut user = UserValue {
id: uuid::Uuid::new_v4(),
did: "did:plc:test".to_owned(),
handle: "t.invalid".to_owned(),
email: None,
email_verified: false,
password_hash: None,
created_at_ms: 0,
deactivated_at_ms: None,
takedown_ref: None,
is_admin: false,
preferred_comms_channel: None,
key_bytes: vec![],
encryption_version: 1,
account_type: 0,
password_required: false,
two_factor_enabled: false,
email_2fa_enabled: false,
totp_enabled: false,
allow_legacy_login: false,
preferred_locale: None,
invites_disabled: false,
migrated_to_pds: None,
migrated_at_ms: None,
discord_username: None,
discord_id: None,
discord_verified: false,
telegram_username: None,
telegram_chat_id: None,
telegram_verified: false,
signal_username: None,
signal_verified: false,
delete_after_ms: None,
};
assert_eq!(user.channel_verification(), 0);
user.email_verified = true;
assert_eq!(user.channel_verification(), 1);
user.discord_verified = true;
assert_eq!(user.channel_verification(), 3);
user.telegram_verified = true;
assert_eq!(user.channel_verification(), 7);
user.signal_verified = true;
assert_eq!(user.channel_verification(), 15);
}
#[test]
fn deserialize_unknown_version_returns_none() {
let val = UserValue {
id: uuid::Uuid::new_v4(),
did: String::new(),
handle: String::new(),
email: None,
email_verified: false,
password_hash: None,
created_at_ms: 0,
deactivated_at_ms: None,
takedown_ref: None,
is_admin: false,
preferred_comms_channel: None,
key_bytes: vec![],
encryption_version: 0,
account_type: 0,
password_required: false,
two_factor_enabled: false,
email_2fa_enabled: false,
totp_enabled: false,
allow_legacy_login: false,
preferred_locale: None,
invites_disabled: false,
migrated_to_pds: None,
migrated_at_ms: None,
discord_username: None,
discord_id: None,
discord_verified: false,
telegram_username: None,
telegram_chat_id: None,
telegram_verified: false,
signal_username: None,
signal_verified: false,
delete_after_ms: None,
};
let mut bytes = val.serialize();
bytes[0] = 99;
assert!(UserValue::deserialize(&bytes).is_none());
}
}