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