stack_auth/access_key_strategy.rs
1use cts_common::{Crn, CtsServiceDiscovery, Region, ServiceDiscovery};
2
3use crate::access_key::AccessKey;
4use crate::access_key_refresher::AccessKeyRefresher;
5use crate::auto_refresh::AutoRefresh;
6use crate::{ensure_trailing_slash, AuthError, AuthStrategy, SecretToken, ServiceToken};
7
8/// An [`AuthStrategy`] that uses a static access key to authenticate.
9///
10/// The first call to [`get_token`](AuthStrategy::get_token) authenticates with
11/// the server. Subsequent calls return the cached token until it expires, at
12/// which point re-authentication happens automatically.
13///
14/// # Example
15///
16/// ```no_run
17/// use stack_auth::{AccessKey, AccessKeyStrategy};
18/// use cts_common::Region;
19///
20/// let region = Region::aws("ap-southeast-2").unwrap();
21/// let key: AccessKey = "CSAKmyKeyId.myKeySecret".parse().unwrap();
22/// let strategy = AccessKeyStrategy::new(region, key).unwrap();
23/// ```
24pub struct AccessKeyStrategy {
25 crn: Option<Crn>,
26 inner: AutoRefresh<AccessKeyRefresher>,
27}
28
29impl AccessKeyStrategy {
30 /// Create a new `AccessKeyStrategy` for the given region and access key.
31 ///
32 /// The auth endpoint is resolved automatically via service discovery.
33 pub fn new(region: Region, access_key: AccessKey) -> Result<Self, AuthError> {
34 Self::builder(region, access_key).build()
35 }
36
37 /// Create a new `AccessKeyStrategy` for the given CRN and access key.
38 ///
39 /// The region is extracted from the CRN for service discovery.
40 /// The full CRN is stored and available via [`workspace_crn`](Self::workspace_crn).
41 pub fn new_with_crn(crn: Crn, access_key: AccessKey) -> Result<Self, AuthError> {
42 Self::builder(crn.region, access_key).crn(crn).build()
43 }
44
45 /// Return a builder for configuring an `AccessKeyStrategy` before construction.
46 ///
47 /// # Example
48 ///
49 /// ```no_run
50 /// use stack_auth::{AccessKey, AccessKeyStrategy};
51 /// use cts_common::Region;
52 ///
53 /// let region = Region::aws("ap-southeast-2").unwrap();
54 /// let key: AccessKey = "CSAKmyKeyId.myKeySecret".parse().unwrap();
55 /// let strategy = AccessKeyStrategy::builder(region, key)
56 /// .audience("my-audience")
57 /// .build()
58 /// .unwrap();
59 /// ```
60 pub fn builder(region: Region, access_key: AccessKey) -> AccessKeyStrategyBuilder {
61 AccessKeyStrategyBuilder {
62 region,
63 access_key: access_key.into_secret_token(),
64 audience: None,
65 base_url_override: None,
66 crn: None,
67 }
68 }
69
70 /// Return the workspace CRN, if one was provided at construction time.
71 pub fn workspace_crn(&self) -> Option<&Crn> {
72 self.crn.as_ref()
73 }
74}
75
76impl AuthStrategy for &AccessKeyStrategy {
77 async fn get_token(self) -> Result<ServiceToken, AuthError> {
78 Ok(self.inner.get_token().await?)
79 }
80}
81
82/// Builder for [`AccessKeyStrategy`].
83///
84/// Created via [`AccessKeyStrategy::builder`].
85pub struct AccessKeyStrategyBuilder {
86 region: Region,
87 access_key: SecretToken,
88 audience: Option<String>,
89 base_url_override: Option<url::Url>,
90 crn: Option<Crn>,
91}
92
93impl AccessKeyStrategyBuilder {
94 /// Set the audience for token requests.
95 pub fn audience(mut self, audience: impl Into<String>) -> Self {
96 self.audience = Some(audience.into());
97 self
98 }
99
100 /// Associate a workspace CRN with this strategy.
101 pub fn crn(mut self, crn: Crn) -> Self {
102 self.crn = Some(crn);
103 self
104 }
105
106 /// Override the base URL resolved by service discovery.
107 ///
108 /// Useful for pointing at a local or mock auth server during testing.
109 #[cfg(any(test, feature = "test-utils"))]
110 pub fn base_url(mut self, url: url::Url) -> Self {
111 self.base_url_override = Some(url);
112 self
113 }
114
115 /// Build the [`AccessKeyStrategy`].
116 ///
117 /// Resolves the base URL via service discovery unless overridden with
118 /// `base_url` (available when the `test-utils` feature is enabled).
119 pub fn build(self) -> Result<AccessKeyStrategy, AuthError> {
120 let base_url = match self.base_url_override {
121 Some(url) => url,
122 None => crate::cts_base_url_from_env()?
123 .unwrap_or(CtsServiceDiscovery::endpoint(self.region)?),
124 };
125 let refresher = AccessKeyRefresher::new(
126 self.access_key,
127 ensure_trailing_slash(base_url),
128 self.audience,
129 );
130 Ok(AccessKeyStrategy {
131 crn: self.crn,
132 inner: AutoRefresh::new(refresher),
133 })
134 }
135}