1mod base64_array;
2mod base64_vec;
3mod error;
4mod identified_by;
5
6use cts_common::claims::{
7 ClientPermission, DataKeyPermission, KeysetPermission, Permission, Scope,
8};
9pub use identified_by::*;
10
11mod unverified_context;
12
13use serde::{Deserialize, Serialize};
14use std::{
15 borrow::Cow,
16 fmt::{self, Debug, Display, Formatter},
17 ops::Deref,
18};
19use utoipa::ToSchema;
20use uuid::Uuid;
21use validator::Validate;
22use zeroize::{Zeroize, ZeroizeOnDrop};
23
24pub use cipherstash_config;
25pub use error::*;
27
28pub use crate::unverified_context::{UnverifiedContext, UnverifiedContextValue};
29pub use crate::{IdentifiedBy, Name};
30pub mod testing;
31
32pub trait ViturResponse: Serialize + for<'de> Deserialize<'de> + Send {}
33
34pub trait ViturRequest: Serialize + for<'de> Deserialize<'de> + Sized + Send {
35 type Response: ViturResponse;
36
37 const SCOPE: Scope;
38 const ENDPOINT: &'static str;
39}
40
41#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
43#[serde(rename_all = "snake_case")]
44pub enum ClientType {
45 Device,
46}
47
48#[derive(Debug, Serialize, Deserialize, Validate, ToSchema)]
50pub struct CreateClientSpec<'a> {
51 pub client_type: ClientType,
52 #[validate(length(min = 1, max = 64))]
54 #[schema(value_type = String, min_length = 1, max_length = 64)]
55 pub name: Cow<'a, str>,
56}
57
58#[derive(Debug, Serialize, Deserialize, ToSchema)]
60pub struct CreatedClient {
61 pub id: Uuid,
62 #[schema(value_type = String, format = Byte)]
64 pub client_key: ViturKeyMaterial,
65}
66
67#[derive(Debug, Serialize, Deserialize, ToSchema)]
71pub struct CreateKeysetResponse {
72 #[serde(flatten)]
73 pub keyset: Keyset,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub client: Option<CreatedClient>,
76}
77
78impl ViturResponse for CreateKeysetResponse {}
79
80fn validate_keyset_name(name: &str) -> Result<(), validator::ValidationError> {
81 if name.eq_ignore_ascii_case("default") {
82 let mut err = validator::ValidationError::new("reserved_name");
83 err.message =
84 Some("the name 'default' is reserved for the workspace default keyset".into());
85 return Err(err);
86 }
87 if !name
88 .chars()
89 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '/')
90 {
91 let mut err = validator::ValidationError::new("invalid_characters");
92 err.message = Some("name must only contain: A-Z a-z 0-9 _ - /".into());
93 return Err(err);
94 }
95 Ok(())
96}
97
98#[derive(Debug, Serialize, Deserialize, Validate, ToSchema)]
102pub struct CreateKeysetRequest<'a> {
103 #[validate(length(min = 1, max = 64), custom(function = "validate_keyset_name"))]
106 #[schema(value_type = String, min_length = 1, max_length = 64, pattern = r"^[A-Za-z0-9_\-/]+$")]
107 pub name: Cow<'a, str>,
108 #[validate(length(min = 1, max = 256))]
110 #[schema(value_type = String, min_length = 1, max_length = 256)]
111 pub description: Cow<'a, str>,
112 #[validate(nested)]
113 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub client: Option<CreateClientSpec<'a>>,
115}
116
117impl ViturRequest for CreateKeysetRequest<'_> {
118 type Response = CreateKeysetResponse;
119
120 const ENDPOINT: &'static str = "create-keyset";
121 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Create));
122}
123
124#[derive(Default, Debug, Serialize, Deserialize, ToSchema)]
129pub struct ListKeysetRequest {
130 #[serde(default)]
131 pub show_disabled: bool,
132}
133
134impl ViturRequest for ListKeysetRequest {
135 type Response = Vec<Keyset>;
136
137 const ENDPOINT: &'static str = "list-keysets";
138 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::List));
139}
140
141#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
144pub struct Keyset {
145 pub id: Uuid,
146 pub name: String,
147 pub description: String,
148 pub is_disabled: bool,
149 #[serde(default)]
150 pub is_default: bool,
151}
152
153impl ViturResponse for Vec<Keyset> {}
154
155#[derive(Default, Debug, Serialize, Deserialize, ToSchema)]
157pub struct EmptyResponse {}
158
159impl ViturResponse for EmptyResponse {}
160
161#[derive(Debug, Serialize, Deserialize, ToSchema)]
168pub struct CreateClientRequest<'a> {
169 #[serde(alias = "dataset_id", default, skip_serializing_if = "Option::is_none")]
172 #[schema(value_type = Option<String>, example = "550e8400-e29b-41d4-a716-446655440000")]
173 pub keyset_id: Option<IdentifiedBy>,
174 #[schema(value_type = String)]
176 pub name: Cow<'a, str>,
177 #[schema(value_type = String)]
179 pub description: Cow<'a, str>,
180}
181
182impl ViturRequest for CreateClientRequest<'_> {
183 type Response = CreateClientResponse;
184
185 const ENDPOINT: &'static str = "create-client";
186 const SCOPE: Scope = Scope::with_permission(Permission::Client(ClientPermission::Create));
187}
188
189#[derive(Debug, Serialize, Deserialize, ToSchema)]
194pub struct CreateClientResponse {
195 pub id: Uuid,
197 #[serde(rename = "dataset_id")]
199 pub keyset_id: Uuid,
200 pub name: String,
202 pub description: String,
204 #[schema(value_type = String, format = Byte)]
206 pub client_key: ViturKeyMaterial,
207}
208
209impl ViturResponse for CreateClientResponse {}
210
211#[derive(Debug, Default, Serialize, Deserialize, ToSchema)]
219pub struct ListClientRequest {
220 #[serde(default, skip_serializing_if = "Option::is_none")]
224 pub keyset_id: Option<IdentifiedBy>,
225}
226
227impl ViturRequest for ListClientRequest {
228 type Response = Vec<KeysetClient>;
229
230 const ENDPOINT: &'static str = "list-clients";
231 const SCOPE: Scope = Scope::with_permission(Permission::Client(ClientPermission::List));
232}
233
234#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, ToSchema)]
237#[serde(untagged)]
238pub enum ClientKeysetId {
239 Single(Uuid),
240 Multiple(Vec<Uuid>),
241}
242
243impl PartialEq<Uuid> for ClientKeysetId {
245 fn eq(&self, other: &Uuid) -> bool {
246 if let ClientKeysetId::Single(id) = self {
247 id == other
248 } else {
249 false
250 }
251 }
252}
253
254#[derive(Debug, Serialize, Deserialize, ToSchema)]
256pub struct KeysetClient {
257 pub id: Uuid,
258 #[serde(alias = "dataset_id")]
259 pub keyset_id: ClientKeysetId,
260 pub name: String,
261 pub description: String,
262 pub created_by: Option<String>,
263}
264
265impl ViturResponse for Vec<KeysetClient> {}
266
267#[derive(Debug, Serialize, Deserialize, ToSchema)]
272pub struct DeleteClientRequest {
273 pub client_id: Uuid,
274}
275
276impl ViturRequest for DeleteClientRequest {
277 type Response = DeleteClientResponse;
278
279 const ENDPOINT: &'static str = "delete-client";
280 const SCOPE: Scope = Scope::with_permission(Permission::Client(ClientPermission::Delete));
281}
282
283#[derive(Default, Debug, Serialize, Deserialize, ToSchema)]
284pub struct DeleteClientResponse {}
285
286impl ViturResponse for DeleteClientResponse {}
287
288#[derive(Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
290pub struct ViturKeyMaterial(#[serde(with = "base64_vec")] Vec<u8>);
291opaque_debug::implement!(ViturKeyMaterial);
292
293impl From<Vec<u8>> for ViturKeyMaterial {
294 fn from(inner: Vec<u8>) -> Self {
295 Self(inner)
296 }
297}
298
299impl Deref for ViturKeyMaterial {
300 type Target = [u8];
301
302 fn deref(&self) -> &Self::Target {
303 &self.0
304 }
305}
306
307#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Zeroize)]
308#[serde(transparent)]
309pub struct KeyId(#[serde(with = "base64_array")] [u8; 16]);
310
311impl KeyId {
312 pub fn into_inner(self) -> [u8; 16] {
313 self.0
314 }
315}
316
317impl Display for KeyId {
318 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
319 write!(f, "{}", const_hex::encode(self.0))
320 }
321}
322
323impl From<[u8; 16]> for KeyId {
324 fn from(inner: [u8; 16]) -> Self {
325 Self(inner)
326 }
327}
328
329impl AsRef<[u8; 16]> for KeyId {
330 fn as_ref(&self) -> &[u8; 16] {
331 &self.0
332 }
333}
334
335#[derive(Debug, Serialize, Deserialize, ToSchema)]
339pub struct GeneratedKey {
340 #[schema(value_type = String, format = Byte)]
341 pub key_material: ViturKeyMaterial,
342 #[serde(with = "base64_vec")]
344 #[schema(value_type = String, format = Byte)]
345 pub tag: Vec<u8>,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub decryption_policy: Option<DecryptionPolicy>,
349}
350
351#[derive(Debug, Serialize, Deserialize, ToSchema)]
353pub struct GenerateKeyResponse {
354 pub keys: Vec<GeneratedKey>,
355}
356
357impl ViturResponse for GenerateKeyResponse {}
358
359#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
361pub struct GenerateKeySpec<'a> {
362 #[serde(alias = "id")]
364 #[schema(value_type = String, format = Byte)]
365 pub iv: KeyId,
366 #[schema(value_type = String)]
368 pub descriptor: Cow<'a, str>,
369
370 #[serde(default)]
371 #[schema(value_type = Vec<Context>)]
372 pub context: Cow<'a, [Context]>,
373
374 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub decryption_policy: Option<DecryptionPolicy>,
378}
379
380impl<'a> GenerateKeySpec<'a> {
381 pub fn new(iv: [u8; 16], descriptor: &'a str) -> Self {
382 Self {
383 iv: KeyId(iv),
384 descriptor: Cow::from(descriptor),
385 context: Default::default(),
386 decryption_policy: None,
387 }
388 }
389
390 pub fn new_with_context(
391 iv: [u8; 16],
392 descriptor: &'a str,
393 context: Cow<'a, [Context]>,
394 ) -> Self {
395 Self {
396 iv: KeyId(iv),
397 descriptor: Cow::from(descriptor),
398 context,
399 decryption_policy: None,
400 }
401 }
402
403 pub fn new_with_policy(iv: [u8; 16], descriptor: &'a str, policy: DecryptionPolicy) -> Self {
404 Self {
405 iv: KeyId(iv),
406 descriptor: Cow::from(descriptor),
407 context: Default::default(),
408 decryption_policy: Some(policy),
409 }
410 }
411}
412#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
421pub struct PolicyCondition {
422 pub claim: String,
424 #[serde(default, skip_serializing_if = "Option::is_none")]
426 pub value: Option<String>,
427}
428
429#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
435pub struct DecryptionPolicy {
436 pub conditions: Vec<PolicyCondition>,
437}
438
439#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
443pub enum Context {
445 Tag(String),
447
448 Value(String, String),
451
452 #[serde(alias = "identityClaim")]
457 IdentityClaim(String),
458}
459
460impl Context {
461 pub fn new_tag(tag: impl Into<String>) -> Self {
462 Self::Tag(tag.into())
463 }
464
465 pub fn new_value(key: impl Into<String>, value: impl Into<String>) -> Self {
466 Self::Value(key.into(), value.into())
467 }
468
469 pub fn new_identity_claim(claim: &str) -> Self {
470 Self::IdentityClaim(claim.to_string())
471 }
472}
473
474#[derive(Debug, Serialize, Deserialize, ToSchema)]
482pub struct GenerateKeyRequest<'a> {
483 pub client_id: Uuid,
484 #[serde(alias = "dataset_id")]
485 #[schema(value_type = Option<String>, example = "550e8400-e29b-41d4-a716-446655440000")]
486 pub keyset_id: Option<IdentifiedBy>,
487 #[schema(value_type = Vec<GenerateKeySpec>)]
488 pub keys: Cow<'a, [GenerateKeySpec<'a>]>,
489 #[serde(default)]
490 #[schema(value_type = Object)]
491 pub unverified_context: Cow<'a, UnverifiedContext>,
492}
493
494impl ViturRequest for GenerateKeyRequest<'_> {
495 type Response = GenerateKeyResponse;
496
497 const ENDPOINT: &'static str = "generate-data-key";
498 const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Generate));
499}
500
501#[derive(Debug, Serialize, Deserialize, ToSchema)]
503pub struct RetrievedKey {
504 #[schema(value_type = String, format = Byte)]
506 pub key_material: ViturKeyMaterial,
507}
508
509#[derive(Debug, Serialize, Deserialize, ToSchema)]
512pub struct RetrieveKeyResponse {
513 pub keys: Vec<RetrievedKey>,
514}
515
516impl ViturResponse for RetrieveKeyResponse {}
517
518#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
520pub struct RetrieveKeySpec<'a> {
521 #[serde(alias = "id")]
522 #[schema(value_type = String, format = Byte)]
523 pub iv: KeyId,
524 #[schema(value_type = String)]
526 pub descriptor: Cow<'a, str>,
527 #[schema(value_type = String, format = Byte)]
528 pub tag: Cow<'a, [u8]>,
529
530 #[serde(default)]
531 #[schema(value_type = Vec<Context>)]
532 pub context: Cow<'a, [Context]>,
533
534 #[serde(default)]
537 pub tag_version: usize,
538
539 #[serde(default, skip_serializing_if = "Option::is_none")]
542 pub decryption_policy: Option<DecryptionPolicy>,
543}
544
545impl<'a> RetrieveKeySpec<'a> {
546 const DEFAULT_TAG_VERSION: usize = 0;
547
548 pub fn new(id: KeyId, tag: &'a [u8], descriptor: &'a str) -> Self {
549 Self {
550 iv: id,
551 descriptor: Cow::from(descriptor),
552 tag: Cow::from(tag),
553 context: Cow::Owned(Vec::new()),
554 tag_version: Self::DEFAULT_TAG_VERSION,
555 decryption_policy: None,
556 }
557 }
558
559 pub fn with_context(mut self, context: Cow<'a, [Context]>) -> Self {
560 self.context = context;
561 self
562 }
563
564 pub fn with_policy(mut self, policy: DecryptionPolicy) -> Self {
565 self.decryption_policy = Some(policy);
566 self.tag_version = 1;
567 self
568 }
569}
570
571#[derive(Debug, Serialize, Deserialize, ToSchema)]
577pub struct RetrieveKeyRequest<'a> {
578 pub client_id: Uuid,
579 #[serde(alias = "dataset_id")]
580 #[schema(value_type = Option<String>, example = "550e8400-e29b-41d4-a716-446655440000")]
581 pub keyset_id: Option<IdentifiedBy>,
582 #[schema(value_type = Vec<RetrieveKeySpec>)]
583 pub keys: Cow<'a, [RetrieveKeySpec<'a>]>,
584 #[serde(default)]
585 #[schema(value_type = Object)]
586 pub unverified_context: UnverifiedContext,
587}
588
589impl ViturRequest for RetrieveKeyRequest<'_> {
590 type Response = RetrieveKeyResponse;
591
592 const ENDPOINT: &'static str = "retrieve-data-key";
593 const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Retrieve));
594}
595
596#[derive(Debug, Serialize, Deserialize)]
602pub struct RetrieveKeyRequestFallible<'a> {
603 pub client_id: Uuid,
604 #[serde(alias = "dataset_id")]
605 pub keyset_id: Option<IdentifiedBy>,
606 pub keys: Cow<'a, [RetrieveKeySpec<'a>]>,
607 #[serde(default)]
608 pub unverified_context: Cow<'a, UnverifiedContext>,
609}
610
611impl ViturRequest for RetrieveKeyRequestFallible<'_> {
612 type Response = RetrieveKeyResponseFallible;
613
614 const ENDPOINT: &'static str = "retrieve-data-key-fallible";
615 const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Retrieve));
616}
617
618#[derive(Debug, Serialize, Deserialize, ToSchema)]
620pub struct RetrieveKeyResponseFallible {
621 #[schema(value_type = Vec<serde_json::Value>)]
622 pub keys: Vec<Result<RetrievedKey, String>>, }
624
625impl ViturResponse for RetrieveKeyResponseFallible {}
626
627#[derive(Debug, Serialize, Deserialize, ToSchema)]
631pub struct DisableKeysetRequest {
632 #[serde(alias = "dataset_id")]
634 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
635 pub keyset_id: IdentifiedBy,
636}
637
638impl ViturRequest for DisableKeysetRequest {
639 type Response = EmptyResponse;
640
641 const ENDPOINT: &'static str = "disable-keyset";
642 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Disable));
643}
644
645#[derive(Debug, Serialize, Deserialize, ToSchema)]
649pub struct EnableKeysetRequest {
650 #[serde(alias = "dataset_id")]
652 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
653 pub keyset_id: IdentifiedBy,
654}
655
656impl ViturRequest for EnableKeysetRequest {
657 type Response = EmptyResponse;
658
659 const ENDPOINT: &'static str = "enable-keyset";
660 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Enable));
661}
662
663#[derive(Debug, Serialize, Deserialize, ToSchema)]
669pub struct ModifyKeysetRequest<'a> {
670 #[serde(alias = "dataset_id")]
672 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
673 pub keyset_id: IdentifiedBy,
674 #[schema(value_type = Option<String>)]
676 pub name: Option<Cow<'a, str>>,
677 #[schema(value_type = Option<String>)]
679 pub description: Option<Cow<'a, str>>,
680}
681
682impl ViturRequest for ModifyKeysetRequest<'_> {
683 type Response = EmptyResponse;
684
685 const ENDPOINT: &'static str = "modify-keyset";
686 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Modify));
687}
688
689#[derive(Debug, Serialize, Deserialize, ToSchema)]
694pub struct GrantKeysetRequest {
695 pub client_id: Uuid,
696 #[serde(alias = "dataset_id")]
698 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
699 pub keyset_id: IdentifiedBy,
700}
701
702impl ViturRequest for GrantKeysetRequest {
703 type Response = EmptyResponse;
704
705 const ENDPOINT: &'static str = "grant-keyset";
706 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Grant));
707}
708
709#[derive(Debug, Serialize, Deserialize, ToSchema)]
713pub struct RevokeKeysetRequest {
714 pub client_id: Uuid,
715 #[serde(alias = "dataset_id")]
717 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
718 pub keyset_id: IdentifiedBy,
719}
720
721impl ViturRequest for RevokeKeysetRequest {
722 type Response = EmptyResponse;
723
724 const ENDPOINT: &'static str = "revoke-keyset";
725 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Revoke));
726}
727
728#[derive(Debug, Serialize, Deserialize, PartialEq, PartialOrd, ToSchema)]
737pub struct LoadKeysetRequest {
738 pub client_id: Uuid,
739 #[serde(alias = "dataset_id")]
741 #[schema(value_type = Option<String>, example = "550e8400-e29b-41d4-a716-446655440000")]
742 pub keyset_id: Option<IdentifiedBy>,
743}
744
745impl ViturRequest for LoadKeysetRequest {
746 type Response = LoadKeysetResponse;
747
748 const ENDPOINT: &'static str = "load-keyset";
749
750 const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Retrieve));
754}
755
756#[derive(Debug, Serialize, Deserialize, ToSchema)]
760pub struct LoadKeysetResponse {
761 pub partial_index_key: RetrievedKey,
762 #[serde(rename = "dataset")]
763 pub keyset: Keyset,
764}
765
766impl ViturResponse for LoadKeysetResponse {}
767
768#[cfg(test)]
769mod test {
770 use serde_json::json;
771 use uuid::Uuid;
772
773 use crate::{CreateKeysetResponse, CreatedClient, IdentifiedBy, LoadKeysetRequest, Name};
774
775 mod create_keyset_response_serialization {
776 use super::*;
777 use crate::{Keyset, ViturKeyMaterial};
778
779 #[test]
780 fn without_client_is_flat_keyset() {
781 let id = Uuid::new_v4();
782 let response = CreateKeysetResponse {
783 keyset: Keyset {
784 id,
785 name: "test-keyset".into(),
786 description: "A test keyset".into(),
787 is_disabled: false,
788 is_default: false,
789 },
790 client: None,
791 };
792
793 let serialized = serde_json::to_value(&response).unwrap();
794
795 assert_eq!(
797 serialized,
798 json!({
799 "id": id,
800 "name": "test-keyset",
801 "description": "A test keyset",
802 "is_disabled": false,
803 "is_default": false,
804 })
805 );
806
807 let deserialized: CreateKeysetResponse = serde_json::from_value(serialized).unwrap();
809 assert_eq!(deserialized.keyset.id, id);
810 assert!(deserialized.client.is_none());
811 }
812
813 #[test]
814 fn with_client_includes_client_field() {
815 let keyset_id = Uuid::new_v4();
816 let client_id = Uuid::new_v4();
817
818 let response = CreateKeysetResponse {
819 keyset: Keyset {
820 id: keyset_id,
821 name: "device-keyset".into(),
822 description: "Keyset with device client".into(),
823 is_disabled: false,
824 is_default: false,
825 },
826 client: Some(CreatedClient {
827 id: client_id,
828 client_key: ViturKeyMaterial::from(vec![1, 2, 3, 4]),
829 }),
830 };
831
832 let serialized = serde_json::to_value(&response).unwrap();
833
834 assert_eq!(
836 serialized,
837 json!({
838 "id": keyset_id,
839 "name": "device-keyset",
840 "description": "Keyset with device client",
841 "is_disabled": false,
842 "is_default": false,
843 "client": {
844 "id": client_id,
845 "client_key": "AQIDBA==",
846 },
847 })
848 );
849
850 let deserialized: CreateKeysetResponse = serde_json::from_value(serialized).unwrap();
852 assert_eq!(deserialized.keyset.id, keyset_id);
853 let created_client = deserialized.client.unwrap();
854 assert_eq!(created_client.id, client_id);
855 assert_eq!(&*created_client.client_key, &[1, 2, 3, 4]);
856 }
857 }
858
859 mod create_client_request_serialization {
860 use super::*;
861 use crate::CreateClientRequest;
862
863 #[test]
864 fn with_keyset_id_round_trips() {
865 let keyset_id = Uuid::new_v4();
866 let req = CreateClientRequest {
867 keyset_id: Some(IdentifiedBy::Uuid(keyset_id)),
868 name: "my-client".into(),
869 description: "desc".into(),
870 };
871
872 let serialized = serde_json::to_value(&req).unwrap();
873 assert!(serialized.get("keyset_id").is_some());
874
875 let deserialized: CreateClientRequest = serde_json::from_value(serialized).unwrap();
876 assert_eq!(deserialized.keyset_id, Some(IdentifiedBy::Uuid(keyset_id)));
877 }
878
879 #[test]
880 fn without_keyset_id_round_trips() {
881 let req = CreateClientRequest {
882 keyset_id: None,
883 name: "my-client".into(),
884 description: "desc".into(),
885 };
886
887 let serialized = serde_json::to_value(&req).unwrap();
888 assert!(serialized.get("keyset_id").is_none());
889
890 let deserialized: CreateClientRequest = serde_json::from_value(serialized).unwrap();
891 assert_eq!(deserialized.keyset_id, None);
892 }
893
894 #[test]
895 fn backwards_compatible_with_dataset_id() {
896 let dataset_id = Uuid::new_v4();
897 let json = json!({
898 "dataset_id": dataset_id,
899 "name": "old-client",
900 "description": "old desc",
901 });
902
903 let req: CreateClientRequest = serde_json::from_value(json).unwrap();
904 assert_eq!(req.keyset_id, Some(IdentifiedBy::Uuid(dataset_id)));
905 }
906
907 #[test]
908 fn omitted_keyset_id_defaults_to_none() {
909 let json = json!({
910 "name": "no-keyset",
911 "description": "no keyset",
912 });
913
914 let req: CreateClientRequest = serde_json::from_value(json).unwrap();
915 assert_eq!(req.keyset_id, None);
916 }
917 }
918
919 mod create_keyset_request_validation {
920 use crate::CreateKeysetRequest;
921 use validator::Validate;
922
923 fn valid_request() -> CreateKeysetRequest<'static> {
924 CreateKeysetRequest {
925 name: "my-keyset".into(),
926 description: "A test keyset".into(),
927 client: None,
928 }
929 }
930
931 #[test]
932 fn valid_request_passes() {
933 assert!(valid_request().validate().is_ok());
934 }
935
936 #[test]
937 fn empty_name_fails() {
938 let req = CreateKeysetRequest {
939 name: "".into(),
940 ..valid_request()
941 };
942 let errors = req.validate().unwrap_err();
943 assert!(errors.field_errors().contains_key("name"));
944 }
945
946 #[test]
947 fn name_over_64_chars_fails() {
948 let req = CreateKeysetRequest {
949 name: "a".repeat(65).into(),
950 ..valid_request()
951 };
952 let errors = req.validate().unwrap_err();
953 assert!(errors.field_errors().contains_key("name"));
954 }
955
956 #[test]
957 fn reserved_default_name_fails() {
958 let req = CreateKeysetRequest {
959 name: "default".into(),
960 ..valid_request()
961 };
962 let errors = req.validate().unwrap_err();
963 let name_errors = &errors.field_errors()["name"];
964 assert!(name_errors.iter().any(|e| e.code == "reserved_name"));
965 }
966
967 #[test]
968 fn reserved_default_name_case_insensitive() {
969 let req = CreateKeysetRequest {
970 name: "DEFAULT".into(),
971 ..valid_request()
972 };
973 assert!(req.validate().is_err());
974 }
975
976 #[test]
977 fn name_with_invalid_characters_fails() {
978 let req = CreateKeysetRequest {
979 name: "has spaces".into(),
980 ..valid_request()
981 };
982 let errors = req.validate().unwrap_err();
983 let name_errors = &errors.field_errors()["name"];
984 assert!(name_errors.iter().any(|e| e.code == "invalid_characters"));
985 }
986
987 #[test]
988 fn name_with_special_chars_fails() {
989 for name in ["test@keyset", "test!keyset", "test.keyset", "test%keyset"] {
990 let req = CreateKeysetRequest {
991 name: name.into(),
992 ..valid_request()
993 };
994 assert!(
995 req.validate().is_err(),
996 "expected {name} to fail validation"
997 );
998 }
999 }
1000
1001 #[test]
1002 fn name_with_allowed_chars_passes() {
1003 for name in ["my-keyset", "my_keyset", "my/keyset", "MyKeyset123"] {
1004 let req = CreateKeysetRequest {
1005 name: name.into(),
1006 ..valid_request()
1007 };
1008 assert!(req.validate().is_ok(), "expected {name} to pass validation");
1009 }
1010 }
1011
1012 #[test]
1013 fn empty_description_fails() {
1014 let req = CreateKeysetRequest {
1015 description: "".into(),
1016 ..valid_request()
1017 };
1018 let errors = req.validate().unwrap_err();
1019 assert!(errors.field_errors().contains_key("description"));
1020 }
1021
1022 #[test]
1023 fn description_over_256_chars_fails() {
1024 let req = CreateKeysetRequest {
1025 description: "a".repeat(257).into(),
1026 ..valid_request()
1027 };
1028 let errors = req.validate().unwrap_err();
1029 assert!(errors.field_errors().contains_key("description"));
1030 }
1031
1032 #[test]
1033 fn description_at_256_chars_passes() {
1034 let req = CreateKeysetRequest {
1035 description: "a".repeat(256).into(),
1036 ..valid_request()
1037 };
1038 assert!(req.validate().is_ok());
1039 }
1040
1041 #[test]
1042 fn nested_client_name_validation() {
1043 use crate::{ClientType, CreateClientSpec};
1044
1045 let req = CreateKeysetRequest {
1046 name: "my-keyset".into(),
1047 description: "desc".into(),
1048 client: Some(CreateClientSpec {
1049 client_type: ClientType::Device,
1050 name: "".into(),
1051 }),
1052 };
1053 let errors = req.validate().unwrap_err();
1054 assert!(
1055 errors.errors().contains_key("client"),
1056 "expected nested client validation error"
1057 );
1058 }
1059 }
1060
1061 mod openapi_schema {
1062 use crate::{CreateClientSpec, CreateKeysetRequest};
1063 use utoipa::PartialSchema;
1064
1065 fn schema_json<T: PartialSchema>() -> serde_json::Value {
1066 serde_json::to_value(T::schema()).unwrap()
1067 }
1068
1069 #[test]
1070 fn create_keyset_request_name_has_constraints() {
1071 let schema = schema_json::<CreateKeysetRequest>();
1072 let name = &schema["properties"]["name"];
1073
1074 assert_eq!(name["minLength"], 1);
1075 assert_eq!(name["maxLength"], 64);
1076 assert_eq!(name["pattern"], r"^[A-Za-z0-9_\-/]+$");
1077 }
1078
1079 #[test]
1080 fn create_keyset_request_description_has_constraints() {
1081 let schema = schema_json::<CreateKeysetRequest>();
1082 let desc = &schema["properties"]["description"];
1083
1084 assert_eq!(desc["minLength"], 1);
1085 assert_eq!(desc["maxLength"], 256);
1086 }
1087
1088 #[test]
1089 fn create_client_spec_name_has_constraints() {
1090 let schema = schema_json::<CreateClientSpec>();
1091 let name = &schema["properties"]["name"];
1092
1093 assert_eq!(name["minLength"], 1);
1094 assert_eq!(name["maxLength"], 64);
1095 }
1096 }
1097
1098 mod backwards_compatible_deserialisation {
1099 use super::*;
1100
1101 #[test]
1102 fn when_dataset_id_is_uuid() {
1103 let client_id = Uuid::new_v4();
1104 let dataset_id = Uuid::new_v4();
1105
1106 let json = json!({
1107 "client_id": client_id,
1108 "dataset_id": dataset_id,
1109 });
1110
1111 let req: LoadKeysetRequest = serde_json::from_value(json).unwrap();
1112
1113 assert_eq!(
1114 req,
1115 LoadKeysetRequest {
1116 client_id,
1117 keyset_id: Some(IdentifiedBy::Uuid(dataset_id))
1118 }
1119 );
1120 }
1121
1122 #[test]
1123 fn when_keyset_id_is_uuid() {
1124 let client_id = Uuid::new_v4();
1125 let keyset_id = Uuid::new_v4();
1126
1127 let json = json!({
1128 "client_id": client_id,
1129 "keyset_id": keyset_id,
1130 });
1131
1132 let req: LoadKeysetRequest = serde_json::from_value(json).unwrap();
1133
1134 assert_eq!(
1135 req,
1136 LoadKeysetRequest {
1137 client_id,
1138 keyset_id: Some(IdentifiedBy::Uuid(keyset_id))
1139 }
1140 );
1141 }
1142
1143 #[test]
1144 fn when_dataset_id_is_id_name() {
1145 let client_id = Uuid::new_v4();
1146 let dataset_id = IdentifiedBy::Name(Name::new_untrusted("some-dataset-name"));
1147
1148 let json = json!({
1149 "client_id": client_id,
1150 "dataset_id": dataset_id,
1151 });
1152
1153 let req: LoadKeysetRequest = serde_json::from_value(json).unwrap();
1154
1155 assert_eq!(
1156 req,
1157 LoadKeysetRequest {
1158 client_id,
1159 keyset_id: Some(dataset_id)
1160 }
1161 );
1162 }
1163 }
1164}