use chrono::{DateTime, Utc};
use thiserror::Error;
use uuid::Uuid;
#[derive(Debug, Error)]
pub enum StoreError {
#[error("not found")]
NotFound,
#[error("already exists")]
AlreadyExists,
#[error("conflict")]
Conflict,
#[error("backend error: {0}")]
Backend(String),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct UserId(pub Uuid);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct PrincipalId(pub Uuid);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct InviteId(pub Uuid);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct WorkspaceId(pub Uuid);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ProjectId(pub Uuid);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ProjectName(pub String);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct EnvironmentId(pub Uuid);
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct EnvName(pub String);
#[derive(Clone, Debug)]
pub struct SecretRow {
pub nonce: Vec<u8>, pub ciphertext: Vec<u8>, }
#[derive(Clone, Debug)]
pub struct CreateUserParams {
pub email: String,
pub principal: Option<CreatePrincipalData>,
pub workspace_ids: Vec<WorkspaceId>,
}
#[derive(Clone, Debug)]
pub struct CreatePrincipalData {
pub name: String,
pub public_key: Vec<u8>, pub x25519_public_key: Option<Vec<u8>>, pub is_service: bool, }
#[derive(Clone, Debug)]
pub struct CreatePrincipalParams {
pub user_id: Option<UserId>, pub name: String,
pub public_key: Vec<u8>, pub x25519_public_key: Option<Vec<u8>>, }
#[derive(Clone, Debug)]
pub struct CreateWorkspaceParams {
pub id: WorkspaceId, pub name: String,
pub owner_user_id: UserId,
pub kdf_salt: Vec<u8>, pub m_cost_kib: u32, pub t_cost: u32, pub p_cost: u32, }
#[derive(Clone, Debug)]
pub struct CreateInviteParams {
pub workspace_ids: Vec<WorkspaceId>,
pub token: String, pub kek_encrypted: Option<Vec<u8>>, pub kek_nonce: Option<Vec<u8>>, pub expires_at: DateTime<Utc>,
pub created_by_user_id: Option<UserId>, }
#[derive(Clone, Debug)]
pub struct CreateProjectParams {
pub workspace_id: WorkspaceId,
pub name: String,
}
#[derive(Clone, Debug)]
pub struct CreateEnvParams {
pub project_id: ProjectId,
pub name: String,
pub dek_wrapped: Vec<u8>, pub dek_nonce: Vec<u8>, }
#[derive(Clone, Debug)]
pub struct User {
pub id: UserId,
pub email: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Clone, Debug)]
pub struct Principal {
pub id: PrincipalId,
pub user_id: Option<UserId>, pub name: String,
pub public_key: Vec<u8>, pub x25519_public_key: Option<Vec<u8>>, pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Clone, Debug)]
pub struct Invite {
pub id: InviteId,
pub token: String,
pub workspace_ids: Vec<WorkspaceId>,
pub kek_encrypted: Option<Vec<u8>>, pub kek_nonce: Option<Vec<u8>>, pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub created_by_user_id: Option<UserId>, }
#[derive(Clone, Debug)]
pub struct Workspace {
pub id: WorkspaceId,
pub name: String,
pub owner_user_id: UserId,
pub kdf_salt: Vec<u8>,
pub m_cost_kib: u32,
pub t_cost: u32,
pub p_cost: u32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Clone, Debug)]
pub struct WorkspacePrincipal {
pub workspace_id: WorkspaceId,
pub principal_id: PrincipalId,
pub ephemeral_pub: Vec<u8>, pub kek_wrapped: Vec<u8>, pub kek_nonce: Vec<u8>, pub created_at: DateTime<Utc>,
}
#[derive(Clone, Debug)]
pub struct AddWorkspacePrincipalParams {
pub workspace_id: WorkspaceId,
pub principal_id: PrincipalId,
pub ephemeral_pub: Vec<u8>,
pub kek_wrapped: Vec<u8>,
pub kek_nonce: Vec<u8>,
}
#[derive(Clone, Debug)]
pub struct Project {
pub id: ProjectId,
pub workspace_id: WorkspaceId,
pub name: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Clone, Debug)]
pub struct Environment {
pub id: EnvironmentId,
pub project_id: ProjectId,
pub name: String,
pub dek_wrapped: Vec<u8>,
pub dek_nonce: Vec<u8>,
pub version: i64, pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[async_trait::async_trait]
pub trait Store {
async fn create_user(
&self,
params: &CreateUserParams,
) -> Result<(UserId, Option<PrincipalId>), StoreError>;
async fn get_user_by_email(&self, email: &str) -> Result<User, StoreError>;
async fn get_user_by_id(&self, user_id: &UserId) -> Result<User, StoreError>;
async fn create_principal(
&self,
params: &CreatePrincipalParams,
) -> Result<PrincipalId, StoreError>;
async fn get_principal(&self, principal_id: &PrincipalId) -> Result<Principal, StoreError>;
async fn rename_principal(
&self,
principal_id: &PrincipalId,
new_name: &str,
) -> Result<(), StoreError>;
async fn list_principals(&self, user_id: &UserId) -> Result<Vec<Principal>, StoreError>;
async fn create_invite(&self, params: &CreateInviteParams) -> Result<Invite, StoreError>;
async fn get_invite_by_token(&self, token: &str) -> Result<Invite, StoreError>;
async fn list_invites(&self, user_id: Option<&UserId>) -> Result<Vec<Invite>, StoreError>;
async fn revoke_invite(&self, invite_id: &InviteId) -> Result<(), StoreError>;
async fn create_workspace(
&self,
params: &CreateWorkspaceParams,
) -> Result<WorkspaceId, StoreError>;
async fn list_workspaces(&self, user_id: &UserId) -> Result<Vec<Workspace>, StoreError>;
async fn get_workspace(&self, ws: &WorkspaceId) -> Result<Workspace, StoreError>;
async fn get_workspace_by_name(
&self,
user_id: &UserId,
name: &str,
) -> Result<Workspace, StoreError>;
async fn get_workspace_by_name_for_principal(
&self,
principal_id: &PrincipalId,
name: &str,
) -> Result<Workspace, StoreError>;
async fn add_workspace_principal(
&self,
params: &AddWorkspacePrincipalParams,
) -> Result<(), StoreError>;
async fn get_workspace_principal(
&self,
workspace_id: &WorkspaceId,
principal_id: &PrincipalId,
) -> Result<WorkspacePrincipal, StoreError>;
async fn list_workspace_principals(
&self,
workspace_id: &WorkspaceId,
) -> Result<Vec<WorkspacePrincipal>, StoreError>;
async fn add_user_to_workspace(
&self,
workspace_id: &WorkspaceId,
user_id: &UserId,
) -> Result<(), StoreError>;
async fn create_project(&self, params: &CreateProjectParams) -> Result<ProjectId, StoreError>;
async fn list_projects(&self, workspace_id: &WorkspaceId) -> Result<Vec<Project>, StoreError>;
async fn get_project(&self, project_id: &ProjectId) -> Result<Project, StoreError>;
async fn get_project_by_name(
&self,
workspace_id: &WorkspaceId,
name: &str,
) -> Result<Project, StoreError>;
async fn delete_project(&self, project_id: &ProjectId) -> Result<(), StoreError>;
async fn create_env(&self, params: &CreateEnvParams) -> Result<EnvironmentId, StoreError>;
async fn list_environments(
&self,
project_id: &ProjectId,
) -> Result<Vec<Environment>, StoreError>;
async fn get_environment(&self, env_id: &EnvironmentId) -> Result<Environment, StoreError>;
async fn get_environment_by_name(
&self,
project_id: &ProjectId,
name: &str,
) -> Result<Environment, StoreError>;
async fn delete_environment(&self, env_id: &EnvironmentId) -> Result<(), StoreError>;
async fn upsert_secret(
&self,
env_id: &EnvironmentId,
key: &str,
nonce: &[u8], ciphertext: &[u8], ) -> Result<i64, StoreError>;
async fn get_secret(&self, env_id: &EnvironmentId, key: &str) -> Result<SecretRow, StoreError>;
async fn list_secret_keys(&self, env_id: &EnvironmentId) -> Result<Vec<String>, StoreError>;
async fn delete_secret(&self, env_id: &EnvironmentId, key: &str) -> Result<i64, StoreError>;
async fn get_env_wrap(
&self,
ws: &WorkspaceId,
project: &ProjectName,
env: &EnvName,
) -> Result<(Vec<u8>, Vec<u8>), StoreError>;
}
#[cfg(test)]
mod tests {
use super::*;
struct NoopStore;
#[async_trait::async_trait]
impl Store for NoopStore {
async fn create_user(
&self,
_params: &CreateUserParams,
) -> Result<(UserId, Option<PrincipalId>), StoreError> {
let user_id = UserId(Uuid::new_v4());
let principal_id = _params
.principal
.as_ref()
.map(|_| PrincipalId(Uuid::new_v4()));
Ok((user_id, principal_id))
}
async fn get_user_by_email(&self, _email: &str) -> Result<User, StoreError> {
Err(StoreError::NotFound)
}
async fn get_user_by_id(&self, _user_id: &UserId) -> Result<User, StoreError> {
Err(StoreError::NotFound)
}
async fn create_principal(
&self,
_params: &CreatePrincipalParams,
) -> Result<PrincipalId, StoreError> {
Ok(PrincipalId(Uuid::new_v4()))
}
async fn get_principal(
&self,
_principal_id: &PrincipalId,
) -> Result<Principal, StoreError> {
Err(StoreError::NotFound)
}
async fn rename_principal(
&self,
_principal_id: &PrincipalId,
_new_name: &str,
) -> Result<(), StoreError> {
Ok(())
}
async fn list_principals(&self, _user_id: &UserId) -> Result<Vec<Principal>, StoreError> {
Ok(vec![])
}
async fn create_invite(&self, _params: &CreateInviteParams) -> Result<Invite, StoreError> {
Ok(Invite {
id: InviteId(Uuid::new_v4()),
token: "test-token".to_string(),
workspace_ids: vec![],
kek_encrypted: None,
kek_nonce: None,
created_at: Utc::now(),
updated_at: Utc::now(),
expires_at: Utc::now(),
created_by_user_id: _params.created_by_user_id.clone(),
})
}
async fn get_invite_by_token(&self, _token: &str) -> Result<Invite, StoreError> {
Err(StoreError::NotFound)
}
async fn list_invites(&self, _user_id: Option<&UserId>) -> Result<Vec<Invite>, StoreError> {
Ok(vec![])
}
async fn revoke_invite(&self, _invite_id: &InviteId) -> Result<(), StoreError> {
Ok(())
}
async fn create_workspace(
&self,
_params: &CreateWorkspaceParams,
) -> Result<WorkspaceId, StoreError> {
Ok(WorkspaceId(Uuid::new_v4()))
}
async fn list_workspaces(&self, _user_id: &UserId) -> Result<Vec<Workspace>, StoreError> {
Ok(vec![])
}
async fn get_workspace(&self, _ws: &WorkspaceId) -> Result<Workspace, StoreError> {
Err(StoreError::NotFound)
}
async fn get_workspace_by_name(
&self,
_user_id: &UserId,
_name: &str,
) -> Result<Workspace, StoreError> {
Err(StoreError::NotFound)
}
async fn get_workspace_by_name_for_principal(
&self,
_principal_id: &PrincipalId,
_name: &str,
) -> Result<Workspace, StoreError> {
Err(StoreError::NotFound)
}
async fn add_workspace_principal(
&self,
_params: &AddWorkspacePrincipalParams,
) -> Result<(), StoreError> {
Ok(())
}
async fn get_workspace_principal(
&self,
_workspace_id: &WorkspaceId,
_principal_id: &PrincipalId,
) -> Result<WorkspacePrincipal, StoreError> {
Err(StoreError::NotFound)
}
async fn list_workspace_principals(
&self,
_workspace_id: &WorkspaceId,
) -> Result<Vec<WorkspacePrincipal>, StoreError> {
Ok(vec![])
}
async fn add_user_to_workspace(
&self,
_workspace_id: &WorkspaceId,
_user_id: &UserId,
) -> Result<(), StoreError> {
Ok(())
}
async fn create_project(
&self,
_params: &CreateProjectParams,
) -> Result<ProjectId, StoreError> {
Ok(ProjectId(Uuid::new_v4()))
}
async fn list_projects(
&self,
_workspace_id: &WorkspaceId,
) -> Result<Vec<Project>, StoreError> {
Ok(vec![])
}
async fn get_project(&self, _project_id: &ProjectId) -> Result<Project, StoreError> {
Err(StoreError::NotFound)
}
async fn get_project_by_name(
&self,
_workspace_id: &WorkspaceId,
_name: &str,
) -> Result<Project, StoreError> {
Err(StoreError::NotFound)
}
async fn delete_project(&self, _project_id: &ProjectId) -> Result<(), StoreError> {
Ok(())
}
async fn create_env(&self, _params: &CreateEnvParams) -> Result<EnvironmentId, StoreError> {
Ok(EnvironmentId(Uuid::new_v4()))
}
async fn list_environments(
&self,
_project_id: &ProjectId,
) -> Result<Vec<Environment>, StoreError> {
Ok(vec![])
}
async fn get_environment(
&self,
_env_id: &EnvironmentId,
) -> Result<Environment, StoreError> {
Err(StoreError::NotFound)
}
async fn get_environment_by_name(
&self,
_project_id: &ProjectId,
_name: &str,
) -> Result<Environment, StoreError> {
Err(StoreError::NotFound)
}
async fn delete_environment(&self, _env_id: &EnvironmentId) -> Result<(), StoreError> {
Ok(())
}
async fn get_env_wrap(
&self,
_ws: &WorkspaceId,
_project: &ProjectName,
_env: &EnvName,
) -> Result<(Vec<u8>, Vec<u8>), StoreError> {
Err(StoreError::NotFound)
}
async fn upsert_secret(
&self,
_env_id: &EnvironmentId,
_key: &str,
_nonce: &[u8],
_ciphertext: &[u8],
) -> Result<i64, StoreError> {
Ok(1)
}
async fn get_secret(
&self,
_env_id: &EnvironmentId,
_key: &str,
) -> Result<SecretRow, StoreError> {
Err(StoreError::NotFound)
}
async fn list_secret_keys(
&self,
_env_id: &EnvironmentId,
) -> Result<Vec<String>, StoreError> {
Ok(vec![])
}
async fn delete_secret(
&self,
_env_id: &EnvironmentId,
_key: &str,
) -> Result<i64, StoreError> {
Ok(1)
}
}
#[tokio::test]
async fn trait_smoke() {
let s = NoopStore;
let (user_id, _) = s
.create_user(&CreateUserParams {
email: "test@example.com".to_string(),
principal: None,
workspace_ids: vec![],
})
.await
.unwrap();
let ws = s
.create_workspace(&CreateWorkspaceParams {
id: WorkspaceId(uuid::Uuid::now_v7()),
name: "test-workspace".to_string(),
owner_user_id: user_id.clone(),
kdf_salt: b"0123456789abcdef".to_vec(),
m_cost_kib: 64 * 1024,
t_cost: 3,
p_cost: 1,
})
.await
.unwrap();
let project_id = s
.create_project(&CreateProjectParams {
workspace_id: ws.clone(),
name: "p1".to_string(),
})
.await
.unwrap();
let _ = s.list_workspaces(&user_id).await.unwrap();
let _ = s.list_projects(&ws).await.unwrap();
let _ = s.get_project(&project_id).await;
}
}