1use s2_common::{
2 self,
3 access::{AccessTokenId, AccessTokenIdPrefix, AccessTokenIdStartAfter},
4 basin::{BasinName, BasinNamePrefix},
5 stream::{StreamName, StreamNamePrefix},
6};
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone)]
10#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
11pub enum MaybeEmpty<T> {
12 Empty,
13 NonEmpty(T),
14}
15
16impl<T: Serialize> Serialize for MaybeEmpty<T> {
17 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
18 where
19 S: serde::Serializer,
20 {
21 match self {
22 Self::NonEmpty(v) => v.serialize(serializer),
23 Self::Empty => serializer.serialize_str(""),
24 }
25 }
26}
27
28impl<'de, T> Deserialize<'de> for MaybeEmpty<T>
29where
30 T: Deserialize<'de>,
31{
32 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
33 where
34 D: serde::Deserializer<'de>,
35 {
36 let s = String::deserialize(deserializer)?;
37 if s.is_empty() {
38 Ok(MaybeEmpty::Empty)
39 } else {
40 T::deserialize(serde::de::value::StringDeserializer::new(s)).map(MaybeEmpty::NonEmpty)
41 }
42 }
43}
44
45use time::OffsetDateTime;
46
47#[rustfmt::skip]
48#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
49#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
50#[serde(rename_all = "kebab-case")]
51pub enum Operation {
52 ListBasins,
54 CreateBasin,
56 DeleteBasin,
58 ReconfigureBasin,
60 GetBasinConfig,
62 IssueAccessToken,
64 RevokeAccessToken,
66 ListAccessTokens,
68 ListStreams,
70 CreateStream,
72 DeleteStream,
74 GetStreamConfig,
76 ReconfigureStream,
78 CheckTail,
80 Append,
82 Read,
84 Trim,
86 Fence,
88 AccountMetrics,
90 BasinMetrics,
92 StreamMetrics,
94 ListLocations,
96 GetDefaultLocation,
98 SetDefaultLocation,
100}
101
102impl From<Operation> for s2_common::access::Operation {
103 fn from(value: Operation) -> Self {
104 match value {
105 Operation::ListBasins => Self::ListBasins,
106 Operation::CreateBasin => Self::CreateBasin,
107 Operation::DeleteBasin => Self::DeleteBasin,
108 Operation::ReconfigureBasin => Self::ReconfigureBasin,
109 Operation::GetBasinConfig => Self::GetBasinConfig,
110 Operation::IssueAccessToken => Self::IssueAccessToken,
111 Operation::RevokeAccessToken => Self::RevokeAccessToken,
112 Operation::ListAccessTokens => Self::ListAccessTokens,
113 Operation::ListStreams => Self::ListStreams,
114 Operation::CreateStream => Self::CreateStream,
115 Operation::DeleteStream => Self::DeleteStream,
116 Operation::GetStreamConfig => Self::GetStreamConfig,
117 Operation::ReconfigureStream => Self::ReconfigureStream,
118 Operation::CheckTail => Self::CheckTail,
119 Operation::Append => Self::Append,
120 Operation::Read => Self::Read,
121 Operation::Trim => Self::Trim,
122 Operation::Fence => Self::Fence,
123 Operation::AccountMetrics => Self::AccountMetrics,
124 Operation::BasinMetrics => Self::BasinMetrics,
125 Operation::StreamMetrics => Self::StreamMetrics,
126 Operation::ListLocations => Self::ListLocations,
127 Operation::GetDefaultLocation => Self::GetDefaultLocation,
128 Operation::SetDefaultLocation => Self::SetDefaultLocation,
129 }
130 }
131}
132
133impl From<s2_common::access::Operation> for Operation {
134 fn from(value: s2_common::access::Operation) -> Self {
135 use s2_common::access::Operation::*;
136 match value {
137 ListBasins => Self::ListBasins,
138 CreateBasin => Self::CreateBasin,
139 DeleteBasin => Self::DeleteBasin,
140 ReconfigureBasin => Self::ReconfigureBasin,
141 GetBasinConfig => Self::GetBasinConfig,
142 IssueAccessToken => Self::IssueAccessToken,
143 RevokeAccessToken => Self::RevokeAccessToken,
144 ListAccessTokens => Self::ListAccessTokens,
145 ListStreams => Self::ListStreams,
146 CreateStream => Self::CreateStream,
147 DeleteStream => Self::DeleteStream,
148 GetStreamConfig => Self::GetStreamConfig,
149 ReconfigureStream => Self::ReconfigureStream,
150 CheckTail => Self::CheckTail,
151 Append => Self::Append,
152 Read => Self::Read,
153 Trim => Self::Trim,
154 Fence => Self::Fence,
155 AccountMetrics => Self::AccountMetrics,
156 BasinMetrics => Self::BasinMetrics,
157 StreamMetrics => Self::StreamMetrics,
158 ListLocations => Self::ListLocations,
159 GetDefaultLocation => Self::GetDefaultLocation,
160 SetDefaultLocation => Self::SetDefaultLocation,
161 }
162 }
163}
164
165#[rustfmt::skip]
166#[derive(Debug, Clone, Serialize, Deserialize)]
167#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
168pub struct AccessTokenInfo {
169 pub id: AccessTokenId,
172 #[serde(default, with = "time::serde::rfc3339::option")]
175 pub expires_at: Option<OffsetDateTime>,
176 #[cfg_attr(feature = "utoipa", schema(value_type = bool, default = false, required = false))]
179 pub auto_prefix_streams: Option<bool>,
180 pub scope: AccessTokenScope,
182}
183
184impl TryFrom<AccessTokenInfo> for s2_common::access::IssueAccessTokenRequest {
185 type Error = s2_common::ValidationError;
186
187 fn try_from(value: AccessTokenInfo) -> Result<Self, Self::Error> {
188 Ok(Self {
189 id: value.id,
190 expires_at: value.expires_at,
191 auto_prefix_streams: value.auto_prefix_streams.unwrap_or_default(),
192 scope: value.scope.try_into()?,
193 })
194 }
195}
196
197impl From<s2_common::access::AccessTokenInfo> for AccessTokenInfo {
198 fn from(value: s2_common::access::AccessTokenInfo) -> Self {
199 Self {
200 id: value.id,
201 expires_at: Some(value.expires_at),
202 auto_prefix_streams: Some(value.auto_prefix_streams),
203 scope: value.scope.into(),
204 }
205 }
206}
207
208impl From<s2_common::access::IssueAccessTokenRequest> for AccessTokenInfo {
209 fn from(value: s2_common::access::IssueAccessTokenRequest) -> Self {
210 Self {
211 id: value.id,
212 expires_at: value.expires_at,
213 auto_prefix_streams: Some(value.auto_prefix_streams),
214 scope: value.scope.into(),
215 }
216 }
217}
218
219#[rustfmt::skip]
220#[derive(Debug, Clone, Serialize, Deserialize)]
221#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
222pub struct AccessTokenScope {
223 pub basins: Option<ResourceSet<MaybeEmpty<BasinName>, BasinNamePrefix>>,
225 pub streams: Option<ResourceSet<MaybeEmpty<StreamName>, StreamNamePrefix>>,
227 pub access_tokens: Option<ResourceSet<MaybeEmpty<AccessTokenId>, AccessTokenIdPrefix>>,
229 pub op_groups: Option<PermittedOperationGroups>,
231 #[cfg_attr(feature = "utoipa", schema(required = false))]
234 pub ops: Option<Vec<Operation>>,
235}
236
237impl TryFrom<AccessTokenScope> for s2_common::access::AccessTokenScope {
238 type Error = s2_common::ValidationError;
239
240 fn try_from(value: AccessTokenScope) -> Result<Self, Self::Error> {
241 let AccessTokenScope {
242 basins,
243 streams,
244 access_tokens,
245 op_groups,
246 ops,
247 } = value;
248
249 Ok(Self {
250 basins: basins.map(Into::into).unwrap_or_default(),
251 streams: streams.map(Into::into).unwrap_or_default(),
252 access_tokens: access_tokens.map(Into::into).unwrap_or_default(),
253 op_groups: op_groups.map(Into::into).unwrap_or_default(),
254 ops: ops
255 .map(|o| {
256 o.into_iter()
257 .map(s2_common::access::Operation::from)
258 .collect()
259 })
260 .unwrap_or_default(),
261 })
262 }
263}
264
265impl From<s2_common::access::AccessTokenScope> for AccessTokenScope {
266 fn from(value: s2_common::access::AccessTokenScope) -> Self {
267 let s2_common::access::AccessTokenScope {
268 basins,
269 streams,
270 access_tokens,
271 op_groups,
272 ops,
273 } = value;
274
275 Self {
276 basins: ResourceSet::to_opt(basins),
277 streams: ResourceSet::to_opt(streams),
278 access_tokens: ResourceSet::to_opt(access_tokens),
279 op_groups: Some(op_groups.into()),
280 ops: Some(ops.into_iter().map(Operation::from).collect()),
281 }
282 }
283}
284
285#[rustfmt::skip]
286#[derive(Debug, Clone, Serialize, Deserialize)]
287#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
288#[serde(rename_all = "kebab-case")]
289pub enum ResourceSet<E, P> {
290 #[cfg_attr(feature = "utoipa", schema(title = "exact", value_type = String))]
293 Exact(E),
294 #[cfg_attr(feature = "utoipa", schema(title = "prefix", value_type = String))]
297 Prefix(P),
298}
299
300impl<E, P> ResourceSet<MaybeEmpty<E>, P> {
301 pub fn to_opt(rs: s2_common::access::ResourceSet<E, P>) -> Option<Self> {
302 match rs {
303 s2_common::access::ResourceSet::None => None,
304 s2_common::access::ResourceSet::Exact(e) => {
305 Some(ResourceSet::Exact(MaybeEmpty::NonEmpty(e)))
306 }
307 s2_common::access::ResourceSet::Prefix(p) => Some(ResourceSet::Prefix(p)),
308 }
309 }
310}
311
312impl<E, P> From<ResourceSet<MaybeEmpty<E>, P>> for s2_common::access::ResourceSet<E, P> {
313 fn from(value: ResourceSet<MaybeEmpty<E>, P>) -> Self {
314 match value {
315 ResourceSet::Exact(MaybeEmpty::Empty) => Self::None,
316 ResourceSet::Exact(MaybeEmpty::NonEmpty(e)) => Self::Exact(e),
317 ResourceSet::Prefix(p) => Self::Prefix(p),
318 }
319 }
320}
321
322#[rustfmt::skip]
323#[derive(Debug, Clone, Serialize, Deserialize)]
324#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
325pub struct PermittedOperationGroups {
326 pub account: Option<ReadWritePermissions>,
328 pub basin: Option<ReadWritePermissions>,
330 pub stream: Option<ReadWritePermissions>,
332}
333
334impl From<PermittedOperationGroups> for s2_common::access::PermittedOperationGroups {
335 fn from(value: PermittedOperationGroups) -> Self {
336 let PermittedOperationGroups {
337 account,
338 basin,
339 stream,
340 } = value;
341
342 Self {
343 account: account.map(Into::into).unwrap_or_default(),
344 basin: basin.map(Into::into).unwrap_or_default(),
345 stream: stream.map(Into::into).unwrap_or_default(),
346 }
347 }
348}
349
350impl From<s2_common::access::PermittedOperationGroups> for PermittedOperationGroups {
351 fn from(value: s2_common::access::PermittedOperationGroups) -> Self {
352 let s2_common::access::PermittedOperationGroups {
353 account,
354 basin,
355 stream,
356 } = value;
357
358 Self {
359 account: Some(account.into()),
360 basin: Some(basin.into()),
361 stream: Some(stream.into()),
362 }
363 }
364}
365
366#[rustfmt::skip]
367#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
368#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
369pub struct ReadWritePermissions {
370 #[cfg_attr(feature = "utoipa", schema(value_type = bool, default = false, required = false))]
372 pub read: Option<bool>,
373 #[cfg_attr(feature = "utoipa", schema(value_type = bool, default = false, required = false))]
375 pub write: Option<bool>,
376}
377
378impl From<ReadWritePermissions> for s2_common::access::ReadWritePermissions {
379 fn from(value: ReadWritePermissions) -> Self {
380 let ReadWritePermissions { read, write } = value;
381
382 Self {
383 read: read.unwrap_or_default(),
384 write: write.unwrap_or_default(),
385 }
386 }
387}
388
389impl From<s2_common::access::ReadWritePermissions> for ReadWritePermissions {
390 fn from(value: s2_common::access::ReadWritePermissions) -> Self {
391 let s2_common::access::ReadWritePermissions { read, write } = value;
392
393 Self {
394 read: Some(read),
395 write: Some(write),
396 }
397 }
398}
399
400#[rustfmt::skip]
401#[derive(Debug, Clone, Serialize, Deserialize)]
402#[cfg_attr(feature = "utoipa", derive(utoipa::IntoParams))]
403#[cfg_attr(feature = "utoipa", into_params(parameter_in = Query))]
404pub struct ListAccessTokensRequest {
405 #[cfg_attr(feature = "utoipa", param(value_type = String, default = "", required = false))]
407 pub prefix: Option<AccessTokenIdPrefix>,
408 #[cfg_attr(feature = "utoipa", param(value_type = String, default = "", required = false))]
410 pub start_after: Option<AccessTokenIdStartAfter>,
411 #[cfg_attr(feature = "utoipa", param(value_type = usize, maximum = 1000, default = 1000, required = false))]
413 pub limit: Option<usize>,
414}
415
416super::impl_list_request_conversions!(
417 ListAccessTokensRequest,
418 AccessTokenIdPrefix,
419 AccessTokenIdStartAfter
420);
421
422#[rustfmt::skip]
423#[derive(Debug, Clone, Serialize, Deserialize)]
424#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
425pub struct ListAccessTokensResponse {
426 #[cfg_attr(feature = "utoipa", schema(max_items = 1000))]
428 pub access_tokens: Vec<AccessTokenInfo>,
429 pub has_more: bool,
431}
432
433#[rustfmt::skip]
434#[derive(Debug, Clone, Serialize, Deserialize)]
435#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
436pub struct IssueAccessTokenResponse {
437 pub access_token: String,
439}
440
441#[cfg(test)]
442mod tests {
443 use proptest::prelude::*;
444
445 use super::*;
446
447 fn random_basin_resource_set() -> impl Strategy<Value = serde_json::Value> {
448 prop_oneof![
449 Just(serde_json::json!({"exact": ""})),
450 "[a-z][a-z0-9]{7,20}".prop_map(|s| serde_json::json!({"exact": s})),
451 Just(serde_json::json!({"prefix": ""})),
452 "[a-z][a-z0-9]{0,10}".prop_map(|s| serde_json::json!({"prefix": s})),
453 ]
454 }
455
456 fn random_resource_set() -> impl Strategy<Value = serde_json::Value> {
457 prop_oneof![
458 Just(serde_json::json!({"exact": ""})),
459 "[a-z][a-z0-9]{0,20}".prop_map(|s| serde_json::json!({"exact": s})),
460 Just(serde_json::json!({"prefix": ""})),
461 "[a-z][a-z0-9]{0,10}".prop_map(|s| serde_json::json!({"prefix": s})),
462 ]
463 }
464
465 fn random_access_token_info() -> impl Strategy<Value = serde_json::Value> {
466 (
467 "[a-z][a-z0-9]{0,20}",
468 proptest::option::of(random_basin_resource_set()),
469 proptest::option::of(random_resource_set()),
470 proptest::option::of(random_resource_set()),
471 )
472 .prop_map(|(id, basins, streams, access_tokens)| {
473 serde_json::json!({
474 "id": id,
475 "scope": {
476 "basins": basins,
477 "streams": streams,
478 "access_tokens": access_tokens
479 }
480 })
481 })
482 }
483
484 proptest! {
485 #[test]
486 fn access_token_info_roundtrip(json in random_access_token_info()) {
487 let parsed: AccessTokenInfo = serde_json::from_value(json).unwrap();
488 let internal: s2_common::access::IssueAccessTokenRequest = parsed.clone().try_into().unwrap();
489 let back: AccessTokenInfo = internal.into();
490 prop_assert_eq!(parsed.id, back.id);
491 }
492 }
493
494 #[test]
495 fn empty_exact_converts_to_resource_set_none() {
496 let json = serde_json::json!({
497 "id": "test-token",
498 "scope": {
499 "streams": {"exact": ""},
500 "basins": {"exact": ""},
501 "access_tokens": {"exact": ""}
502 }
503 });
504
505 let parsed: AccessTokenInfo = serde_json::from_value(json).unwrap();
506 let internal: s2_common::access::IssueAccessTokenRequest = parsed.try_into().unwrap();
507
508 assert!(matches!(
509 internal.scope.streams,
510 s2_common::access::ResourceSet::None
511 ));
512 assert!(matches!(
513 internal.scope.basins,
514 s2_common::access::ResourceSet::None
515 ));
516 assert!(matches!(
517 internal.scope.access_tokens,
518 s2_common::access::ResourceSet::None
519 ));
520 }
521
522 #[test]
523 fn missing_scope_fields_default_to_resource_set_none() {
524 let json = serde_json::json!({
525 "id": "test-token",
526 "scope": {}
527 });
528
529 let parsed: AccessTokenInfo = serde_json::from_value(json).unwrap();
530 let internal: s2_common::access::IssueAccessTokenRequest = parsed.try_into().unwrap();
531
532 assert!(matches!(
533 internal.scope.streams,
534 s2_common::access::ResourceSet::None
535 ));
536 assert!(matches!(
537 internal.scope.basins,
538 s2_common::access::ResourceSet::None
539 ));
540 assert!(matches!(
541 internal.scope.access_tokens,
542 s2_common::access::ResourceSet::None
543 ));
544 }
545}