Skip to main content

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