1use chrono::{DateTime, Utc};
7use thiserror::Error;
8use uuid::Uuid;
9
10#[derive(Debug, Error)]
12pub enum StoreError {
13 #[error("not found")]
14 NotFound,
15 #[error("already exists")]
16 AlreadyExists,
17 #[error("conflict")]
18 Conflict,
19 #[error("backend error: {0}")]
20 Backend(String),
21}
22
23#[derive(Clone, Debug, PartialEq, Eq, Hash)]
25pub struct UserId(pub Uuid);
26
27#[derive(Clone, Debug, PartialEq, Eq, Hash)]
28pub struct PrincipalId(pub Uuid);
29
30#[derive(Clone, Debug, PartialEq, Eq, Hash)]
31pub struct InviteId(pub Uuid);
32
33#[derive(Clone, Debug, PartialEq, Eq, Hash)]
34pub struct WorkspaceId(pub Uuid);
35
36#[derive(Clone, Debug, PartialEq, Eq, Hash)]
37pub struct ProjectId(pub Uuid);
38
39#[derive(Clone, Debug, PartialEq, Eq, Hash)]
40pub struct ProjectName(pub String);
41
42#[derive(Clone, Debug, PartialEq, Eq, Hash)]
43pub struct EnvironmentId(pub Uuid);
44
45#[derive(Clone, Debug, PartialEq, Eq, Hash)]
46pub struct EnvName(pub String);
47
48#[derive(Clone, Debug)]
50pub struct SecretRow {
51 pub nonce: Vec<u8>, pub ciphertext: Vec<u8>, }
54
55#[derive(Clone, Debug)]
57pub struct CreateUserParams {
58 pub email: String,
59 pub principal: Option<CreatePrincipalData>,
61 pub workspace_ids: Vec<WorkspaceId>,
63}
64
65#[derive(Clone, Debug)]
67pub struct CreatePrincipalData {
68 pub name: String,
69 pub public_key: Vec<u8>, pub x25519_public_key: Option<Vec<u8>>, pub is_service: bool, }
73
74#[derive(Clone, Debug)]
76pub struct CreatePrincipalParams {
77 pub user_id: Option<UserId>, pub name: String,
79 pub public_key: Vec<u8>, pub x25519_public_key: Option<Vec<u8>>, }
82
83#[derive(Clone, Debug)]
85pub struct CreateWorkspaceParams {
86 pub id: WorkspaceId, pub name: String,
88 pub owner_user_id: UserId,
89 pub kdf_salt: Vec<u8>, pub m_cost_kib: u32, pub t_cost: u32, pub p_cost: u32, }
94
95#[derive(Clone, Debug)]
97pub struct CreateInviteParams {
98 pub workspace_ids: Vec<WorkspaceId>,
99 pub token: String, pub kek_encrypted: Option<Vec<u8>>, pub kek_nonce: Option<Vec<u8>>, pub expires_at: DateTime<Utc>,
103 pub created_by_user_id: Option<UserId>, }
105
106#[derive(Clone, Debug)]
108pub struct CreateProjectParams {
109 pub workspace_id: WorkspaceId,
110 pub name: String,
111}
112
113#[derive(Clone, Debug)]
115pub struct CreateEnvParams {
116 pub project_id: ProjectId,
117 pub name: String,
118 pub dek_wrapped: Vec<u8>, pub dek_nonce: Vec<u8>, }
121
122#[derive(Clone, Debug)]
124pub struct User {
125 pub id: UserId,
126 pub email: String,
127 pub created_at: DateTime<Utc>,
128 pub updated_at: DateTime<Utc>,
129}
130
131#[derive(Clone, Debug)]
133pub struct Principal {
134 pub id: PrincipalId,
135 pub user_id: Option<UserId>, pub name: String,
137 pub public_key: Vec<u8>, pub x25519_public_key: Option<Vec<u8>>, pub created_at: DateTime<Utc>,
140 pub updated_at: DateTime<Utc>,
141}
142
143#[derive(Clone, Debug)]
145pub struct Invite {
146 pub id: InviteId,
147 pub token: String,
148 pub workspace_ids: Vec<WorkspaceId>,
149 pub kek_encrypted: Option<Vec<u8>>, pub kek_nonce: Option<Vec<u8>>, pub created_at: DateTime<Utc>,
152 pub updated_at: DateTime<Utc>,
153 pub expires_at: DateTime<Utc>,
154 pub created_by_user_id: Option<UserId>, }
156
157#[derive(Clone, Debug)]
159pub struct Workspace {
160 pub id: WorkspaceId,
161 pub name: String,
162 pub owner_user_id: UserId,
163 pub kdf_salt: Vec<u8>,
164 pub m_cost_kib: u32,
165 pub t_cost: u32,
166 pub p_cost: u32,
167 pub created_at: DateTime<Utc>,
168 pub updated_at: DateTime<Utc>,
169}
170
171#[derive(Clone, Debug)]
173pub struct WorkspacePrincipal {
174 pub workspace_id: WorkspaceId,
175 pub principal_id: PrincipalId,
176 pub ephemeral_pub: Vec<u8>, pub kek_wrapped: Vec<u8>, pub kek_nonce: Vec<u8>, pub created_at: DateTime<Utc>,
180}
181
182#[derive(Clone, Debug)]
184pub struct AddWorkspacePrincipalParams {
185 pub workspace_id: WorkspaceId,
186 pub principal_id: PrincipalId,
187 pub ephemeral_pub: Vec<u8>,
188 pub kek_wrapped: Vec<u8>,
189 pub kek_nonce: Vec<u8>,
190}
191
192#[derive(Clone, Debug)]
194pub struct Project {
195 pub id: ProjectId,
196 pub workspace_id: WorkspaceId,
197 pub name: String,
198 pub created_at: DateTime<Utc>,
199 pub updated_at: DateTime<Utc>,
200}
201
202#[derive(Clone, Debug)]
204pub struct Environment {
205 pub id: EnvironmentId,
206 pub project_id: ProjectId,
207 pub name: String,
208 pub dek_wrapped: Vec<u8>,
209 pub dek_nonce: Vec<u8>,
210 pub version: i64, pub created_at: DateTime<Utc>,
212 pub updated_at: DateTime<Utc>,
213}
214
215#[async_trait::async_trait]
219pub trait Store {
220 async fn create_user(
225 &self,
226 params: &CreateUserParams,
227 ) -> Result<(UserId, Option<PrincipalId>), StoreError>;
228
229 async fn get_user_by_email(&self, email: &str) -> Result<User, StoreError>;
231
232 async fn get_user_by_id(&self, user_id: &UserId) -> Result<User, StoreError>;
234
235 async fn create_principal(
239 &self,
240 params: &CreatePrincipalParams,
241 ) -> Result<PrincipalId, StoreError>;
242
243 async fn get_principal(&self, principal_id: &PrincipalId) -> Result<Principal, StoreError>;
245
246 async fn rename_principal(
248 &self,
249 principal_id: &PrincipalId,
250 new_name: &str,
251 ) -> Result<(), StoreError>;
252
253 async fn list_principals(&self, user_id: &UserId) -> Result<Vec<Principal>, StoreError>;
255
256 async fn create_invite(&self, params: &CreateInviteParams) -> Result<Invite, StoreError>;
260
261 async fn get_invite_by_token(&self, token: &str) -> Result<Invite, StoreError>;
263
264 async fn list_invites(&self, user_id: Option<&UserId>) -> Result<Vec<Invite>, StoreError>;
266
267 async fn revoke_invite(&self, invite_id: &InviteId) -> Result<(), StoreError>;
269
270 async fn create_workspace(
274 &self,
275 params: &CreateWorkspaceParams,
276 ) -> Result<WorkspaceId, StoreError>;
277
278 async fn list_workspaces(&self, user_id: &UserId) -> Result<Vec<Workspace>, StoreError>;
280
281 async fn get_workspace(&self, ws: &WorkspaceId) -> Result<Workspace, StoreError>;
283
284 async fn get_workspace_by_name(
286 &self,
287 user_id: &UserId,
288 name: &str,
289 ) -> Result<Workspace, StoreError>;
290
291 async fn get_workspace_by_name_for_principal(
293 &self,
294 principal_id: &PrincipalId,
295 name: &str,
296 ) -> Result<Workspace, StoreError>;
297
298 async fn add_workspace_principal(
300 &self,
301 params: &AddWorkspacePrincipalParams,
302 ) -> Result<(), StoreError>;
303
304 async fn get_workspace_principal(
306 &self,
307 workspace_id: &WorkspaceId,
308 principal_id: &PrincipalId,
309 ) -> Result<WorkspacePrincipal, StoreError>;
310
311 async fn list_workspace_principals(
313 &self,
314 workspace_id: &WorkspaceId,
315 ) -> Result<Vec<WorkspacePrincipal>, StoreError>;
316
317 async fn add_user_to_workspace(
319 &self,
320 workspace_id: &WorkspaceId,
321 user_id: &UserId,
322 ) -> Result<(), StoreError>;
323
324 async fn create_project(&self, params: &CreateProjectParams) -> Result<ProjectId, StoreError>;
328
329 async fn list_projects(&self, workspace_id: &WorkspaceId) -> Result<Vec<Project>, StoreError>;
331
332 async fn get_project(&self, project_id: &ProjectId) -> Result<Project, StoreError>;
334
335 async fn get_project_by_name(
337 &self,
338 workspace_id: &WorkspaceId,
339 name: &str,
340 ) -> Result<Project, StoreError>;
341
342 async fn delete_project(&self, project_id: &ProjectId) -> Result<(), StoreError>;
344
345 async fn create_env(&self, params: &CreateEnvParams) -> Result<EnvironmentId, StoreError>;
349
350 async fn list_environments(
352 &self,
353 project_id: &ProjectId,
354 ) -> Result<Vec<Environment>, StoreError>;
355
356 async fn get_environment(&self, env_id: &EnvironmentId) -> Result<Environment, StoreError>;
358
359 async fn get_environment_by_name(
361 &self,
362 project_id: &ProjectId,
363 name: &str,
364 ) -> Result<Environment, StoreError>;
365
366 async fn delete_environment(&self, env_id: &EnvironmentId) -> Result<(), StoreError>;
368
369 async fn upsert_secret(
374 &self,
375 env_id: &EnvironmentId,
376 key: &str,
377 nonce: &[u8], ciphertext: &[u8], ) -> Result<i64, StoreError>;
380
381 async fn get_secret(&self, env_id: &EnvironmentId, key: &str) -> Result<SecretRow, StoreError>;
383
384 async fn list_secret_keys(&self, env_id: &EnvironmentId) -> Result<Vec<String>, StoreError>;
386
387 async fn delete_secret(&self, env_id: &EnvironmentId, key: &str) -> Result<i64, StoreError>;
390
391 async fn get_env_wrap(
393 &self,
394 ws: &WorkspaceId,
395 project: &ProjectName,
396 env: &EnvName,
397 ) -> Result<(Vec<u8>, Vec<u8>), StoreError>;
398}
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 struct NoopStore;
406 #[async_trait::async_trait]
407 impl Store for NoopStore {
408 async fn create_user(
409 &self,
410 _params: &CreateUserParams,
411 ) -> Result<(UserId, Option<PrincipalId>), StoreError> {
412 let user_id = UserId(Uuid::new_v4());
413 let principal_id = _params
414 .principal
415 .as_ref()
416 .map(|_| PrincipalId(Uuid::new_v4()));
417 Ok((user_id, principal_id))
418 }
419
420 async fn get_user_by_email(&self, _email: &str) -> Result<User, StoreError> {
421 Err(StoreError::NotFound)
422 }
423
424 async fn get_user_by_id(&self, _user_id: &UserId) -> Result<User, StoreError> {
425 Err(StoreError::NotFound)
426 }
427
428 async fn create_principal(
429 &self,
430 _params: &CreatePrincipalParams,
431 ) -> Result<PrincipalId, StoreError> {
432 Ok(PrincipalId(Uuid::new_v4()))
433 }
434
435 async fn get_principal(
436 &self,
437 _principal_id: &PrincipalId,
438 ) -> Result<Principal, StoreError> {
439 Err(StoreError::NotFound)
440 }
441
442 async fn rename_principal(
443 &self,
444 _principal_id: &PrincipalId,
445 _new_name: &str,
446 ) -> Result<(), StoreError> {
447 Ok(())
448 }
449
450 async fn list_principals(&self, _user_id: &UserId) -> Result<Vec<Principal>, StoreError> {
451 Ok(vec![])
452 }
453
454 async fn create_invite(&self, _params: &CreateInviteParams) -> Result<Invite, StoreError> {
455 Ok(Invite {
456 id: InviteId(Uuid::new_v4()),
457 token: "test-token".to_string(),
458 workspace_ids: vec![],
459 kek_encrypted: None,
460 kek_nonce: None,
461 created_at: Utc::now(),
462 updated_at: Utc::now(),
463 expires_at: Utc::now(),
464 created_by_user_id: _params.created_by_user_id.clone(),
465 })
466 }
467
468 async fn get_invite_by_token(&self, _token: &str) -> Result<Invite, StoreError> {
469 Err(StoreError::NotFound)
470 }
471
472 async fn list_invites(&self, _user_id: Option<&UserId>) -> Result<Vec<Invite>, StoreError> {
473 Ok(vec![])
474 }
475
476 async fn revoke_invite(&self, _invite_id: &InviteId) -> Result<(), StoreError> {
477 Ok(())
478 }
479
480 async fn create_workspace(
481 &self,
482 _params: &CreateWorkspaceParams,
483 ) -> Result<WorkspaceId, StoreError> {
484 Ok(WorkspaceId(Uuid::new_v4()))
485 }
486
487 async fn list_workspaces(&self, _user_id: &UserId) -> Result<Vec<Workspace>, StoreError> {
488 Ok(vec![])
489 }
490
491 async fn get_workspace(&self, _ws: &WorkspaceId) -> Result<Workspace, StoreError> {
492 Err(StoreError::NotFound)
493 }
494
495 async fn get_workspace_by_name(
496 &self,
497 _user_id: &UserId,
498 _name: &str,
499 ) -> Result<Workspace, StoreError> {
500 Err(StoreError::NotFound)
501 }
502
503 async fn get_workspace_by_name_for_principal(
504 &self,
505 _principal_id: &PrincipalId,
506 _name: &str,
507 ) -> Result<Workspace, StoreError> {
508 Err(StoreError::NotFound)
509 }
510
511 async fn add_workspace_principal(
512 &self,
513 _params: &AddWorkspacePrincipalParams,
514 ) -> Result<(), StoreError> {
515 Ok(())
516 }
517
518 async fn get_workspace_principal(
519 &self,
520 _workspace_id: &WorkspaceId,
521 _principal_id: &PrincipalId,
522 ) -> Result<WorkspacePrincipal, StoreError> {
523 Err(StoreError::NotFound)
524 }
525
526 async fn list_workspace_principals(
527 &self,
528 _workspace_id: &WorkspaceId,
529 ) -> Result<Vec<WorkspacePrincipal>, StoreError> {
530 Ok(vec![])
531 }
532
533 async fn add_user_to_workspace(
534 &self,
535 _workspace_id: &WorkspaceId,
536 _user_id: &UserId,
537 ) -> Result<(), StoreError> {
538 Ok(())
539 }
540
541 async fn create_project(
542 &self,
543 _params: &CreateProjectParams,
544 ) -> Result<ProjectId, StoreError> {
545 Ok(ProjectId(Uuid::new_v4()))
546 }
547
548 async fn list_projects(
549 &self,
550 _workspace_id: &WorkspaceId,
551 ) -> Result<Vec<Project>, StoreError> {
552 Ok(vec![])
553 }
554
555 async fn get_project(&self, _project_id: &ProjectId) -> Result<Project, StoreError> {
556 Err(StoreError::NotFound)
557 }
558
559 async fn get_project_by_name(
560 &self,
561 _workspace_id: &WorkspaceId,
562 _name: &str,
563 ) -> Result<Project, StoreError> {
564 Err(StoreError::NotFound)
565 }
566
567 async fn delete_project(&self, _project_id: &ProjectId) -> Result<(), StoreError> {
568 Ok(())
569 }
570
571 async fn create_env(&self, _params: &CreateEnvParams) -> Result<EnvironmentId, StoreError> {
572 Ok(EnvironmentId(Uuid::new_v4()))
573 }
574
575 async fn list_environments(
576 &self,
577 _project_id: &ProjectId,
578 ) -> Result<Vec<Environment>, StoreError> {
579 Ok(vec![])
580 }
581
582 async fn get_environment(
583 &self,
584 _env_id: &EnvironmentId,
585 ) -> Result<Environment, StoreError> {
586 Err(StoreError::NotFound)
587 }
588
589 async fn get_environment_by_name(
590 &self,
591 _project_id: &ProjectId,
592 _name: &str,
593 ) -> Result<Environment, StoreError> {
594 Err(StoreError::NotFound)
595 }
596
597 async fn delete_environment(&self, _env_id: &EnvironmentId) -> Result<(), StoreError> {
598 Ok(())
599 }
600
601 async fn get_env_wrap(
602 &self,
603 _ws: &WorkspaceId,
604 _project: &ProjectName,
605 _env: &EnvName,
606 ) -> Result<(Vec<u8>, Vec<u8>), StoreError> {
607 Err(StoreError::NotFound)
608 }
609
610 async fn upsert_secret(
611 &self,
612 _env_id: &EnvironmentId,
613 _key: &str,
614 _nonce: &[u8],
615 _ciphertext: &[u8],
616 ) -> Result<i64, StoreError> {
617 Ok(1)
618 }
619
620 async fn get_secret(
621 &self,
622 _env_id: &EnvironmentId,
623 _key: &str,
624 ) -> Result<SecretRow, StoreError> {
625 Err(StoreError::NotFound)
626 }
627
628 async fn list_secret_keys(
629 &self,
630 _env_id: &EnvironmentId,
631 ) -> Result<Vec<String>, StoreError> {
632 Ok(vec![])
633 }
634
635 async fn delete_secret(
636 &self,
637 _env_id: &EnvironmentId,
638 _key: &str,
639 ) -> Result<i64, StoreError> {
640 Ok(1)
641 }
642 }
643
644 #[tokio::test]
645 async fn trait_smoke() {
646 let s = NoopStore;
647
648 let (user_id, _) = s
649 .create_user(&CreateUserParams {
650 email: "test@example.com".to_string(),
651 principal: None,
652 workspace_ids: vec![],
653 })
654 .await
655 .unwrap();
656
657 let ws = s
658 .create_workspace(&CreateWorkspaceParams {
659 id: WorkspaceId(uuid::Uuid::now_v7()),
660 name: "test-workspace".to_string(),
661 owner_user_id: user_id.clone(),
662 kdf_salt: b"0123456789abcdef".to_vec(),
663 m_cost_kib: 64 * 1024,
664 t_cost: 3,
665 p_cost: 1,
666 })
667 .await
668 .unwrap();
669
670 let project_id = s
672 .create_project(&CreateProjectParams {
673 workspace_id: ws.clone(),
674 name: "p1".to_string(),
675 })
676 .await
677 .unwrap();
678
679 let _ = s.list_workspaces(&user_id).await.unwrap();
680 let _ = s.list_projects(&ws).await.unwrap();
681 let _ = s.get_project(&project_id).await;
682 }
683}