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}
254
255impl ViturResponse for Vec<KeysetClient> {}
256
257#[derive(Debug, Serialize, Deserialize, ToSchema)]
262pub struct DeleteClientRequest {
263 pub client_id: Uuid,
264}
265
266impl ViturRequest for DeleteClientRequest {
267 type Response = DeleteClientResponse;
268
269 const ENDPOINT: &'static str = "delete-client";
270 const SCOPE: Scope = Scope::with_permission(Permission::Client(ClientPermission::Delete));
271}
272
273#[derive(Default, Debug, Serialize, Deserialize, ToSchema)]
274pub struct DeleteClientResponse {}
275
276impl ViturResponse for DeleteClientResponse {}
277
278#[derive(Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
280pub struct ViturKeyMaterial(#[serde(with = "base64_vec")] Vec<u8>);
281opaque_debug::implement!(ViturKeyMaterial);
282
283impl From<Vec<u8>> for ViturKeyMaterial {
284 fn from(inner: Vec<u8>) -> Self {
285 Self(inner)
286 }
287}
288
289impl Deref for ViturKeyMaterial {
290 type Target = [u8];
291
292 fn deref(&self) -> &Self::Target {
293 &self.0
294 }
295}
296
297#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Zeroize)]
298#[serde(transparent)]
299pub struct KeyId(#[serde(with = "base64_array")] [u8; 16]);
300
301impl KeyId {
302 pub fn into_inner(self) -> [u8; 16] {
303 self.0
304 }
305}
306
307impl Display for KeyId {
308 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
309 write!(f, "{}", const_hex::encode(self.0))
310 }
311}
312
313impl From<[u8; 16]> for KeyId {
314 fn from(inner: [u8; 16]) -> Self {
315 Self(inner)
316 }
317}
318
319impl AsRef<[u8; 16]> for KeyId {
320 fn as_ref(&self) -> &[u8; 16] {
321 &self.0
322 }
323}
324
325#[derive(Debug, Serialize, Deserialize, ToSchema)]
329pub struct GeneratedKey {
330 #[schema(value_type = String, format = Byte)]
331 pub key_material: ViturKeyMaterial,
332 #[serde(with = "base64_vec")]
334 #[schema(value_type = String, format = Byte)]
335 pub tag: Vec<u8>,
336}
337
338#[derive(Debug, Serialize, Deserialize, ToSchema)]
340pub struct GenerateKeyResponse {
341 pub keys: Vec<GeneratedKey>,
342}
343
344impl ViturResponse for GenerateKeyResponse {}
345
346#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
348pub struct GenerateKeySpec<'a> {
349 #[serde(alias = "id")]
351 #[schema(value_type = String, format = Byte)]
352 pub iv: KeyId,
353 #[schema(value_type = String)]
355 pub descriptor: Cow<'a, str>,
356
357 #[serde(default)]
358 #[schema(value_type = Vec<Context>)]
359 pub context: Cow<'a, [Context]>,
360}
361
362impl<'a> GenerateKeySpec<'a> {
363 pub fn new(iv: [u8; 16], descriptor: &'a str) -> Self {
364 Self {
365 iv: KeyId(iv),
366 descriptor: Cow::from(descriptor),
367 context: Default::default(),
368 }
369 }
370
371 pub fn new_with_context(
372 iv: [u8; 16],
373 descriptor: &'a str,
374 context: Cow<'a, [Context]>,
375 ) -> Self {
376 Self {
377 iv: KeyId(iv),
378 descriptor: Cow::from(descriptor),
379 context,
380 }
381 }
382}
383#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
387pub enum Context {
389 Tag(String),
391
392 Value(String, String),
395
396 #[serde(alias = "identityClaim")]
401 IdentityClaim(String),
402}
403
404impl Context {
405 pub fn new_tag(tag: impl Into<String>) -> Self {
406 Self::Tag(tag.into())
407 }
408
409 pub fn new_value(key: impl Into<String>, value: impl Into<String>) -> Self {
410 Self::Value(key.into(), value.into())
411 }
412
413 pub fn new_identity_claim(claim: &str) -> Self {
414 Self::IdentityClaim(claim.to_string())
415 }
416}
417
418#[derive(Debug, Serialize, Deserialize, ToSchema)]
426pub struct GenerateKeyRequest<'a> {
427 pub client_id: Uuid,
428 #[serde(alias = "dataset_id")]
429 #[schema(value_type = Option<String>, example = "550e8400-e29b-41d4-a716-446655440000")]
430 pub keyset_id: Option<IdentifiedBy>,
431 #[schema(value_type = Vec<GenerateKeySpec>)]
432 pub keys: Cow<'a, [GenerateKeySpec<'a>]>,
433 #[serde(default)]
434 #[schema(value_type = Object)]
435 pub unverified_context: Cow<'a, UnverifiedContext>,
436}
437
438impl ViturRequest for GenerateKeyRequest<'_> {
439 type Response = GenerateKeyResponse;
440
441 const ENDPOINT: &'static str = "generate-data-key";
442 const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Generate));
443}
444
445#[derive(Debug, Serialize, Deserialize, ToSchema)]
447pub struct RetrievedKey {
448 #[schema(value_type = String, format = Byte)]
450 pub key_material: ViturKeyMaterial,
451}
452
453#[derive(Debug, Serialize, Deserialize, ToSchema)]
456pub struct RetrieveKeyResponse {
457 pub keys: Vec<RetrievedKey>,
458}
459
460impl ViturResponse for RetrieveKeyResponse {}
461
462#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
464pub struct RetrieveKeySpec<'a> {
465 #[serde(alias = "id")]
466 #[schema(value_type = String, format = Byte)]
467 pub iv: KeyId,
468 #[schema(value_type = String)]
470 pub descriptor: Cow<'a, str>,
471 #[schema(value_type = String, format = Byte)]
472 pub tag: Cow<'a, [u8]>,
473
474 #[serde(default)]
475 #[schema(value_type = Vec<Context>)]
476 pub context: Cow<'a, [Context]>,
477
478 #[serde(default)]
481 pub tag_version: usize,
482}
483
484impl<'a> RetrieveKeySpec<'a> {
485 const DEFAULT_TAG_VERSION: usize = 0;
486
487 pub fn new(id: KeyId, tag: &'a [u8], descriptor: &'a str) -> Self {
488 Self {
489 iv: id,
490 descriptor: Cow::from(descriptor),
491 tag: Cow::from(tag),
492 context: Cow::Owned(Vec::new()),
493 tag_version: Self::DEFAULT_TAG_VERSION,
494 }
495 }
496
497 pub fn with_context(mut self, context: Cow<'a, [Context]>) -> Self {
498 self.context = context;
499 self
500 }
501}
502
503#[derive(Debug, Serialize, Deserialize, ToSchema)]
509pub struct RetrieveKeyRequest<'a> {
510 pub client_id: Uuid,
511 #[serde(alias = "dataset_id")]
512 #[schema(value_type = Option<String>, example = "550e8400-e29b-41d4-a716-446655440000")]
513 pub keyset_id: Option<IdentifiedBy>,
514 #[schema(value_type = Vec<RetrieveKeySpec>)]
515 pub keys: Cow<'a, [RetrieveKeySpec<'a>]>,
516 #[serde(default)]
517 #[schema(value_type = Object)]
518 pub unverified_context: UnverifiedContext,
519}
520
521impl ViturRequest for RetrieveKeyRequest<'_> {
522 type Response = RetrieveKeyResponse;
523
524 const ENDPOINT: &'static str = "retrieve-data-key";
525 const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Retrieve));
526}
527
528#[derive(Debug, Serialize, Deserialize)]
534pub struct RetrieveKeyRequestFallible<'a> {
535 pub client_id: Uuid,
536 #[serde(alias = "dataset_id")]
537 pub keyset_id: Option<IdentifiedBy>,
538 pub keys: Cow<'a, [RetrieveKeySpec<'a>]>,
539 #[serde(default)]
540 pub unverified_context: Cow<'a, UnverifiedContext>,
541}
542
543impl ViturRequest for RetrieveKeyRequestFallible<'_> {
544 type Response = RetrieveKeyResponseFallible;
545
546 const ENDPOINT: &'static str = "retrieve-data-key-fallible";
547 const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Retrieve));
548}
549
550#[derive(Debug, Serialize, Deserialize, ToSchema)]
552pub struct RetrieveKeyResponseFallible {
553 #[schema(value_type = Vec<serde_json::Value>)]
554 pub keys: Vec<Result<RetrievedKey, String>>, }
556
557impl ViturResponse for RetrieveKeyResponseFallible {}
558
559#[derive(Debug, Serialize, Deserialize, ToSchema)]
563pub struct DisableKeysetRequest {
564 #[serde(alias = "dataset_id")]
566 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
567 pub keyset_id: IdentifiedBy,
568}
569
570impl ViturRequest for DisableKeysetRequest {
571 type Response = EmptyResponse;
572
573 const ENDPOINT: &'static str = "disable-keyset";
574 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Disable));
575}
576
577#[derive(Debug, Serialize, Deserialize, ToSchema)]
581pub struct EnableKeysetRequest {
582 #[serde(alias = "dataset_id")]
584 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
585 pub keyset_id: IdentifiedBy,
586}
587
588impl ViturRequest for EnableKeysetRequest {
589 type Response = EmptyResponse;
590
591 const ENDPOINT: &'static str = "enable-keyset";
592 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Enable));
593}
594
595#[derive(Debug, Serialize, Deserialize, ToSchema)]
601pub struct ModifyKeysetRequest<'a> {
602 #[serde(alias = "dataset_id")]
604 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
605 pub keyset_id: IdentifiedBy,
606 #[schema(value_type = Option<String>)]
608 pub name: Option<Cow<'a, str>>,
609 #[schema(value_type = Option<String>)]
611 pub description: Option<Cow<'a, str>>,
612}
613
614impl ViturRequest for ModifyKeysetRequest<'_> {
615 type Response = EmptyResponse;
616
617 const ENDPOINT: &'static str = "modify-keyset";
618 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Modify));
619}
620
621#[derive(Debug, Serialize, Deserialize, ToSchema)]
626pub struct GrantKeysetRequest {
627 pub client_id: Uuid,
628 #[serde(alias = "dataset_id")]
630 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
631 pub keyset_id: IdentifiedBy,
632}
633
634impl ViturRequest for GrantKeysetRequest {
635 type Response = EmptyResponse;
636
637 const ENDPOINT: &'static str = "grant-keyset";
638 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Grant));
639}
640
641#[derive(Debug, Serialize, Deserialize, ToSchema)]
645pub struct RevokeKeysetRequest {
646 pub client_id: Uuid,
647 #[serde(alias = "dataset_id")]
649 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
650 pub keyset_id: IdentifiedBy,
651}
652
653impl ViturRequest for RevokeKeysetRequest {
654 type Response = EmptyResponse;
655
656 const ENDPOINT: &'static str = "revoke-keyset";
657 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Revoke));
658}
659
660#[derive(Debug, Serialize, Deserialize, PartialEq, PartialOrd, ToSchema)]
669pub struct LoadKeysetRequest {
670 pub client_id: Uuid,
671 #[serde(alias = "dataset_id")]
673 #[schema(value_type = Option<String>, example = "550e8400-e29b-41d4-a716-446655440000")]
674 pub keyset_id: Option<IdentifiedBy>,
675}
676
677impl ViturRequest for LoadKeysetRequest {
678 type Response = LoadKeysetResponse;
679
680 const ENDPOINT: &'static str = "load-keyset";
681
682 const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Retrieve));
686}
687
688#[derive(Debug, Serialize, Deserialize, ToSchema)]
692pub struct LoadKeysetResponse {
693 pub partial_index_key: RetrievedKey,
694 #[serde(rename = "dataset")]
695 pub keyset: Keyset,
696}
697
698impl ViturResponse for LoadKeysetResponse {}
699
700#[cfg(test)]
701mod test {
702 use serde_json::json;
703 use uuid::Uuid;
704
705 use crate::{CreateKeysetResponse, CreatedClient, IdentifiedBy, LoadKeysetRequest, Name};
706
707 mod create_keyset_response_serialization {
708 use super::*;
709 use crate::{Keyset, ViturKeyMaterial};
710
711 #[test]
712 fn without_client_is_flat_keyset() {
713 let id = Uuid::new_v4();
714 let response = CreateKeysetResponse {
715 keyset: Keyset {
716 id,
717 name: "test-keyset".into(),
718 description: "A test keyset".into(),
719 is_disabled: false,
720 is_default: false,
721 },
722 client: None,
723 };
724
725 let serialized = serde_json::to_value(&response).unwrap();
726
727 assert_eq!(
729 serialized,
730 json!({
731 "id": id,
732 "name": "test-keyset",
733 "description": "A test keyset",
734 "is_disabled": false,
735 "is_default": false,
736 })
737 );
738
739 let deserialized: CreateKeysetResponse = serde_json::from_value(serialized).unwrap();
741 assert_eq!(deserialized.keyset.id, id);
742 assert!(deserialized.client.is_none());
743 }
744
745 #[test]
746 fn with_client_includes_client_field() {
747 let keyset_id = Uuid::new_v4();
748 let client_id = Uuid::new_v4();
749
750 let response = CreateKeysetResponse {
751 keyset: Keyset {
752 id: keyset_id,
753 name: "device-keyset".into(),
754 description: "Keyset with device client".into(),
755 is_disabled: false,
756 is_default: false,
757 },
758 client: Some(CreatedClient {
759 id: client_id,
760 client_key: ViturKeyMaterial::from(vec![1, 2, 3, 4]),
761 }),
762 };
763
764 let serialized = serde_json::to_value(&response).unwrap();
765
766 assert_eq!(
768 serialized,
769 json!({
770 "id": keyset_id,
771 "name": "device-keyset",
772 "description": "Keyset with device client",
773 "is_disabled": false,
774 "is_default": false,
775 "client": {
776 "id": client_id,
777 "client_key": "AQIDBA==",
778 },
779 })
780 );
781
782 let deserialized: CreateKeysetResponse = serde_json::from_value(serialized).unwrap();
784 assert_eq!(deserialized.keyset.id, keyset_id);
785 let created_client = deserialized.client.unwrap();
786 assert_eq!(created_client.id, client_id);
787 assert_eq!(&*created_client.client_key, &[1, 2, 3, 4]);
788 }
789 }
790
791 mod create_client_request_serialization {
792 use super::*;
793 use crate::CreateClientRequest;
794
795 #[test]
796 fn with_keyset_id_round_trips() {
797 let keyset_id = Uuid::new_v4();
798 let req = CreateClientRequest {
799 keyset_id: Some(IdentifiedBy::Uuid(keyset_id)),
800 name: "my-client".into(),
801 description: "desc".into(),
802 };
803
804 let serialized = serde_json::to_value(&req).unwrap();
805 assert!(serialized.get("keyset_id").is_some());
806
807 let deserialized: CreateClientRequest = serde_json::from_value(serialized).unwrap();
808 assert_eq!(deserialized.keyset_id, Some(IdentifiedBy::Uuid(keyset_id)));
809 }
810
811 #[test]
812 fn without_keyset_id_round_trips() {
813 let req = CreateClientRequest {
814 keyset_id: None,
815 name: "my-client".into(),
816 description: "desc".into(),
817 };
818
819 let serialized = serde_json::to_value(&req).unwrap();
820 assert!(serialized.get("keyset_id").is_none());
821
822 let deserialized: CreateClientRequest = serde_json::from_value(serialized).unwrap();
823 assert_eq!(deserialized.keyset_id, None);
824 }
825
826 #[test]
827 fn backwards_compatible_with_dataset_id() {
828 let dataset_id = Uuid::new_v4();
829 let json = json!({
830 "dataset_id": dataset_id,
831 "name": "old-client",
832 "description": "old desc",
833 });
834
835 let req: CreateClientRequest = serde_json::from_value(json).unwrap();
836 assert_eq!(req.keyset_id, Some(IdentifiedBy::Uuid(dataset_id)));
837 }
838
839 #[test]
840 fn omitted_keyset_id_defaults_to_none() {
841 let json = json!({
842 "name": "no-keyset",
843 "description": "no keyset",
844 });
845
846 let req: CreateClientRequest = serde_json::from_value(json).unwrap();
847 assert_eq!(req.keyset_id, None);
848 }
849 }
850
851 mod create_keyset_request_validation {
852 use crate::CreateKeysetRequest;
853 use validator::Validate;
854
855 fn valid_request() -> CreateKeysetRequest<'static> {
856 CreateKeysetRequest {
857 name: "my-keyset".into(),
858 description: "A test keyset".into(),
859 client: None,
860 }
861 }
862
863 #[test]
864 fn valid_request_passes() {
865 assert!(valid_request().validate().is_ok());
866 }
867
868 #[test]
869 fn empty_name_fails() {
870 let req = CreateKeysetRequest {
871 name: "".into(),
872 ..valid_request()
873 };
874 let errors = req.validate().unwrap_err();
875 assert!(errors.field_errors().contains_key("name"));
876 }
877
878 #[test]
879 fn name_over_64_chars_fails() {
880 let req = CreateKeysetRequest {
881 name: "a".repeat(65).into(),
882 ..valid_request()
883 };
884 let errors = req.validate().unwrap_err();
885 assert!(errors.field_errors().contains_key("name"));
886 }
887
888 #[test]
889 fn reserved_default_name_fails() {
890 let req = CreateKeysetRequest {
891 name: "default".into(),
892 ..valid_request()
893 };
894 let errors = req.validate().unwrap_err();
895 let name_errors = &errors.field_errors()["name"];
896 assert!(name_errors.iter().any(|e| e.code == "reserved_name"));
897 }
898
899 #[test]
900 fn reserved_default_name_case_insensitive() {
901 let req = CreateKeysetRequest {
902 name: "DEFAULT".into(),
903 ..valid_request()
904 };
905 assert!(req.validate().is_err());
906 }
907
908 #[test]
909 fn name_with_invalid_characters_fails() {
910 let req = CreateKeysetRequest {
911 name: "has spaces".into(),
912 ..valid_request()
913 };
914 let errors = req.validate().unwrap_err();
915 let name_errors = &errors.field_errors()["name"];
916 assert!(name_errors.iter().any(|e| e.code == "invalid_characters"));
917 }
918
919 #[test]
920 fn name_with_special_chars_fails() {
921 for name in ["test@keyset", "test!keyset", "test.keyset", "test%keyset"] {
922 let req = CreateKeysetRequest {
923 name: name.into(),
924 ..valid_request()
925 };
926 assert!(
927 req.validate().is_err(),
928 "expected {name} to fail validation"
929 );
930 }
931 }
932
933 #[test]
934 fn name_with_allowed_chars_passes() {
935 for name in ["my-keyset", "my_keyset", "my/keyset", "MyKeyset123"] {
936 let req = CreateKeysetRequest {
937 name: name.into(),
938 ..valid_request()
939 };
940 assert!(req.validate().is_ok(), "expected {name} to pass validation");
941 }
942 }
943
944 #[test]
945 fn empty_description_fails() {
946 let req = CreateKeysetRequest {
947 description: "".into(),
948 ..valid_request()
949 };
950 let errors = req.validate().unwrap_err();
951 assert!(errors.field_errors().contains_key("description"));
952 }
953
954 #[test]
955 fn description_over_256_chars_fails() {
956 let req = CreateKeysetRequest {
957 description: "a".repeat(257).into(),
958 ..valid_request()
959 };
960 let errors = req.validate().unwrap_err();
961 assert!(errors.field_errors().contains_key("description"));
962 }
963
964 #[test]
965 fn description_at_256_chars_passes() {
966 let req = CreateKeysetRequest {
967 description: "a".repeat(256).into(),
968 ..valid_request()
969 };
970 assert!(req.validate().is_ok());
971 }
972
973 #[test]
974 fn nested_client_name_validation() {
975 use crate::{ClientType, CreateClientSpec};
976
977 let req = CreateKeysetRequest {
978 name: "my-keyset".into(),
979 description: "desc".into(),
980 client: Some(CreateClientSpec {
981 client_type: ClientType::Device,
982 name: "".into(),
983 }),
984 };
985 let errors = req.validate().unwrap_err();
986 assert!(
987 errors.errors().contains_key("client"),
988 "expected nested client validation error"
989 );
990 }
991 }
992
993 mod openapi_schema {
994 use crate::{CreateClientSpec, CreateKeysetRequest};
995 use utoipa::PartialSchema;
996
997 fn schema_json<T: PartialSchema>() -> serde_json::Value {
998 serde_json::to_value(T::schema()).unwrap()
999 }
1000
1001 #[test]
1002 fn create_keyset_request_name_has_constraints() {
1003 let schema = schema_json::<CreateKeysetRequest>();
1004 let name = &schema["properties"]["name"];
1005
1006 assert_eq!(name["minLength"], 1);
1007 assert_eq!(name["maxLength"], 64);
1008 assert_eq!(name["pattern"], r"^[A-Za-z0-9_\-/]+$");
1009 }
1010
1011 #[test]
1012 fn create_keyset_request_description_has_constraints() {
1013 let schema = schema_json::<CreateKeysetRequest>();
1014 let desc = &schema["properties"]["description"];
1015
1016 assert_eq!(desc["minLength"], 1);
1017 assert_eq!(desc["maxLength"], 256);
1018 }
1019
1020 #[test]
1021 fn create_client_spec_name_has_constraints() {
1022 let schema = schema_json::<CreateClientSpec>();
1023 let name = &schema["properties"]["name"];
1024
1025 assert_eq!(name["minLength"], 1);
1026 assert_eq!(name["maxLength"], 64);
1027 }
1028 }
1029
1030 mod backwards_compatible_deserialisation {
1031 use super::*;
1032
1033 #[test]
1034 fn when_dataset_id_is_uuid() {
1035 let client_id = Uuid::new_v4();
1036 let dataset_id = Uuid::new_v4();
1037
1038 let json = json!({
1039 "client_id": client_id,
1040 "dataset_id": dataset_id,
1041 });
1042
1043 let req: LoadKeysetRequest = serde_json::from_value(json).unwrap();
1044
1045 assert_eq!(
1046 req,
1047 LoadKeysetRequest {
1048 client_id,
1049 keyset_id: Some(IdentifiedBy::Uuid(dataset_id))
1050 }
1051 );
1052 }
1053
1054 #[test]
1055 fn when_keyset_id_is_uuid() {
1056 let client_id = Uuid::new_v4();
1057 let keyset_id = Uuid::new_v4();
1058
1059 let json = json!({
1060 "client_id": client_id,
1061 "keyset_id": keyset_id,
1062 });
1063
1064 let req: LoadKeysetRequest = serde_json::from_value(json).unwrap();
1065
1066 assert_eq!(
1067 req,
1068 LoadKeysetRequest {
1069 client_id,
1070 keyset_id: Some(IdentifiedBy::Uuid(keyset_id))
1071 }
1072 );
1073 }
1074
1075 #[test]
1076 fn when_dataset_id_is_id_name() {
1077 let client_id = Uuid::new_v4();
1078 let dataset_id = IdentifiedBy::Name(Name::new_untrusted("some-dataset-name"));
1079
1080 let json = json!({
1081 "client_id": client_id,
1082 "dataset_id": dataset_id,
1083 });
1084
1085 let req: LoadKeysetRequest = serde_json::from_value(json).unwrap();
1086
1087 assert_eq!(
1088 req,
1089 LoadKeysetRequest {
1090 client_id,
1091 keyset_id: Some(dataset_id)
1092 }
1093 );
1094 }
1095 }
1096}