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 const MAX_DESCRIPTOR_LEN: usize = 512;
40
41pub trait ViturResponse: Serialize + for<'de> Deserialize<'de> + Send {}
42
43pub trait ViturRequest: Serialize + for<'de> Deserialize<'de> + Sized + Send {
44 type Response: ViturResponse;
45
46 const SCOPE: Scope;
47 const ENDPOINT: &'static str;
48}
49
50#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
52#[serde(rename_all = "snake_case")]
53pub enum ClientType {
54 Device,
55}
56
57#[derive(Debug, Serialize, Deserialize, Validate, ToSchema)]
59pub struct CreateClientSpec<'a> {
60 pub client_type: ClientType,
61 #[validate(length(min = 1, max = 64))]
63 #[schema(value_type = String, min_length = 1, max_length = 64)]
64 pub name: Cow<'a, str>,
65}
66
67#[derive(Debug, Serialize, Deserialize, ToSchema)]
69pub struct CreatedClient {
70 pub id: Uuid,
71 #[schema(value_type = String, format = Byte)]
73 pub client_key: ViturKeyMaterial,
74}
75
76#[derive(Debug, Serialize, Deserialize, ToSchema)]
80pub struct CreateKeysetResponse {
81 #[serde(flatten)]
82 pub keyset: Keyset,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub client: Option<CreatedClient>,
85}
86
87impl ViturResponse for CreateKeysetResponse {}
88
89fn validate_keyset_name(name: &str) -> Result<(), validator::ValidationError> {
90 if name.eq_ignore_ascii_case("default") {
91 let mut err = validator::ValidationError::new("reserved_name");
92 err.message =
93 Some("the name 'default' is reserved for the workspace default keyset".into());
94 return Err(err);
95 }
96 if !name
97 .chars()
98 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '/')
99 {
100 let mut err = validator::ValidationError::new("invalid_characters");
101 err.message = Some("name must only contain: A-Z a-z 0-9 _ - /".into());
102 return Err(err);
103 }
104 Ok(())
105}
106
107#[derive(Debug, Serialize, Deserialize, Validate, ToSchema)]
111pub struct CreateKeysetRequest<'a> {
112 #[validate(length(min = 1, max = 64), custom(function = "validate_keyset_name"))]
115 #[schema(value_type = String, min_length = 1, max_length = 64, pattern = r"^[A-Za-z0-9_\-/]+$")]
116 pub name: Cow<'a, str>,
117 #[validate(length(min = 1, max = 256))]
119 #[schema(value_type = String, min_length = 1, max_length = 256)]
120 pub description: Cow<'a, str>,
121 #[validate(nested)]
122 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub client: Option<CreateClientSpec<'a>>,
124}
125
126impl ViturRequest for CreateKeysetRequest<'_> {
127 type Response = CreateKeysetResponse;
128
129 const ENDPOINT: &'static str = "create-keyset";
130 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Create));
131}
132
133#[derive(Default, Debug, Serialize, Deserialize, ToSchema)]
138pub struct ListKeysetRequest {
139 #[serde(default)]
140 pub show_disabled: bool,
141}
142
143impl ViturRequest for ListKeysetRequest {
144 type Response = Vec<Keyset>;
145
146 const ENDPOINT: &'static str = "list-keysets";
147 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::List));
148}
149
150#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
153pub struct Keyset {
154 pub id: Uuid,
155 pub name: String,
156 pub description: String,
157 pub is_disabled: bool,
158 #[serde(default)]
159 pub is_default: bool,
160}
161
162impl ViturResponse for Vec<Keyset> {}
163
164#[derive(Default, Debug, Serialize, Deserialize, ToSchema)]
166pub struct EmptyResponse {}
167
168impl ViturResponse for EmptyResponse {}
169
170#[derive(Debug, Serialize, Deserialize, ToSchema)]
177pub struct CreateClientRequest<'a> {
178 #[serde(alias = "dataset_id", default, skip_serializing_if = "Option::is_none")]
181 #[schema(value_type = Option<String>, example = "550e8400-e29b-41d4-a716-446655440000")]
182 pub keyset_id: Option<IdentifiedBy>,
183 #[schema(value_type = String)]
185 pub name: Cow<'a, str>,
186 #[schema(value_type = String)]
188 pub description: Cow<'a, str>,
189}
190
191impl ViturRequest for CreateClientRequest<'_> {
192 type Response = CreateClientResponse;
193
194 const ENDPOINT: &'static str = "create-client";
195 const SCOPE: Scope = Scope::with_permission(Permission::Client(ClientPermission::Create));
196}
197
198#[derive(Debug, Serialize, Deserialize, ToSchema)]
203pub struct CreateClientResponse {
204 pub id: Uuid,
206 #[serde(rename = "dataset_id")]
208 pub keyset_id: Uuid,
209 pub name: String,
211 pub description: String,
213 #[schema(value_type = String, format = Byte)]
215 pub client_key: ViturKeyMaterial,
216}
217
218impl ViturResponse for CreateClientResponse {}
219
220#[derive(Debug, Default, Serialize, Deserialize, ToSchema)]
228pub struct ListClientRequest {
229 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub keyset_id: Option<IdentifiedBy>,
234}
235
236impl ViturRequest for ListClientRequest {
237 type Response = Vec<KeysetClient>;
238
239 const ENDPOINT: &'static str = "list-clients";
240 const SCOPE: Scope = Scope::with_permission(Permission::Client(ClientPermission::List));
241}
242
243#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, ToSchema)]
246#[serde(untagged)]
247pub enum ClientKeysetId {
248 Single(Uuid),
249 Multiple(Vec<Uuid>),
250}
251
252impl PartialEq<Uuid> for ClientKeysetId {
254 fn eq(&self, other: &Uuid) -> bool {
255 if let ClientKeysetId::Single(id) = self {
256 id == other
257 } else {
258 false
259 }
260 }
261}
262
263#[derive(Debug, Serialize, Deserialize, ToSchema)]
265pub struct KeysetClient {
266 pub id: Uuid,
267 #[serde(alias = "dataset_id")]
268 pub keyset_id: ClientKeysetId,
269 pub name: String,
270 pub description: String,
271 pub created_by: Option<String>,
272}
273
274impl ViturResponse for Vec<KeysetClient> {}
275
276#[derive(Debug, Serialize, Deserialize, ToSchema)]
281pub struct DeleteClientRequest {
282 pub client_id: Uuid,
283}
284
285impl ViturRequest for DeleteClientRequest {
286 type Response = DeleteClientResponse;
287
288 const ENDPOINT: &'static str = "delete-client";
289 const SCOPE: Scope = Scope::with_permission(Permission::Client(ClientPermission::Delete));
290}
291
292#[derive(Default, Debug, Serialize, Deserialize, ToSchema)]
293pub struct DeleteClientResponse {}
294
295impl ViturResponse for DeleteClientResponse {}
296
297#[derive(Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
299pub struct ViturKeyMaterial(#[serde(with = "base64_vec")] Vec<u8>);
300opaque_debug::implement!(ViturKeyMaterial);
301
302impl From<Vec<u8>> for ViturKeyMaterial {
303 fn from(inner: Vec<u8>) -> Self {
304 Self(inner)
305 }
306}
307
308impl Deref for ViturKeyMaterial {
309 type Target = [u8];
310
311 fn deref(&self) -> &Self::Target {
312 &self.0
313 }
314}
315
316#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, Zeroize)]
317#[serde(transparent)]
318pub struct KeyId(#[serde(with = "base64_array")] [u8; 16]);
319
320impl KeyId {
321 pub fn into_inner(self) -> [u8; 16] {
322 self.0
323 }
324}
325
326impl Display for KeyId {
327 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
328 write!(f, "{}", const_hex::encode(self.0))
329 }
330}
331
332impl From<[u8; 16]> for KeyId {
333 fn from(inner: [u8; 16]) -> Self {
334 Self(inner)
335 }
336}
337
338impl AsRef<[u8; 16]> for KeyId {
339 fn as_ref(&self) -> &[u8; 16] {
340 &self.0
341 }
342}
343
344#[derive(Debug, Serialize, Deserialize, ToSchema)]
348pub struct GeneratedKey {
349 #[schema(value_type = String, format = Byte)]
350 pub key_material: ViturKeyMaterial,
351 #[serde(with = "base64_vec")]
353 #[schema(value_type = String, format = Byte)]
354 pub tag: Vec<u8>,
355 #[serde(default, skip_serializing_if = "Option::is_none")]
357 pub decryption_policy: Option<DecryptionPolicy>,
358}
359
360#[derive(Debug, Serialize, Deserialize, ToSchema)]
362pub struct GenerateKeyResponse {
363 pub keys: Vec<GeneratedKey>,
364}
365
366impl ViturResponse for GenerateKeyResponse {}
367
368#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
370pub struct GenerateKeySpec<'a> {
371 #[serde(alias = "id")]
373 #[schema(value_type = String, format = Byte)]
374 pub iv: KeyId,
375 #[schema(value_type = String)]
377 pub descriptor: Cow<'a, str>,
378
379 #[serde(default)]
380 #[schema(value_type = Vec<Context>)]
381 pub context: Cow<'a, [Context]>,
382
383 #[serde(default, skip_serializing_if = "Option::is_none")]
386 pub decryption_policy: Option<DecryptionPolicy>,
387}
388
389impl<'a> GenerateKeySpec<'a> {
390 pub fn new(iv: [u8; 16], descriptor: &'a str) -> Self {
391 Self {
392 iv: KeyId(iv),
393 descriptor: Cow::from(descriptor),
394 context: Default::default(),
395 decryption_policy: None,
396 }
397 }
398
399 pub fn new_with_context(
400 iv: [u8; 16],
401 descriptor: &'a str,
402 context: Cow<'a, [Context]>,
403 ) -> Self {
404 Self {
405 iv: KeyId(iv),
406 descriptor: Cow::from(descriptor),
407 context,
408 decryption_policy: None,
409 }
410 }
411
412 pub fn new_with_policy(iv: [u8; 16], descriptor: &'a str, policy: DecryptionPolicy) -> Self {
413 Self {
414 iv: KeyId(iv),
415 descriptor: Cow::from(descriptor),
416 context: Default::default(),
417 decryption_policy: Some(policy),
418 }
419 }
420}
421#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
430pub struct PolicyCondition {
431 pub claim: String,
433 #[serde(default, skip_serializing_if = "Option::is_none")]
435 pub value: Option<String>,
436}
437
438#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
444pub struct DecryptionPolicy {
445 pub conditions: Vec<PolicyCondition>,
446}
447
448#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
452pub enum Context {
454 Tag(String),
456
457 Value(String, String),
460
461 #[serde(alias = "identityClaim")]
466 IdentityClaim(String),
467}
468
469impl Context {
470 pub fn new_tag(tag: impl Into<String>) -> Self {
471 Self::Tag(tag.into())
472 }
473
474 pub fn new_value(key: impl Into<String>, value: impl Into<String>) -> Self {
475 Self::Value(key.into(), value.into())
476 }
477
478 pub fn new_identity_claim(claim: &str) -> Self {
479 Self::IdentityClaim(claim.to_string())
480 }
481}
482
483#[derive(Debug, Serialize, Deserialize, ToSchema)]
491pub struct GenerateKeyRequest<'a> {
492 pub client_id: Uuid,
493 #[serde(alias = "dataset_id")]
494 #[schema(value_type = Option<String>, example = "550e8400-e29b-41d4-a716-446655440000")]
495 pub keyset_id: Option<IdentifiedBy>,
496 #[schema(value_type = Vec<GenerateKeySpec>)]
497 pub keys: Cow<'a, [GenerateKeySpec<'a>]>,
498 #[serde(default)]
499 #[schema(value_type = Object)]
500 pub unverified_context: Cow<'a, UnverifiedContext>,
501}
502
503impl ViturRequest for GenerateKeyRequest<'_> {
504 type Response = GenerateKeyResponse;
505
506 const ENDPOINT: &'static str = "generate-data-key";
507 const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Generate));
508}
509
510#[derive(Debug, Serialize, Deserialize, ToSchema)]
512pub struct RetrievedKey {
513 #[schema(value_type = String, format = Byte)]
515 pub key_material: ViturKeyMaterial,
516}
517
518#[derive(Debug, Serialize, Deserialize, ToSchema)]
521pub struct RetrieveKeyResponse {
522 pub keys: Vec<RetrievedKey>,
523}
524
525impl ViturResponse for RetrieveKeyResponse {}
526
527#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
529pub struct RetrieveKeySpec<'a> {
530 #[serde(alias = "id")]
531 #[schema(value_type = String, format = Byte)]
532 pub iv: KeyId,
533 #[schema(value_type = String)]
535 pub descriptor: Cow<'a, str>,
536 #[schema(value_type = String, format = Byte)]
537 pub tag: Cow<'a, [u8]>,
538
539 #[serde(default)]
540 #[schema(value_type = Vec<Context>)]
541 pub context: Cow<'a, [Context]>,
542
543 #[serde(default)]
546 pub tag_version: usize,
547
548 #[serde(default, skip_serializing_if = "Option::is_none")]
551 pub decryption_policy: Option<DecryptionPolicy>,
552}
553
554impl<'a> RetrieveKeySpec<'a> {
555 const DEFAULT_TAG_VERSION: usize = 0;
556
557 pub fn new(id: KeyId, tag: &'a [u8], descriptor: &'a str) -> Self {
558 Self {
559 iv: id,
560 descriptor: Cow::from(descriptor),
561 tag: Cow::from(tag),
562 context: Cow::Owned(Vec::new()),
563 tag_version: Self::DEFAULT_TAG_VERSION,
564 decryption_policy: None,
565 }
566 }
567
568 pub fn with_context(mut self, context: Cow<'a, [Context]>) -> Self {
569 self.context = context;
570 self
571 }
572
573 pub fn with_policy(mut self, policy: DecryptionPolicy) -> Self {
574 self.decryption_policy = Some(policy);
575 self.tag_version = 1;
576 self
577 }
578}
579
580#[derive(Debug, Serialize, Deserialize, ToSchema)]
586pub struct RetrieveKeyRequest<'a> {
587 pub client_id: Uuid,
588 #[serde(alias = "dataset_id")]
589 #[schema(value_type = Option<String>, example = "550e8400-e29b-41d4-a716-446655440000")]
590 pub keyset_id: Option<IdentifiedBy>,
591 #[schema(value_type = Vec<RetrieveKeySpec>)]
592 pub keys: Cow<'a, [RetrieveKeySpec<'a>]>,
593 #[serde(default)]
594 #[schema(value_type = Object)]
595 pub unverified_context: UnverifiedContext,
596}
597
598impl ViturRequest for RetrieveKeyRequest<'_> {
599 type Response = RetrieveKeyResponse;
600
601 const ENDPOINT: &'static str = "retrieve-data-key";
602 const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Retrieve));
603}
604
605#[derive(Debug, Serialize, Deserialize)]
611pub struct RetrieveKeyRequestFallible<'a> {
612 pub client_id: Uuid,
613 #[serde(alias = "dataset_id")]
614 pub keyset_id: Option<IdentifiedBy>,
615 pub keys: Cow<'a, [RetrieveKeySpec<'a>]>,
616 #[serde(default)]
617 pub unverified_context: Cow<'a, UnverifiedContext>,
618}
619
620impl ViturRequest for RetrieveKeyRequestFallible<'_> {
621 type Response = RetrieveKeyResponseFallible;
622
623 const ENDPOINT: &'static str = "retrieve-data-key-fallible";
624 const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Retrieve));
625}
626
627#[derive(Debug, Serialize, Deserialize, ToSchema)]
629pub struct RetrieveKeyResponseFallible {
630 #[schema(value_type = Vec<serde_json::Value>)]
631 pub keys: Vec<Result<RetrievedKey, String>>, }
633
634impl ViturResponse for RetrieveKeyResponseFallible {}
635
636#[derive(Debug, Serialize, Deserialize, ToSchema)]
640pub struct DisableKeysetRequest {
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 DisableKeysetRequest {
648 type Response = EmptyResponse;
649
650 const ENDPOINT: &'static str = "disable-keyset";
651 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Disable));
652}
653
654#[derive(Debug, Serialize, Deserialize, ToSchema)]
658pub struct EnableKeysetRequest {
659 #[serde(alias = "dataset_id")]
661 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
662 pub keyset_id: IdentifiedBy,
663}
664
665impl ViturRequest for EnableKeysetRequest {
666 type Response = EmptyResponse;
667
668 const ENDPOINT: &'static str = "enable-keyset";
669 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Enable));
670}
671
672#[derive(Debug, Serialize, Deserialize, ToSchema)]
678pub struct ModifyKeysetRequest<'a> {
679 #[serde(alias = "dataset_id")]
681 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
682 pub keyset_id: IdentifiedBy,
683 #[schema(value_type = Option<String>)]
685 pub name: Option<Cow<'a, str>>,
686 #[schema(value_type = Option<String>)]
688 pub description: Option<Cow<'a, str>>,
689}
690
691impl ViturRequest for ModifyKeysetRequest<'_> {
692 type Response = EmptyResponse;
693
694 const ENDPOINT: &'static str = "modify-keyset";
695 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Modify));
696}
697
698#[derive(Debug, Serialize, Deserialize, ToSchema)]
703pub struct GrantKeysetRequest {
704 pub client_id: Uuid,
705 #[serde(alias = "dataset_id")]
707 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
708 pub keyset_id: IdentifiedBy,
709}
710
711impl ViturRequest for GrantKeysetRequest {
712 type Response = EmptyResponse;
713
714 const ENDPOINT: &'static str = "grant-keyset";
715 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Grant));
716}
717
718#[derive(Debug, Serialize, Deserialize, ToSchema)]
722pub struct RevokeKeysetRequest {
723 pub client_id: Uuid,
724 #[serde(alias = "dataset_id")]
726 #[schema(value_type = String, example = "550e8400-e29b-41d4-a716-446655440000")]
727 pub keyset_id: IdentifiedBy,
728}
729
730impl ViturRequest for RevokeKeysetRequest {
731 type Response = EmptyResponse;
732
733 const ENDPOINT: &'static str = "revoke-keyset";
734 const SCOPE: Scope = Scope::with_permission(Permission::Keyset(KeysetPermission::Revoke));
735}
736
737#[derive(Debug, Serialize, Deserialize, PartialEq, PartialOrd, ToSchema)]
746pub struct LoadKeysetRequest {
747 pub client_id: Uuid,
748 #[serde(alias = "dataset_id")]
750 #[schema(value_type = Option<String>, example = "550e8400-e29b-41d4-a716-446655440000")]
751 pub keyset_id: Option<IdentifiedBy>,
752}
753
754impl ViturRequest for LoadKeysetRequest {
755 type Response = LoadKeysetResponse;
756
757 const ENDPOINT: &'static str = "load-keyset";
758
759 const SCOPE: Scope = Scope::with_permission(Permission::DataKey(DataKeyPermission::Retrieve));
763}
764
765#[derive(Debug, Serialize, Deserialize, ToSchema)]
769pub struct LoadKeysetResponse {
770 pub partial_index_key: RetrievedKey,
771 #[serde(rename = "dataset")]
772 pub keyset: Keyset,
773}
774
775impl ViturResponse for LoadKeysetResponse {}
776
777#[cfg(test)]
778mod test {
779 use serde_json::json;
780 use uuid::Uuid;
781
782 use crate::{CreateKeysetResponse, CreatedClient, IdentifiedBy, LoadKeysetRequest, Name};
783
784 mod create_keyset_response_serialization {
785 use super::*;
786 use crate::{Keyset, ViturKeyMaterial};
787
788 #[test]
789 fn without_client_is_flat_keyset() {
790 let id = Uuid::new_v4();
791 let response = CreateKeysetResponse {
792 keyset: Keyset {
793 id,
794 name: "test-keyset".into(),
795 description: "A test keyset".into(),
796 is_disabled: false,
797 is_default: false,
798 },
799 client: None,
800 };
801
802 let serialized = serde_json::to_value(&response).unwrap();
803
804 assert_eq!(
806 serialized,
807 json!({
808 "id": id,
809 "name": "test-keyset",
810 "description": "A test keyset",
811 "is_disabled": false,
812 "is_default": false,
813 })
814 );
815
816 let deserialized: CreateKeysetResponse = serde_json::from_value(serialized).unwrap();
818 assert_eq!(deserialized.keyset.id, id);
819 assert!(deserialized.client.is_none());
820 }
821
822 #[test]
823 fn with_client_includes_client_field() {
824 let keyset_id = Uuid::new_v4();
825 let client_id = Uuid::new_v4();
826
827 let response = CreateKeysetResponse {
828 keyset: Keyset {
829 id: keyset_id,
830 name: "device-keyset".into(),
831 description: "Keyset with device client".into(),
832 is_disabled: false,
833 is_default: false,
834 },
835 client: Some(CreatedClient {
836 id: client_id,
837 client_key: ViturKeyMaterial::from(vec![1, 2, 3, 4]),
838 }),
839 };
840
841 let serialized = serde_json::to_value(&response).unwrap();
842
843 assert_eq!(
845 serialized,
846 json!({
847 "id": keyset_id,
848 "name": "device-keyset",
849 "description": "Keyset with device client",
850 "is_disabled": false,
851 "is_default": false,
852 "client": {
853 "id": client_id,
854 "client_key": "AQIDBA==",
855 },
856 })
857 );
858
859 let deserialized: CreateKeysetResponse = serde_json::from_value(serialized).unwrap();
861 assert_eq!(deserialized.keyset.id, keyset_id);
862 let created_client = deserialized.client.unwrap();
863 assert_eq!(created_client.id, client_id);
864 assert_eq!(&*created_client.client_key, &[1, 2, 3, 4]);
865 }
866 }
867
868 mod create_client_request_serialization {
869 use super::*;
870 use crate::CreateClientRequest;
871
872 #[test]
873 fn with_keyset_id_round_trips() {
874 let keyset_id = Uuid::new_v4();
875 let req = CreateClientRequest {
876 keyset_id: Some(IdentifiedBy::Uuid(keyset_id)),
877 name: "my-client".into(),
878 description: "desc".into(),
879 };
880
881 let serialized = serde_json::to_value(&req).unwrap();
882 assert!(serialized.get("keyset_id").is_some());
883
884 let deserialized: CreateClientRequest = serde_json::from_value(serialized).unwrap();
885 assert_eq!(deserialized.keyset_id, Some(IdentifiedBy::Uuid(keyset_id)));
886 }
887
888 #[test]
889 fn without_keyset_id_round_trips() {
890 let req = CreateClientRequest {
891 keyset_id: None,
892 name: "my-client".into(),
893 description: "desc".into(),
894 };
895
896 let serialized = serde_json::to_value(&req).unwrap();
897 assert!(serialized.get("keyset_id").is_none());
898
899 let deserialized: CreateClientRequest = serde_json::from_value(serialized).unwrap();
900 assert_eq!(deserialized.keyset_id, None);
901 }
902
903 #[test]
904 fn backwards_compatible_with_dataset_id() {
905 let dataset_id = Uuid::new_v4();
906 let json = json!({
907 "dataset_id": dataset_id,
908 "name": "old-client",
909 "description": "old desc",
910 });
911
912 let req: CreateClientRequest = serde_json::from_value(json).unwrap();
913 assert_eq!(req.keyset_id, Some(IdentifiedBy::Uuid(dataset_id)));
914 }
915
916 #[test]
917 fn omitted_keyset_id_defaults_to_none() {
918 let json = json!({
919 "name": "no-keyset",
920 "description": "no keyset",
921 });
922
923 let req: CreateClientRequest = serde_json::from_value(json).unwrap();
924 assert_eq!(req.keyset_id, None);
925 }
926 }
927
928 mod create_keyset_request_validation {
929 use crate::CreateKeysetRequest;
930 use validator::Validate;
931
932 fn valid_request() -> CreateKeysetRequest<'static> {
933 CreateKeysetRequest {
934 name: "my-keyset".into(),
935 description: "A test keyset".into(),
936 client: None,
937 }
938 }
939
940 #[test]
941 fn valid_request_passes() {
942 assert!(valid_request().validate().is_ok());
943 }
944
945 #[test]
946 fn empty_name_fails() {
947 let req = CreateKeysetRequest {
948 name: "".into(),
949 ..valid_request()
950 };
951 let errors = req.validate().unwrap_err();
952 assert!(errors.field_errors().contains_key("name"));
953 }
954
955 #[test]
956 fn name_over_64_chars_fails() {
957 let req = CreateKeysetRequest {
958 name: "a".repeat(65).into(),
959 ..valid_request()
960 };
961 let errors = req.validate().unwrap_err();
962 assert!(errors.field_errors().contains_key("name"));
963 }
964
965 #[test]
966 fn reserved_default_name_fails() {
967 let req = CreateKeysetRequest {
968 name: "default".into(),
969 ..valid_request()
970 };
971 let errors = req.validate().unwrap_err();
972 let name_errors = &errors.field_errors()["name"];
973 assert!(name_errors.iter().any(|e| e.code == "reserved_name"));
974 }
975
976 #[test]
977 fn reserved_default_name_case_insensitive() {
978 let req = CreateKeysetRequest {
979 name: "DEFAULT".into(),
980 ..valid_request()
981 };
982 assert!(req.validate().is_err());
983 }
984
985 #[test]
986 fn name_with_invalid_characters_fails() {
987 let req = CreateKeysetRequest {
988 name: "has spaces".into(),
989 ..valid_request()
990 };
991 let errors = req.validate().unwrap_err();
992 let name_errors = &errors.field_errors()["name"];
993 assert!(name_errors.iter().any(|e| e.code == "invalid_characters"));
994 }
995
996 #[test]
997 fn name_with_special_chars_fails() {
998 for name in ["test@keyset", "test!keyset", "test.keyset", "test%keyset"] {
999 let req = CreateKeysetRequest {
1000 name: name.into(),
1001 ..valid_request()
1002 };
1003 assert!(
1004 req.validate().is_err(),
1005 "expected {name} to fail validation"
1006 );
1007 }
1008 }
1009
1010 #[test]
1011 fn name_with_allowed_chars_passes() {
1012 for name in ["my-keyset", "my_keyset", "my/keyset", "MyKeyset123"] {
1013 let req = CreateKeysetRequest {
1014 name: name.into(),
1015 ..valid_request()
1016 };
1017 assert!(req.validate().is_ok(), "expected {name} to pass validation");
1018 }
1019 }
1020
1021 #[test]
1022 fn empty_description_fails() {
1023 let req = CreateKeysetRequest {
1024 description: "".into(),
1025 ..valid_request()
1026 };
1027 let errors = req.validate().unwrap_err();
1028 assert!(errors.field_errors().contains_key("description"));
1029 }
1030
1031 #[test]
1032 fn description_over_256_chars_fails() {
1033 let req = CreateKeysetRequest {
1034 description: "a".repeat(257).into(),
1035 ..valid_request()
1036 };
1037 let errors = req.validate().unwrap_err();
1038 assert!(errors.field_errors().contains_key("description"));
1039 }
1040
1041 #[test]
1042 fn description_at_256_chars_passes() {
1043 let req = CreateKeysetRequest {
1044 description: "a".repeat(256).into(),
1045 ..valid_request()
1046 };
1047 assert!(req.validate().is_ok());
1048 }
1049
1050 #[test]
1051 fn nested_client_name_validation() {
1052 use crate::{ClientType, CreateClientSpec};
1053
1054 let req = CreateKeysetRequest {
1055 name: "my-keyset".into(),
1056 description: "desc".into(),
1057 client: Some(CreateClientSpec {
1058 client_type: ClientType::Device,
1059 name: "".into(),
1060 }),
1061 };
1062 let errors = req.validate().unwrap_err();
1063 assert!(
1064 errors.errors().contains_key("client"),
1065 "expected nested client validation error"
1066 );
1067 }
1068 }
1069
1070 mod openapi_schema {
1071 use crate::{CreateClientSpec, CreateKeysetRequest};
1072 use utoipa::PartialSchema;
1073
1074 fn schema_json<T: PartialSchema>() -> serde_json::Value {
1075 serde_json::to_value(T::schema()).unwrap()
1076 }
1077
1078 #[test]
1079 fn create_keyset_request_name_has_constraints() {
1080 let schema = schema_json::<CreateKeysetRequest>();
1081 let name = &schema["properties"]["name"];
1082
1083 assert_eq!(name["minLength"], 1);
1084 assert_eq!(name["maxLength"], 64);
1085 assert_eq!(name["pattern"], r"^[A-Za-z0-9_\-/]+$");
1086 }
1087
1088 #[test]
1089 fn create_keyset_request_description_has_constraints() {
1090 let schema = schema_json::<CreateKeysetRequest>();
1091 let desc = &schema["properties"]["description"];
1092
1093 assert_eq!(desc["minLength"], 1);
1094 assert_eq!(desc["maxLength"], 256);
1095 }
1096
1097 #[test]
1098 fn create_client_spec_name_has_constraints() {
1099 let schema = schema_json::<CreateClientSpec>();
1100 let name = &schema["properties"]["name"];
1101
1102 assert_eq!(name["minLength"], 1);
1103 assert_eq!(name["maxLength"], 64);
1104 }
1105 }
1106
1107 mod backwards_compatible_deserialisation {
1108 use super::*;
1109
1110 #[test]
1111 fn when_dataset_id_is_uuid() {
1112 let client_id = Uuid::new_v4();
1113 let dataset_id = Uuid::new_v4();
1114
1115 let json = json!({
1116 "client_id": client_id,
1117 "dataset_id": dataset_id,
1118 });
1119
1120 let req: LoadKeysetRequest = serde_json::from_value(json).unwrap();
1121
1122 assert_eq!(
1123 req,
1124 LoadKeysetRequest {
1125 client_id,
1126 keyset_id: Some(IdentifiedBy::Uuid(dataset_id))
1127 }
1128 );
1129 }
1130
1131 #[test]
1132 fn when_keyset_id_is_uuid() {
1133 let client_id = Uuid::new_v4();
1134 let keyset_id = Uuid::new_v4();
1135
1136 let json = json!({
1137 "client_id": client_id,
1138 "keyset_id": keyset_id,
1139 });
1140
1141 let req: LoadKeysetRequest = serde_json::from_value(json).unwrap();
1142
1143 assert_eq!(
1144 req,
1145 LoadKeysetRequest {
1146 client_id,
1147 keyset_id: Some(IdentifiedBy::Uuid(keyset_id))
1148 }
1149 );
1150 }
1151
1152 #[test]
1153 fn when_dataset_id_is_id_name() {
1154 let client_id = Uuid::new_v4();
1155 let dataset_id = IdentifiedBy::Name(Name::new_untrusted("some-dataset-name"));
1156
1157 let json = json!({
1158 "client_id": client_id,
1159 "dataset_id": dataset_id,
1160 });
1161
1162 let req: LoadKeysetRequest = serde_json::from_value(json).unwrap();
1163
1164 assert_eq!(
1165 req,
1166 LoadKeysetRequest {
1167 client_id,
1168 keyset_id: Some(dataset_id)
1169 }
1170 );
1171 }
1172 }
1173}