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, Serialize, Deserialize)]
216pub struct ListClientRequest;
217
218impl ViturRequest for ListClientRequest {
219 type Response = Vec<KeysetClient>;
220
221 const ENDPOINT: &'static str = "list-clients";
222 const SCOPE: Scope = Scope::with_permission(Permission::Client(ClientPermission::List));
223}
224
225#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, ToSchema)]
228#[serde(untagged)]
229pub enum ClientKeysetId {
230 Single(Uuid),
231 Multiple(Vec<Uuid>),
232}
233
234impl PartialEq<Uuid> for ClientKeysetId {
236 fn eq(&self, other: &Uuid) -> bool {
237 if let ClientKeysetId::Single(id) = self {
238 id == other
239 } else {
240 false
241 }
242 }
243}
244
245#[derive(Debug, Serialize, Deserialize, ToSchema)]
247pub struct KeysetClient {
248 pub id: Uuid,
249 #[serde(alias = "dataset_id")]
250 pub keyset_id: ClientKeysetId,
251 pub name: String,
252 pub description: String,
253 pub created_by: Option<String>,
254}
255
256impl ViturResponse for Vec<KeysetClient> {}
257
258#[derive(Debug, Serialize, Deserialize, ToSchema)]
263pub struct DeleteClientRequest {
264 pub client_id: Uuid,
265}
266
267impl ViturRequest for DeleteClientRequest {
268 type Response = DeleteClientResponse;
269
270 const ENDPOINT: &'static str = "delete-client";
271 const SCOPE: Scope = Scope::with_permission(Permission::Client(ClientPermission::Delete));
272}
273
274#[derive(Default, Debug, Serialize, Deserialize, ToSchema)]
275pub struct DeleteClientResponse {}
276
277impl ViturResponse for DeleteClientResponse {}
278
279#[derive(Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
281pub struct ViturKeyMaterial(#[serde(with = "base64_vec")] Vec<u8>);
282opaque_debug::implement!(ViturKeyMaterial);
283
284impl From<Vec<u8>> for ViturKeyMaterial {
285 fn from(inner: Vec<u8>) -> Self {
286 Self(inner)
287 }
288}
289
290impl Deref for ViturKeyMaterial {
291 type Target = [u8];
292
293 fn deref(&self) -> &Self::Target {
294 &self.0
295 }
296}
297
298#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Zeroize)]
299#[serde(transparent)]
300pub struct KeyId(#[serde(with = "base64_array")] [u8; 16]);
301
302impl KeyId {
303 pub fn into_inner(self) -> [u8; 16] {
304 self.0
305 }
306}
307
308impl Display for KeyId {
309 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
310 write!(f, "{}", const_hex::encode(self.0))
311 }
312}
313
314impl From<[u8; 16]> for KeyId {
315 fn from(inner: [u8; 16]) -> Self {
316 Self(inner)
317 }
318}
319
320impl AsRef<[u8; 16]> for KeyId {
321 fn as_ref(&self) -> &[u8; 16] {
322 &self.0
323 }
324}
325
326#[derive(Debug, Serialize, Deserialize, ToSchema)]
330pub struct GeneratedKey {
331 #[schema(value_type = String, format = Byte)]
332 pub key_material: ViturKeyMaterial,
333 #[serde(with = "base64_vec")]
335 #[schema(value_type = String, format = Byte)]
336 pub tag: Vec<u8>,
337 #[serde(default, skip_serializing_if = "Option::is_none")]
339 pub decryption_policy: Option<DecryptionPolicy>,
340}
341
342#[derive(Debug, Serialize, Deserialize, ToSchema)]
344pub struct GenerateKeyResponse {
345 pub keys: Vec<GeneratedKey>,
346}
347
348impl ViturResponse for GenerateKeyResponse {}
349
350#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
352pub struct GenerateKeySpec<'a> {
353 #[serde(alias = "id")]
355 #[schema(value_type = String, format = Byte)]
356 pub iv: KeyId,
357 #[schema(value_type = String)]
359 pub descriptor: Cow<'a, str>,
360
361 #[serde(default)]
362 #[schema(value_type = Vec<Context>)]
363 pub context: Cow<'a, [Context]>,
364
365 #[serde(default, skip_serializing_if = "Option::is_none")]
368 pub decryption_policy: Option<DecryptionPolicy>,
369}
370
371impl<'a> GenerateKeySpec<'a> {
372 pub fn new(iv: [u8; 16], descriptor: &'a str) -> Self {
373 Self {
374 iv: KeyId(iv),
375 descriptor: Cow::from(descriptor),
376 context: Default::default(),
377 decryption_policy: None,
378 }
379 }
380
381 pub fn new_with_context(
382 iv: [u8; 16],
383 descriptor: &'a str,
384 context: Cow<'a, [Context]>,
385 ) -> Self {
386 Self {
387 iv: KeyId(iv),
388 descriptor: Cow::from(descriptor),
389 context,
390 decryption_policy: None,
391 }
392 }
393
394 pub fn new_with_policy(iv: [u8; 16], descriptor: &'a str, policy: DecryptionPolicy) -> Self {
395 Self {
396 iv: KeyId(iv),
397 descriptor: Cow::from(descriptor),
398 context: Default::default(),
399 decryption_policy: Some(policy),
400 }
401 }
402}
403#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
412pub struct PolicyCondition {
413 pub claim: String,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
417 pub value: Option<String>,
418}
419
420#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
426pub struct DecryptionPolicy {
427 pub conditions: Vec<PolicyCondition>,
428}
429
430#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
434pub enum Context {
436 Tag(String),
438
439 Value(String, String),
442
443 #[serde(alias = "identityClaim")]
448 IdentityClaim(String),
449}
450
451impl Context {
452 pub fn new_tag(tag: impl Into<String>) -> Self {
453 Self::Tag(tag.into())
454 }
455
456 pub fn new_value(key: impl Into<String>, value: impl Into<String>) -> Self {
457 Self::Value(key.into(), value.into())
458 }
459
460 pub fn new_identity_claim(claim: &str) -> Self {
461 Self::IdentityClaim(claim.to_string())
462 }
463}
464
465#[derive(Debug, Serialize, Deserialize, ToSchema)]
473pub struct GenerateKeyRequest<'a> {
474 pub client_id: Uuid,
475 #[serde(alias = "dataset_id")]
476 #[schema(value_type = Option<String>, example = "550e8400-e29b-41d4-a716-446655440000")]
477 pub keyset_id: Option<IdentifiedBy>,
478 #[schema(value_type = Vec<GenerateKeySpec>)]
479 pub keys: Cow<'a, [GenerateKeySpec<'a>]>,
480 #[serde(default)]
481 #[schema(value_type = Object)]
482 pub unverified_context: Cow<'a, UnverifiedContext>,
483}
484
485impl ViturRequest for GenerateKeyRequest<'_> {
486 type Response = GenerateKeyResponse;
487
488 const ENDPOINT: &'static str = "generate-data-key";
489 const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Generate));
490}
491
492#[derive(Debug, Serialize, Deserialize, ToSchema)]
494pub struct RetrievedKey {
495 #[schema(value_type = String, format = Byte)]
497 pub key_material: ViturKeyMaterial,
498}
499
500#[derive(Debug, Serialize, Deserialize, ToSchema)]
503pub struct RetrieveKeyResponse {
504 pub keys: Vec<RetrievedKey>,
505}
506
507impl ViturResponse for RetrieveKeyResponse {}
508
509#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
511pub struct RetrieveKeySpec<'a> {
512 #[serde(alias = "id")]
513 #[schema(value_type = String, format = Byte)]
514 pub iv: KeyId,
515 #[schema(value_type = String)]
517 pub descriptor: Cow<'a, str>,
518 #[schema(value_type = String, format = Byte)]
519 pub tag: Cow<'a, [u8]>,
520
521 #[serde(default)]
522 #[schema(value_type = Vec<Context>)]
523 pub context: Cow<'a, [Context]>,
524
525 #[serde(default)]
528 pub tag_version: usize,
529
530 #[serde(default, skip_serializing_if = "Option::is_none")]
533 pub decryption_policy: Option<DecryptionPolicy>,
534}
535
536impl<'a> RetrieveKeySpec<'a> {
537 const DEFAULT_TAG_VERSION: usize = 0;
538
539 pub fn new(id: KeyId, tag: &'a [u8], descriptor: &'a str) -> Self {
540 Self {
541 iv: id,
542 descriptor: Cow::from(descriptor),
543 tag: Cow::from(tag),
544 context: Cow::Owned(Vec::new()),
545 tag_version: Self::DEFAULT_TAG_VERSION,
546 decryption_policy: None,
547 }
548 }
549
550 pub fn with_context(mut self, context: Cow<'a, [Context]>) -> Self {
551 self.context = context;
552 self
553 }
554
555 pub fn with_policy(mut self, policy: DecryptionPolicy) -> Self {
556 self.decryption_policy = Some(policy);
557 self.tag_version = 1;
558 self
559 }
560}
561
562#[derive(Debug, Serialize, Deserialize, ToSchema)]
568pub struct RetrieveKeyRequest<'a> {
569 pub client_id: Uuid,
570 #[serde(alias = "dataset_id")]
571 #[schema(value_type = Option<String>, example = "550e8400-e29b-41d4-a716-446655440000")]
572 pub keyset_id: Option<IdentifiedBy>,
573 #[schema(value_type = Vec<RetrieveKeySpec>)]
574 pub keys: Cow<'a, [RetrieveKeySpec<'a>]>,
575 #[serde(default)]
576 #[schema(value_type = Object)]
577 pub unverified_context: UnverifiedContext,
578}
579
580impl ViturRequest for RetrieveKeyRequest<'_> {
581 type Response = RetrieveKeyResponse;
582
583 const ENDPOINT: &'static str = "retrieve-data-key";
584 const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Retrieve));
585}
586
587#[derive(Debug, Serialize, Deserialize)]
593pub struct RetrieveKeyRequestFallible<'a> {
594 pub client_id: Uuid,
595 #[serde(alias = "dataset_id")]
596 pub keyset_id: Option<IdentifiedBy>,
597 pub keys: Cow<'a, [RetrieveKeySpec<'a>]>,
598 #[serde(default)]
599 pub unverified_context: Cow<'a, UnverifiedContext>,
600}
601
602impl ViturRequest for RetrieveKeyRequestFallible<'_> {
603 type Response = RetrieveKeyResponseFallible;
604
605 const ENDPOINT: &'static str = "retrieve-data-key-fallible";
606 const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Retrieve));
607}
608
609#[derive(Debug, Serialize, Deserialize, ToSchema)]
611pub struct RetrieveKeyResponseFallible {
612 #[schema(value_type = Vec<serde_json::Value>)]
613 pub keys: Vec<Result<RetrievedKey, String>>, }
615
616impl ViturResponse for RetrieveKeyResponseFallible {}
617
618#[derive(Debug, Serialize, Deserialize, ToSchema)]
622pub struct DisableKeysetRequest {
623 #[serde(alias = "dataset_id")]
625 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
626 pub keyset_id: IdentifiedBy,
627}
628
629impl ViturRequest for DisableKeysetRequest {
630 type Response = EmptyResponse;
631
632 const ENDPOINT: &'static str = "disable-keyset";
633 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Disable));
634}
635
636#[derive(Debug, Serialize, Deserialize, ToSchema)]
640pub struct EnableKeysetRequest {
641 #[serde(alias = "dataset_id")]
643 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
644 pub keyset_id: IdentifiedBy,
645}
646
647impl ViturRequest for EnableKeysetRequest {
648 type Response = EmptyResponse;
649
650 const ENDPOINT: &'static str = "enable-keyset";
651 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Enable));
652}
653
654#[derive(Debug, Serialize, Deserialize, ToSchema)]
660pub struct ModifyKeysetRequest<'a> {
661 #[serde(alias = "dataset_id")]
663 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
664 pub keyset_id: IdentifiedBy,
665 #[schema(value_type = Option<String>)]
667 pub name: Option<Cow<'a, str>>,
668 #[schema(value_type = Option<String>)]
670 pub description: Option<Cow<'a, str>>,
671}
672
673impl ViturRequest for ModifyKeysetRequest<'_> {
674 type Response = EmptyResponse;
675
676 const ENDPOINT: &'static str = "modify-keyset";
677 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Modify));
678}
679
680#[derive(Debug, Serialize, Deserialize, ToSchema)]
685pub struct GrantKeysetRequest {
686 pub client_id: Uuid,
687 #[serde(alias = "dataset_id")]
689 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
690 pub keyset_id: IdentifiedBy,
691}
692
693impl ViturRequest for GrantKeysetRequest {
694 type Response = EmptyResponse;
695
696 const ENDPOINT: &'static str = "grant-keyset";
697 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Grant));
698}
699
700#[derive(Debug, Serialize, Deserialize, ToSchema)]
704pub struct RevokeKeysetRequest {
705 pub client_id: Uuid,
706 #[serde(alias = "dataset_id")]
708 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
709 pub keyset_id: IdentifiedBy,
710}
711
712impl ViturRequest for RevokeKeysetRequest {
713 type Response = EmptyResponse;
714
715 const ENDPOINT: &'static str = "revoke-keyset";
716 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Revoke));
717}
718
719#[derive(Debug, Serialize, Deserialize, PartialEq, PartialOrd, ToSchema)]
728pub struct LoadKeysetRequest {
729 pub client_id: Uuid,
730 #[serde(alias = "dataset_id")]
732 #[schema(value_type = Option<String>, example = "550e8400-e29b-41d4-a716-446655440000")]
733 pub keyset_id: Option<IdentifiedBy>,
734}
735
736impl ViturRequest for LoadKeysetRequest {
737 type Response = LoadKeysetResponse;
738
739 const ENDPOINT: &'static str = "load-keyset";
740
741 const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Retrieve));
745}
746
747#[derive(Debug, Serialize, Deserialize, ToSchema)]
751pub struct LoadKeysetResponse {
752 pub partial_index_key: RetrievedKey,
753 #[serde(rename = "dataset")]
754 pub keyset: Keyset,
755}
756
757impl ViturResponse for LoadKeysetResponse {}
758
759#[cfg(test)]
760mod test {
761 use serde_json::json;
762 use uuid::Uuid;
763
764 use crate::{CreateKeysetResponse, CreatedClient, IdentifiedBy, LoadKeysetRequest, Name};
765
766 mod create_keyset_response_serialization {
767 use super::*;
768 use crate::{Keyset, ViturKeyMaterial};
769
770 #[test]
771 fn without_client_is_flat_keyset() {
772 let id = Uuid::new_v4();
773 let response = CreateKeysetResponse {
774 keyset: Keyset {
775 id,
776 name: "test-keyset".into(),
777 description: "A test keyset".into(),
778 is_disabled: false,
779 is_default: false,
780 },
781 client: None,
782 };
783
784 let serialized = serde_json::to_value(&response).unwrap();
785
786 assert_eq!(
788 serialized,
789 json!({
790 "id": id,
791 "name": "test-keyset",
792 "description": "A test keyset",
793 "is_disabled": false,
794 "is_default": false,
795 })
796 );
797
798 let deserialized: CreateKeysetResponse = serde_json::from_value(serialized).unwrap();
800 assert_eq!(deserialized.keyset.id, id);
801 assert!(deserialized.client.is_none());
802 }
803
804 #[test]
805 fn with_client_includes_client_field() {
806 let keyset_id = Uuid::new_v4();
807 let client_id = Uuid::new_v4();
808
809 let response = CreateKeysetResponse {
810 keyset: Keyset {
811 id: keyset_id,
812 name: "device-keyset".into(),
813 description: "Keyset with device client".into(),
814 is_disabled: false,
815 is_default: false,
816 },
817 client: Some(CreatedClient {
818 id: client_id,
819 client_key: ViturKeyMaterial::from(vec![1, 2, 3, 4]),
820 }),
821 };
822
823 let serialized = serde_json::to_value(&response).unwrap();
824
825 assert_eq!(
827 serialized,
828 json!({
829 "id": keyset_id,
830 "name": "device-keyset",
831 "description": "Keyset with device client",
832 "is_disabled": false,
833 "is_default": false,
834 "client": {
835 "id": client_id,
836 "client_key": "AQIDBA==",
837 },
838 })
839 );
840
841 let deserialized: CreateKeysetResponse = serde_json::from_value(serialized).unwrap();
843 assert_eq!(deserialized.keyset.id, keyset_id);
844 let created_client = deserialized.client.unwrap();
845 assert_eq!(created_client.id, client_id);
846 assert_eq!(&*created_client.client_key, &[1, 2, 3, 4]);
847 }
848 }
849
850 mod create_client_request_serialization {
851 use super::*;
852 use crate::CreateClientRequest;
853
854 #[test]
855 fn with_keyset_id_round_trips() {
856 let keyset_id = Uuid::new_v4();
857 let req = CreateClientRequest {
858 keyset_id: Some(IdentifiedBy::Uuid(keyset_id)),
859 name: "my-client".into(),
860 description: "desc".into(),
861 };
862
863 let serialized = serde_json::to_value(&req).unwrap();
864 assert!(serialized.get("keyset_id").is_some());
865
866 let deserialized: CreateClientRequest = serde_json::from_value(serialized).unwrap();
867 assert_eq!(deserialized.keyset_id, Some(IdentifiedBy::Uuid(keyset_id)));
868 }
869
870 #[test]
871 fn without_keyset_id_round_trips() {
872 let req = CreateClientRequest {
873 keyset_id: None,
874 name: "my-client".into(),
875 description: "desc".into(),
876 };
877
878 let serialized = serde_json::to_value(&req).unwrap();
879 assert!(serialized.get("keyset_id").is_none());
880
881 let deserialized: CreateClientRequest = serde_json::from_value(serialized).unwrap();
882 assert_eq!(deserialized.keyset_id, None);
883 }
884
885 #[test]
886 fn backwards_compatible_with_dataset_id() {
887 let dataset_id = Uuid::new_v4();
888 let json = json!({
889 "dataset_id": dataset_id,
890 "name": "old-client",
891 "description": "old desc",
892 });
893
894 let req: CreateClientRequest = serde_json::from_value(json).unwrap();
895 assert_eq!(req.keyset_id, Some(IdentifiedBy::Uuid(dataset_id)));
896 }
897
898 #[test]
899 fn omitted_keyset_id_defaults_to_none() {
900 let json = json!({
901 "name": "no-keyset",
902 "description": "no keyset",
903 });
904
905 let req: CreateClientRequest = serde_json::from_value(json).unwrap();
906 assert_eq!(req.keyset_id, None);
907 }
908 }
909
910 mod create_keyset_request_validation {
911 use crate::CreateKeysetRequest;
912 use validator::Validate;
913
914 fn valid_request() -> CreateKeysetRequest<'static> {
915 CreateKeysetRequest {
916 name: "my-keyset".into(),
917 description: "A test keyset".into(),
918 client: None,
919 }
920 }
921
922 #[test]
923 fn valid_request_passes() {
924 assert!(valid_request().validate().is_ok());
925 }
926
927 #[test]
928 fn empty_name_fails() {
929 let req = CreateKeysetRequest {
930 name: "".into(),
931 ..valid_request()
932 };
933 let errors = req.validate().unwrap_err();
934 assert!(errors.field_errors().contains_key("name"));
935 }
936
937 #[test]
938 fn name_over_64_chars_fails() {
939 let req = CreateKeysetRequest {
940 name: "a".repeat(65).into(),
941 ..valid_request()
942 };
943 let errors = req.validate().unwrap_err();
944 assert!(errors.field_errors().contains_key("name"));
945 }
946
947 #[test]
948 fn reserved_default_name_fails() {
949 let req = CreateKeysetRequest {
950 name: "default".into(),
951 ..valid_request()
952 };
953 let errors = req.validate().unwrap_err();
954 let name_errors = &errors.field_errors()["name"];
955 assert!(name_errors.iter().any(|e| e.code == "reserved_name"));
956 }
957
958 #[test]
959 fn reserved_default_name_case_insensitive() {
960 let req = CreateKeysetRequest {
961 name: "DEFAULT".into(),
962 ..valid_request()
963 };
964 assert!(req.validate().is_err());
965 }
966
967 #[test]
968 fn name_with_invalid_characters_fails() {
969 let req = CreateKeysetRequest {
970 name: "has spaces".into(),
971 ..valid_request()
972 };
973 let errors = req.validate().unwrap_err();
974 let name_errors = &errors.field_errors()["name"];
975 assert!(name_errors.iter().any(|e| e.code == "invalid_characters"));
976 }
977
978 #[test]
979 fn name_with_special_chars_fails() {
980 for name in ["test@keyset", "test!keyset", "test.keyset", "test%keyset"] {
981 let req = CreateKeysetRequest {
982 name: name.into(),
983 ..valid_request()
984 };
985 assert!(
986 req.validate().is_err(),
987 "expected {name} to fail validation"
988 );
989 }
990 }
991
992 #[test]
993 fn name_with_allowed_chars_passes() {
994 for name in ["my-keyset", "my_keyset", "my/keyset", "MyKeyset123"] {
995 let req = CreateKeysetRequest {
996 name: name.into(),
997 ..valid_request()
998 };
999 assert!(req.validate().is_ok(), "expected {name} to pass validation");
1000 }
1001 }
1002
1003 #[test]
1004 fn empty_description_fails() {
1005 let req = CreateKeysetRequest {
1006 description: "".into(),
1007 ..valid_request()
1008 };
1009 let errors = req.validate().unwrap_err();
1010 assert!(errors.field_errors().contains_key("description"));
1011 }
1012
1013 #[test]
1014 fn description_over_256_chars_fails() {
1015 let req = CreateKeysetRequest {
1016 description: "a".repeat(257).into(),
1017 ..valid_request()
1018 };
1019 let errors = req.validate().unwrap_err();
1020 assert!(errors.field_errors().contains_key("description"));
1021 }
1022
1023 #[test]
1024 fn description_at_256_chars_passes() {
1025 let req = CreateKeysetRequest {
1026 description: "a".repeat(256).into(),
1027 ..valid_request()
1028 };
1029 assert!(req.validate().is_ok());
1030 }
1031
1032 #[test]
1033 fn nested_client_name_validation() {
1034 use crate::{ClientType, CreateClientSpec};
1035
1036 let req = CreateKeysetRequest {
1037 name: "my-keyset".into(),
1038 description: "desc".into(),
1039 client: Some(CreateClientSpec {
1040 client_type: ClientType::Device,
1041 name: "".into(),
1042 }),
1043 };
1044 let errors = req.validate().unwrap_err();
1045 assert!(
1046 errors.errors().contains_key("client"),
1047 "expected nested client validation error"
1048 );
1049 }
1050 }
1051
1052 mod openapi_schema {
1053 use crate::{CreateClientSpec, CreateKeysetRequest};
1054 use utoipa::PartialSchema;
1055
1056 fn schema_json<T: PartialSchema>() -> serde_json::Value {
1057 serde_json::to_value(T::schema()).unwrap()
1058 }
1059
1060 #[test]
1061 fn create_keyset_request_name_has_constraints() {
1062 let schema = schema_json::<CreateKeysetRequest>();
1063 let name = &schema["properties"]["name"];
1064
1065 assert_eq!(name["minLength"], 1);
1066 assert_eq!(name["maxLength"], 64);
1067 assert_eq!(name["pattern"], r"^[A-Za-z0-9_\-/]+$");
1068 }
1069
1070 #[test]
1071 fn create_keyset_request_description_has_constraints() {
1072 let schema = schema_json::<CreateKeysetRequest>();
1073 let desc = &schema["properties"]["description"];
1074
1075 assert_eq!(desc["minLength"], 1);
1076 assert_eq!(desc["maxLength"], 256);
1077 }
1078
1079 #[test]
1080 fn create_client_spec_name_has_constraints() {
1081 let schema = schema_json::<CreateClientSpec>();
1082 let name = &schema["properties"]["name"];
1083
1084 assert_eq!(name["minLength"], 1);
1085 assert_eq!(name["maxLength"], 64);
1086 }
1087 }
1088
1089 mod backwards_compatible_deserialisation {
1090 use super::*;
1091
1092 #[test]
1093 fn when_dataset_id_is_uuid() {
1094 let client_id = Uuid::new_v4();
1095 let dataset_id = Uuid::new_v4();
1096
1097 let json = json!({
1098 "client_id": client_id,
1099 "dataset_id": dataset_id,
1100 });
1101
1102 let req: LoadKeysetRequest = serde_json::from_value(json).unwrap();
1103
1104 assert_eq!(
1105 req,
1106 LoadKeysetRequest {
1107 client_id,
1108 keyset_id: Some(IdentifiedBy::Uuid(dataset_id))
1109 }
1110 );
1111 }
1112
1113 #[test]
1114 fn when_keyset_id_is_uuid() {
1115 let client_id = Uuid::new_v4();
1116 let keyset_id = Uuid::new_v4();
1117
1118 let json = json!({
1119 "client_id": client_id,
1120 "keyset_id": keyset_id,
1121 });
1122
1123 let req: LoadKeysetRequest = serde_json::from_value(json).unwrap();
1124
1125 assert_eq!(
1126 req,
1127 LoadKeysetRequest {
1128 client_id,
1129 keyset_id: Some(IdentifiedBy::Uuid(keyset_id))
1130 }
1131 );
1132 }
1133
1134 #[test]
1135 fn when_dataset_id_is_id_name() {
1136 let client_id = Uuid::new_v4();
1137 let dataset_id = IdentifiedBy::Name(Name::new_untrusted("some-dataset-name"));
1138
1139 let json = json!({
1140 "client_id": client_id,
1141 "dataset_id": dataset_id,
1142 });
1143
1144 let req: LoadKeysetRequest = serde_json::from_value(json).unwrap();
1145
1146 assert_eq!(
1147 req,
1148 LoadKeysetRequest {
1149 client_id,
1150 keyset_id: Some(dataset_id)
1151 }
1152 );
1153 }
1154 }
1155}