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