mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-04-22 01:10:31 +00:00
feat(tranquil-store): repository traits on MetastoreClient
Lewis: May this revision serve well! <lu5a@proton.me>
This commit is contained in:
22
Cargo.lock
generated
22
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
412
crates/tranquil-store/src/metastore/delegation_ops.rs
Normal file
412
crates/tranquil-store/src/metastore/delegation_ops.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
201
crates/tranquil-store/src/metastore/delegations.rs
Normal file
201
crates/tranquil-store/src/metastore/delegations.rs
Normal 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
1209
crates/tranquil-store/src/metastore/infra_ops.rs
Normal file
1209
crates/tranquil-store/src/metastore/infra_ops.rs
Normal file
File diff suppressed because it is too large
Load Diff
730
crates/tranquil-store/src/metastore/infra_schema.rs
Normal file
730
crates/tranquil-store/src/metastore/infra_schema.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
1602
crates/tranquil-store/src/metastore/oauth_ops.rs
Normal file
1602
crates/tranquil-store/src/metastore/oauth_ops.rs
Normal file
File diff suppressed because it is too large
Load Diff
556
crates/tranquil-store/src/metastore/oauth_schema.rs
Normal file
556
crates/tranquil-store/src/metastore/oauth_schema.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
919
crates/tranquil-store/src/metastore/session_ops.rs
Normal file
919
crates/tranquil-store/src/metastore/session_ops.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
459
crates/tranquil-store/src/metastore/sessions.rs
Normal file
459
crates/tranquil-store/src/metastore/sessions.rs
Normal 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(×tamp_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);
|
||||
}
|
||||
}
|
||||
482
crates/tranquil-store/src/metastore/sso_ops.rs
Normal file
482
crates/tranquil-store/src/metastore/sso_ops.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
227
crates/tranquil-store/src/metastore/sso_schema.rs
Normal file
227
crates/tranquil-store/src/metastore/sso_schema.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
3099
crates/tranquil-store/src/metastore/user_ops.rs
Normal file
3099
crates/tranquil-store/src/metastore/user_ops.rs
Normal file
File diff suppressed because it is too large
Load Diff
829
crates/tranquil-store/src/metastore/users.rs
Normal file
829
crates/tranquil-store/src/metastore/users.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user