cloud_sdk/authentication/
expiry.rs1#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
7pub struct CredentialTimestamp(u64);
8
9impl CredentialTimestamp {
10 #[must_use]
12 pub const fn from_seconds(seconds: u64) -> Self {
13 Self(seconds)
14 }
15
16 #[must_use]
18 pub const fn as_seconds(self) -> u64 {
19 self.0
20 }
21}
22
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum CredentialLifetimeError {
26 ZeroExpiresIn,
28 ZeroRefreshWindow,
30 RefreshWindowTooLarge,
32 TimestampOverflow,
34}
35
36impl_static_error!(CredentialLifetimeError,
37 Self::ZeroExpiresIn => "credential expires_in must be nonzero",
38 Self::ZeroRefreshWindow => "credential refresh window must be nonzero",
39 Self::RefreshWindowTooLarge => "credential refresh window consumes the complete lifetime",
40 Self::TimestampOverflow => "credential expiry timestamp overflows",
41);
42
43#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
45pub enum CredentialLifetimeState {
46 ClockRollback,
48 Fresh,
50 RefreshRequired,
52 Expired,
54}
55
56#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
61pub struct CredentialLifetime {
62 observed_at: CredentialTimestamp,
63 refresh_at: CredentialTimestamp,
64 expires_at: CredentialTimestamp,
65 expires_in: u32,
66 refresh_before: u32,
67}
68
69impl CredentialLifetime {
70 pub fn from_expires_in(
72 observed_at: CredentialTimestamp,
73 expires_in: u32,
74 refresh_before: u32,
75 ) -> Result<Self, CredentialLifetimeError> {
76 if expires_in == 0 {
77 return Err(CredentialLifetimeError::ZeroExpiresIn);
78 }
79 if refresh_before == 0 {
80 return Err(CredentialLifetimeError::ZeroRefreshWindow);
81 }
82 if refresh_before >= expires_in {
83 return Err(CredentialLifetimeError::RefreshWindowTooLarge);
84 }
85 let expires_at = observed_at
86 .0
87 .checked_add(u64::from(expires_in))
88 .ok_or(CredentialLifetimeError::TimestampOverflow)?;
89 let refresh_at = expires_at
90 .checked_sub(u64::from(refresh_before))
91 .ok_or(CredentialLifetimeError::TimestampOverflow)?;
92 Ok(Self {
93 observed_at,
94 refresh_at: CredentialTimestamp(refresh_at),
95 expires_at: CredentialTimestamp(expires_at),
96 expires_in,
97 refresh_before,
98 })
99 }
100
101 #[must_use]
103 pub const fn observed_at(self) -> CredentialTimestamp {
104 self.observed_at
105 }
106
107 #[must_use]
109 pub const fn refresh_at(self) -> CredentialTimestamp {
110 self.refresh_at
111 }
112
113 #[must_use]
115 pub const fn expires_at(self) -> CredentialTimestamp {
116 self.expires_at
117 }
118
119 #[must_use]
121 pub const fn expires_in(self) -> u32 {
122 self.expires_in
123 }
124
125 #[must_use]
127 pub const fn refresh_before(self) -> u32 {
128 self.refresh_before
129 }
130
131 #[must_use]
133 pub const fn state_at(self, now: CredentialTimestamp) -> CredentialLifetimeState {
134 if now.0 < self.observed_at.0 {
135 CredentialLifetimeState::ClockRollback
136 } else if now.0 >= self.expires_at.0 {
137 CredentialLifetimeState::Expired
138 } else if now.0 >= self.refresh_at.0 {
139 CredentialLifetimeState::RefreshRequired
140 } else {
141 CredentialLifetimeState::Fresh
142 }
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::{
149 CredentialLifetime, CredentialLifetimeError, CredentialLifetimeState, CredentialTimestamp,
150 };
151
152 #[test]
153 fn expires_in_uses_explicit_caller_time_and_exclusive_expiry() {
154 let lifetime = CredentialLifetime::from_expires_in(
155 CredentialTimestamp::from_seconds(1_000),
156 3_599,
157 300,
158 );
159 assert!(lifetime.is_ok());
160 let Ok(lifetime) = lifetime else {
161 unreachable!("credential lifetime fixture construction failed");
162 };
163 assert_eq!(lifetime.refresh_at().as_seconds(), 4_299);
164 assert_eq!(lifetime.expires_at().as_seconds(), 4_599);
165 assert_eq!(
166 lifetime.state_at(CredentialTimestamp::from_seconds(999)),
167 CredentialLifetimeState::ClockRollback
168 );
169 assert_eq!(
170 lifetime.state_at(CredentialTimestamp::from_seconds(4_298)),
171 CredentialLifetimeState::Fresh
172 );
173 assert_eq!(
174 lifetime.state_at(CredentialTimestamp::from_seconds(4_299)),
175 CredentialLifetimeState::RefreshRequired
176 );
177 assert_eq!(
178 lifetime.state_at(CredentialTimestamp::from_seconds(4_599)),
179 CredentialLifetimeState::Expired
180 );
181 }
182
183 #[test]
184 fn invalid_or_overflowing_lifetimes_fail_closed() {
185 let now = CredentialTimestamp::from_seconds(10);
186 assert_eq!(
187 CredentialLifetime::from_expires_in(now, 0, 0),
188 Err(CredentialLifetimeError::ZeroExpiresIn)
189 );
190 assert_eq!(
191 CredentialLifetime::from_expires_in(now, 60, 0),
192 Err(CredentialLifetimeError::ZeroRefreshWindow)
193 );
194 assert_eq!(
195 CredentialLifetime::from_expires_in(now, 60, 60),
196 Err(CredentialLifetimeError::RefreshWindowTooLarge)
197 );
198 assert_eq!(
199 CredentialLifetime::from_expires_in(CredentialTimestamp::from_seconds(u64::MAX), 2, 1,),
200 Err(CredentialLifetimeError::TimestampOverflow)
201 );
202 }
203}