Skip to main content

s2_common/types/
access.rs

1use std::{marker::PhantomData, ops::Deref, str::FromStr};
2
3use compact_str::{CompactString, ToCompactString};
4use enumset::{EnumSet, EnumSetType};
5
6use super::{
7    ValidationError,
8    basin::{BasinName, BasinNamePrefix},
9    stream::{StreamName, StreamNamePrefix},
10    strings::{IdProps, PrefixProps, StartAfterProps, StrProps},
11};
12use crate::{caps, types::resources::ListItemsRequest};
13
14#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
15#[cfg_attr(
16    feature = "rkyv",
17    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
18)]
19pub struct AccessTokenIdStr<T: StrProps>(CompactString, PhantomData<T>);
20
21impl<T: StrProps> AccessTokenIdStr<T> {
22    fn validate_str(id: &str) -> Result<(), ValidationError> {
23        if !T::IS_PREFIX && id.is_empty() {
24            return Err(format!("access token {} must not be empty", T::FIELD_NAME).into());
25        }
26
27        if !T::IS_PREFIX && (id == "." || id == "..") {
28            return Err(
29                format!("access token {} must not be \".\" or \"..\"", T::FIELD_NAME).into(),
30            );
31        }
32
33        if id.len() > caps::MAX_ACCESS_TOKEN_ID_LEN {
34            return Err(format!(
35                "access token {} must not exceed {} bytes in length",
36                T::FIELD_NAME,
37                caps::MAX_ACCESS_TOKEN_ID_LEN
38            )
39            .into());
40        }
41
42        Ok(())
43    }
44}
45
46#[cfg(feature = "utoipa")]
47impl<T> utoipa::PartialSchema for AccessTokenIdStr<T>
48where
49    T: StrProps,
50{
51    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
52        utoipa::openapi::Object::builder()
53            .schema_type(utoipa::openapi::Type::String)
54            .min_length((!T::IS_PREFIX).then_some(1))
55            .max_length(Some(caps::MAX_ACCESS_TOKEN_ID_LEN))
56            .into()
57    }
58}
59
60#[cfg(feature = "utoipa")]
61impl<T> utoipa::ToSchema for AccessTokenIdStr<T> where T: StrProps {}
62
63impl<T: StrProps> serde::Serialize for AccessTokenIdStr<T> {
64    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
65    where
66        S: serde::Serializer,
67    {
68        serializer.serialize_str(&self.0)
69    }
70}
71
72impl<'de, T: StrProps> serde::Deserialize<'de> for AccessTokenIdStr<T> {
73    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
74    where
75        D: serde::Deserializer<'de>,
76    {
77        let s = CompactString::deserialize(deserializer)?;
78        s.try_into().map_err(serde::de::Error::custom)
79    }
80}
81
82impl<T: StrProps> AsRef<str> for AccessTokenIdStr<T> {
83    fn as_ref(&self) -> &str {
84        &self.0
85    }
86}
87
88impl<T: StrProps> Deref for AccessTokenIdStr<T> {
89    type Target = str;
90
91    fn deref(&self) -> &Self::Target {
92        &self.0
93    }
94}
95
96impl<T: StrProps> TryFrom<CompactString> for AccessTokenIdStr<T> {
97    type Error = ValidationError;
98
99    fn try_from(name: CompactString) -> Result<Self, Self::Error> {
100        Self::validate_str(&name)?;
101        Ok(Self(name, PhantomData))
102    }
103}
104
105impl<T: StrProps> FromStr for AccessTokenIdStr<T> {
106    type Err = ValidationError;
107
108    fn from_str(s: &str) -> Result<Self, Self::Err> {
109        Self::validate_str(s)?;
110        Ok(Self(s.to_compact_string(), PhantomData))
111    }
112}
113
114impl<T: StrProps> std::fmt::Debug for AccessTokenIdStr<T> {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        f.write_str(&self.0)
117    }
118}
119
120impl<T: StrProps> std::fmt::Display for AccessTokenIdStr<T> {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        f.write_str(&self.0)
123    }
124}
125
126impl<T: StrProps> From<AccessTokenIdStr<T>> for CompactString {
127    fn from(value: AccessTokenIdStr<T>) -> Self {
128        value.0
129    }
130}
131
132pub type AccessTokenId = AccessTokenIdStr<IdProps>;
133
134pub type AccessTokenIdPrefix = AccessTokenIdStr<PrefixProps>;
135
136impl Default for AccessTokenIdPrefix {
137    fn default() -> Self {
138        AccessTokenIdStr(CompactString::default(), PhantomData)
139    }
140}
141
142impl From<AccessTokenId> for AccessTokenIdPrefix {
143    fn from(value: AccessTokenId) -> Self {
144        Self(value.0, PhantomData)
145    }
146}
147
148pub type AccessTokenIdStartAfter = AccessTokenIdStr<StartAfterProps>;
149
150impl Default for AccessTokenIdStartAfter {
151    fn default() -> Self {
152        AccessTokenIdStr(CompactString::default(), PhantomData)
153    }
154}
155
156impl From<AccessTokenId> for AccessTokenIdStartAfter {
157    fn from(value: AccessTokenId) -> Self {
158        Self(value.0, PhantomData)
159    }
160}
161
162#[derive(Debug, Hash, EnumSetType, strum::EnumCount)]
163pub enum Operation {
164    ListBasins = 1,
165    CreateBasin = 2,
166    DeleteBasin = 3,
167    ReconfigureBasin = 4,
168    GetBasinConfig = 5,
169    IssueAccessToken = 6,
170    RevokeAccessToken = 7,
171    ListAccessTokens = 8,
172    ListStreams = 9,
173    CreateStream = 10,
174    DeleteStream = 11,
175    GetStreamConfig = 12,
176    ReconfigureStream = 13,
177    CheckTail = 14,
178    Append = 15,
179    Read = 16,
180    Trim = 17,
181    Fence = 18,
182    AccountMetrics = 19,
183    BasinMetrics = 20,
184    StreamMetrics = 21,
185    ListLocations = 22,
186    GetDefaultLocation = 23,
187    SetDefaultLocation = 24,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
191#[cfg_attr(
192    feature = "rkyv",
193    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
194)]
195pub enum ResourceSet<E, P> {
196    #[default]
197    None,
198    Exact(E),
199    Prefix(P),
200}
201
202pub type BasinResourceSet = ResourceSet<BasinName, BasinNamePrefix>;
203pub type StreamResourceSet = ResourceSet<StreamName, StreamNamePrefix>;
204pub type AccessTokenResourceSet = ResourceSet<AccessTokenId, AccessTokenIdPrefix>;
205
206#[derive(Debug, Clone, Copy, Default)]
207pub struct ReadWritePermissions {
208    pub read: bool,
209    pub write: bool,
210}
211
212#[derive(Debug, Clone, Default)]
213pub struct PermittedOperationGroups {
214    pub account: ReadWritePermissions,
215    pub basin: ReadWritePermissions,
216    pub stream: ReadWritePermissions,
217}
218
219#[derive(Debug, Clone, Default)]
220pub struct AccessTokenScope {
221    pub basins: BasinResourceSet,
222    pub streams: StreamResourceSet,
223    pub access_tokens: AccessTokenResourceSet,
224    pub op_groups: PermittedOperationGroups,
225    pub ops: EnumSet<Operation>,
226}
227
228#[derive(Debug, Clone)]
229pub struct AccessTokenInfo {
230    pub id: AccessTokenId,
231    pub expires_at: time::OffsetDateTime,
232    pub auto_prefix_streams: bool,
233    pub scope: AccessTokenScope,
234}
235
236#[derive(Debug, Clone)]
237pub struct IssueAccessTokenRequest {
238    pub id: AccessTokenId,
239    pub expires_at: Option<time::OffsetDateTime>,
240    pub auto_prefix_streams: bool,
241    pub scope: AccessTokenScope,
242}
243
244pub type ListAccessTokensRequest = ListItemsRequest<AccessTokenIdPrefix, AccessTokenIdStartAfter>;
245
246#[cfg(test)]
247mod test {
248    use rstest::rstest;
249
250    use super::{
251        super::strings::{IdProps, PrefixProps, StartAfterProps},
252        AccessTokenIdStr,
253    };
254
255    #[rstest]
256    #[case::normal("my-token".to_owned())]
257    #[case::max_len("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN))]
258    fn validate_id_ok(#[case] id: String) {
259        assert_eq!(AccessTokenIdStr::<IdProps>::validate_str(&id), Ok(()));
260    }
261
262    #[rstest]
263    #[case::empty("".to_owned())]
264    #[case::dot(".".to_owned())]
265    #[case::dot_dot("..".to_owned())]
266    #[case::too_long("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN + 1))]
267    fn validate_id_err(#[case] id: String) {
268        AccessTokenIdStr::<IdProps>::validate_str(&id).expect_err("expected validation error");
269    }
270
271    #[rstest]
272    #[case::empty("".to_owned())]
273    #[case::dot(".".to_owned())]
274    #[case::dot_dot("..".to_owned())]
275    #[case::max_len("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN))]
276    fn validate_prefix_ok(#[case] prefix: String) {
277        assert_eq!(
278            AccessTokenIdStr::<PrefixProps>::validate_str(&prefix),
279            Ok(())
280        );
281    }
282
283    #[rstest]
284    #[case::too_long("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN + 1))]
285    fn validate_prefix_err(#[case] prefix: String) {
286        AccessTokenIdStr::<PrefixProps>::validate_str(&prefix)
287            .expect_err("expected validation error");
288    }
289
290    #[rstest]
291    #[case::empty("".to_owned())]
292    #[case::dot(".".to_owned())]
293    #[case::dot_dot("..".to_owned())]
294    #[case::max_len("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN))]
295    fn validate_start_after_ok(#[case] start_after: String) {
296        assert_eq!(
297            AccessTokenIdStr::<StartAfterProps>::validate_str(&start_after),
298            Ok(())
299        );
300    }
301
302    #[rstest]
303    #[case::too_long("a".repeat(crate::caps::MAX_ACCESS_TOKEN_ID_LEN + 1))]
304    fn validate_start_after_err(#[case] start_after: String) {
305        AccessTokenIdStr::<StartAfterProps>::validate_str(&start_after)
306            .expect_err("expected validation error");
307    }
308}