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