Line data Source code
1 : // For details about authentication see docs/authentication.md
2 :
3 : use std::borrow::Cow;
4 : use std::fmt::Display;
5 : use std::fs;
6 : use std::sync::Arc;
7 :
8 : use anyhow::Result;
9 : use arc_swap::ArcSwap;
10 : use camino::Utf8Path;
11 : use jsonwebtoken::{
12 : Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation, decode, encode,
13 : };
14 : use pem::Pem;
15 : use serde::{Deserialize, Serialize, de::DeserializeOwned};
16 :
17 : use crate::id::TenantId;
18 :
19 : /// Algorithm to use. We require EdDSA.
20 : const STORAGE_TOKEN_ALGORITHM: Algorithm = Algorithm::EdDSA;
21 :
22 2 : #[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
23 : #[serde(rename_all = "lowercase")]
24 : pub enum Scope {
25 : /// Provides access to all data for a specific tenant (specified in `struct Claims` below)
26 : // TODO: join these two?
27 : Tenant,
28 : /// Provides blanket access to all tenants on the pageserver plus pageserver-wide APIs.
29 : /// Should only be used e.g. for status check/tenant creation/list.
30 : PageServerApi,
31 : /// Provides blanket access to all data on the safekeeper plus safekeeper-wide APIs.
32 : /// Should only be used e.g. for status check.
33 : /// Currently also used for connection from any pageserver to any safekeeper.
34 : SafekeeperData,
35 : /// The scope used by pageservers in upcalls to storage controller and cloud control plane
36 : #[serde(rename = "generations_api")]
37 : GenerationsApi,
38 : /// Allows access to control plane managment API and all storage controller endpoints.
39 : Admin,
40 :
41 : /// Allows access to control plane & storage controller endpoints used in infrastructure automation (e.g. node registration)
42 : Infra,
43 :
44 : /// Allows access to storage controller APIs used by the scrubber, to interrogate the state
45 : /// of a tenant & post scrub results.
46 : Scrubber,
47 :
48 : /// This scope is used for communication with other storage controller instances.
49 : /// At the time of writing, this is only used for the step down request.
50 : #[serde(rename = "controller_peer")]
51 : ControllerPeer,
52 : }
53 :
54 : /// JWT payload. See docs/authentication.md for the format
55 6 : #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
56 : pub struct Claims {
57 : #[serde(default)]
58 : pub tenant_id: Option<TenantId>,
59 : pub scope: Scope,
60 : }
61 :
62 : impl Claims {
63 0 : pub fn new(tenant_id: Option<TenantId>, scope: Scope) -> Self {
64 0 : Self { tenant_id, scope }
65 0 : }
66 : }
67 :
68 : pub struct SwappableJwtAuth(ArcSwap<JwtAuth>);
69 :
70 : impl SwappableJwtAuth {
71 0 : pub fn new(jwt_auth: JwtAuth) -> Self {
72 0 : SwappableJwtAuth(ArcSwap::new(Arc::new(jwt_auth)))
73 0 : }
74 0 : pub fn swap(&self, jwt_auth: JwtAuth) {
75 0 : self.0.swap(Arc::new(jwt_auth));
76 0 : }
77 0 : pub fn decode<D: DeserializeOwned>(
78 0 : &self,
79 0 : token: &str,
80 0 : ) -> std::result::Result<TokenData<D>, AuthError> {
81 0 : self.0.load().decode(token)
82 0 : }
83 : }
84 :
85 : impl std::fmt::Debug for SwappableJwtAuth {
86 0 : fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 0 : write!(f, "Swappable({:?})", self.0.load())
88 0 : }
89 : }
90 :
91 : #[derive(Clone, PartialEq, Eq, Hash, Debug)]
92 : pub struct AuthError(pub Cow<'static, str>);
93 :
94 : impl Display for AuthError {
95 0 : fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 0 : write!(f, "{}", self.0)
97 0 : }
98 : }
99 :
100 : pub struct JwtAuth {
101 : decoding_keys: Vec<DecodingKey>,
102 : validation: Validation,
103 : }
104 :
105 : impl JwtAuth {
106 2 : pub fn new(decoding_keys: Vec<DecodingKey>) -> Self {
107 2 : let mut validation = Validation::default();
108 2 : validation.algorithms = vec![STORAGE_TOKEN_ALGORITHM];
109 2 : // The default 'required_spec_claims' is 'exp'. But we don't want to require
110 2 : // expiration.
111 2 : validation.required_spec_claims = [].into();
112 2 : Self {
113 2 : decoding_keys,
114 2 : validation,
115 2 : }
116 2 : }
117 :
118 0 : pub fn from_key_path(key_path: &Utf8Path) -> Result<Self> {
119 0 : let metadata = key_path.metadata()?;
120 0 : let decoding_keys = if metadata.is_dir() {
121 0 : let mut keys = Vec::new();
122 0 : for entry in fs::read_dir(key_path)? {
123 0 : let path = entry?.path();
124 0 : if !path.is_file() {
125 : // Ignore directories (don't recurse)
126 0 : continue;
127 0 : }
128 0 : let public_key = fs::read(path)?;
129 0 : keys.push(DecodingKey::from_ed_pem(&public_key)?);
130 : }
131 0 : keys
132 0 : } else if metadata.is_file() {
133 0 : let public_key = fs::read(key_path)?;
134 0 : vec![DecodingKey::from_ed_pem(&public_key)?]
135 : } else {
136 0 : anyhow::bail!("path is neither a directory or a file")
137 : };
138 0 : if decoding_keys.is_empty() {
139 0 : anyhow::bail!(
140 0 : "Configured for JWT auth with zero decoding keys. All JWT gated requests would be rejected."
141 0 : );
142 0 : }
143 0 : Ok(Self::new(decoding_keys))
144 0 : }
145 :
146 0 : pub fn from_key(key: String) -> Result<Self> {
147 0 : Ok(Self::new(vec![DecodingKey::from_ed_pem(key.as_bytes())?]))
148 0 : }
149 :
150 : /// Attempt to decode the token with the internal decoding keys.
151 : ///
152 : /// The function tries the stored decoding keys in succession,
153 : /// and returns the first yielding a successful result.
154 : /// If there is no working decoding key, it returns the last error.
155 2 : pub fn decode<D: DeserializeOwned>(
156 2 : &self,
157 2 : token: &str,
158 2 : ) -> std::result::Result<TokenData<D>, AuthError> {
159 2 : let mut res = None;
160 2 : for decoding_key in &self.decoding_keys {
161 2 : res = Some(decode(token, decoding_key, &self.validation));
162 2 : if let Some(Ok(res)) = res {
163 2 : return Ok(res);
164 0 : }
165 : }
166 0 : if let Some(res) = res {
167 0 : res.map_err(|e| AuthError(Cow::Owned(e.to_string())))
168 : } else {
169 0 : Err(AuthError(Cow::Borrowed("no JWT decoding keys configured")))
170 : }
171 2 : }
172 : }
173 :
174 : impl std::fmt::Debug for JwtAuth {
175 0 : fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 0 : f.debug_struct("JwtAuth")
177 0 : .field("validation", &self.validation)
178 0 : .finish()
179 0 : }
180 : }
181 :
182 : // this function is used only for testing purposes in CLI e g generate tokens during init
183 1 : pub fn encode_from_key_file<S: Serialize>(claims: &S, pem: &Pem) -> Result<String> {
184 1 : let key = EncodingKey::from_ed_der(pem.contents());
185 1 : Ok(encode(&Header::new(STORAGE_TOKEN_ALGORITHM), claims, &key)?)
186 1 : }
187 :
188 : #[cfg(test)]
189 : mod tests {
190 : use std::str::FromStr;
191 :
192 : use super::*;
193 :
194 : // Generated with:
195 : //
196 : // openssl genpkey -algorithm ed25519 -out ed25519-priv.pem
197 : // openssl pkey -in ed25519-priv.pem -pubout -out ed25519-pub.pem
198 : const TEST_PUB_KEY_ED25519: &str = r#"
199 : -----BEGIN PUBLIC KEY-----
200 : MCowBQYDK2VwAyEARYwaNBayR+eGI0iXB4s3QxE3Nl2g1iWbr6KtLWeVD/w=
201 : -----END PUBLIC KEY-----
202 : "#;
203 :
204 : const TEST_PRIV_KEY_ED25519: &str = r#"
205 : -----BEGIN PRIVATE KEY-----
206 : MC4CAQAwBQYDK2VwBCIEID/Drmc1AA6U/znNRWpF3zEGegOATQxfkdWxitcOMsIH
207 : -----END PRIVATE KEY-----
208 : "#;
209 :
210 : #[test]
211 1 : fn test_decode() {
212 1 : let expected_claims = Claims {
213 1 : tenant_id: Some(TenantId::from_str("3d1f7595b468230304e0b73cecbcb081").unwrap()),
214 1 : scope: Scope::Tenant,
215 1 : };
216 1 :
217 1 : // A test token containing the following payload, signed using TEST_PRIV_KEY_ED25519:
218 1 : //
219 1 : // ```
220 1 : // {
221 1 : // "scope": "tenant",
222 1 : // "tenant_id": "3d1f7595b468230304e0b73cecbcb081",
223 1 : // "iss": "neon.controlplane",
224 1 : // "iat": 1678442479
225 1 : // }
226 1 : // ```
227 1 : //
228 1 : let encoded_eddsa = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InRlbmFudCIsInRlbmFudF9pZCI6IjNkMWY3NTk1YjQ2ODIzMDMwNGUwYjczY2VjYmNiMDgxIiwiaXNzIjoibmVvbi5jb250cm9scGxhbmUiLCJpYXQiOjE2Nzg0NDI0Nzl9.rNheBnluMJNgXzSTTJoTNIGy4P_qe0JUHl_nVEGuDCTgHOThPVr552EnmKccrCKquPeW3c2YUk0Y9Oh4KyASAw";
229 1 :
230 1 : // Check it can be validated with the public key
231 1 : let auth = JwtAuth::new(vec![
232 1 : DecodingKey::from_ed_pem(TEST_PUB_KEY_ED25519.as_bytes()).unwrap(),
233 1 : ]);
234 1 : let claims_from_token: Claims = auth.decode(encoded_eddsa).unwrap().claims;
235 1 : assert_eq!(claims_from_token, expected_claims);
236 1 : }
237 :
238 : #[test]
239 1 : fn test_encode() {
240 1 : let claims = Claims {
241 1 : tenant_id: Some(TenantId::from_str("3d1f7595b468230304e0b73cecbcb081").unwrap()),
242 1 : scope: Scope::Tenant,
243 1 : };
244 1 :
245 1 : let pem = pem::parse(TEST_PRIV_KEY_ED25519).unwrap();
246 1 : let encoded = encode_from_key_file(&claims, &pem).unwrap();
247 1 :
248 1 : // decode it back
249 1 : let auth = JwtAuth::new(vec![
250 1 : DecodingKey::from_ed_pem(TEST_PUB_KEY_ED25519.as_bytes()).unwrap(),
251 1 : ]);
252 1 : let decoded: TokenData<Claims> = auth.decode(&encoded).unwrap();
253 1 :
254 1 : assert_eq!(decoded.claims, claims);
255 1 : }
256 : }
|