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 8 : #[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 some storage controller endpoints.
35 : Admin,
36 : }
37 :
38 : /// JWT payload. See docs/authentication.md for the format
39 16 : #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
40 : pub struct Claims {
41 : #[serde(default)]
42 : pub tenant_id: Option<TenantId>,
43 : pub scope: Scope,
44 : }
45 :
46 : impl Claims {
47 0 : pub fn new(tenant_id: Option<TenantId>, scope: Scope) -> Self {
48 0 : Self { tenant_id, scope }
49 0 : }
50 : }
51 :
52 : pub struct SwappableJwtAuth(ArcSwap<JwtAuth>);
53 :
54 : impl SwappableJwtAuth {
55 0 : pub fn new(jwt_auth: JwtAuth) -> Self {
56 0 : SwappableJwtAuth(ArcSwap::new(Arc::new(jwt_auth)))
57 0 : }
58 0 : pub fn swap(&self, jwt_auth: JwtAuth) {
59 0 : self.0.swap(Arc::new(jwt_auth));
60 0 : }
61 0 : pub fn decode(&self, token: &str) -> std::result::Result<TokenData<Claims>, AuthError> {
62 0 : self.0.load().decode(token)
63 0 : }
64 : }
65 :
66 : impl std::fmt::Debug for SwappableJwtAuth {
67 0 : fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 0 : write!(f, "Swappable({:?})", self.0.load())
69 0 : }
70 : }
71 :
72 : #[derive(Clone, PartialEq, Eq, Hash, Debug)]
73 : pub struct AuthError(pub Cow<'static, str>);
74 :
75 : impl Display for AuthError {
76 0 : fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 0 : write!(f, "{}", self.0)
78 0 : }
79 : }
80 :
81 : impl From<AuthError> for ApiError {
82 0 : fn from(_value: AuthError) -> Self {
83 0 : // Don't pass on the value of the AuthError as a precautionary measure.
84 0 : // Being intentionally vague in public error communication hurts debugability
85 0 : // but it is more secure.
86 0 : ApiError::Forbidden("JWT authentication error".to_string())
87 0 : }
88 : }
89 :
90 : pub struct JwtAuth {
91 : decoding_keys: Vec<DecodingKey>,
92 : validation: Validation,
93 : }
94 :
95 : impl JwtAuth {
96 4 : pub fn new(decoding_keys: Vec<DecodingKey>) -> Self {
97 4 : let mut validation = Validation::default();
98 4 : validation.algorithms = vec![STORAGE_TOKEN_ALGORITHM];
99 4 : // The default 'required_spec_claims' is 'exp'. But we don't want to require
100 4 : // expiration.
101 4 : validation.required_spec_claims = [].into();
102 4 : Self {
103 4 : decoding_keys,
104 4 : validation,
105 4 : }
106 4 : }
107 :
108 0 : pub fn from_key_path(key_path: &Utf8Path) -> Result<Self> {
109 0 : let metadata = key_path.metadata()?;
110 0 : let decoding_keys = if metadata.is_dir() {
111 0 : let mut keys = Vec::new();
112 0 : for entry in fs::read_dir(key_path)? {
113 0 : let path = entry?.path();
114 0 : if !path.is_file() {
115 : // Ignore directories (don't recurse)
116 0 : continue;
117 0 : }
118 0 : let public_key = fs::read(path)?;
119 0 : keys.push(DecodingKey::from_ed_pem(&public_key)?);
120 : }
121 0 : keys
122 0 : } else if metadata.is_file() {
123 0 : let public_key = fs::read(key_path)?;
124 0 : vec![DecodingKey::from_ed_pem(&public_key)?]
125 : } else {
126 0 : anyhow::bail!("path is neither a directory or a file")
127 : };
128 0 : if decoding_keys.is_empty() {
129 0 : anyhow::bail!("Configured for JWT auth with zero decoding keys. All JWT gated requests would be rejected.");
130 0 : }
131 0 : Ok(Self::new(decoding_keys))
132 0 : }
133 :
134 0 : pub fn from_key(key: String) -> Result<Self> {
135 0 : Ok(Self::new(vec![DecodingKey::from_ed_pem(key.as_bytes())?]))
136 0 : }
137 :
138 : /// Attempt to decode the token with the internal decoding keys.
139 : ///
140 : /// The function tries the stored decoding keys in succession,
141 : /// and returns the first yielding a successful result.
142 : /// If there is no working decoding key, it returns the last error.
143 4 : pub fn decode(&self, token: &str) -> std::result::Result<TokenData<Claims>, AuthError> {
144 4 : let mut res = None;
145 4 : for decoding_key in &self.decoding_keys {
146 4 : res = Some(decode(token, decoding_key, &self.validation));
147 4 : if let Some(Ok(res)) = res {
148 4 : return Ok(res);
149 0 : }
150 : }
151 0 : if let Some(res) = res {
152 0 : res.map_err(|e| AuthError(Cow::Owned(e.to_string())))
153 : } else {
154 0 : Err(AuthError(Cow::Borrowed("no JWT decoding keys configured")))
155 : }
156 4 : }
157 : }
158 :
159 : impl std::fmt::Debug for JwtAuth {
160 0 : fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 0 : f.debug_struct("JwtAuth")
162 0 : .field("validation", &self.validation)
163 0 : .finish()
164 0 : }
165 : }
166 :
167 : // this function is used only for testing purposes in CLI e g generate tokens during init
168 2 : pub fn encode_from_key_file(claims: &Claims, key_data: &[u8]) -> Result<String> {
169 2 : let key = EncodingKey::from_ed_pem(key_data)?;
170 2 : Ok(encode(&Header::new(STORAGE_TOKEN_ALGORITHM), claims, &key)?)
171 2 : }
172 :
173 : #[cfg(test)]
174 : mod tests {
175 : use super::*;
176 : use std::str::FromStr;
177 :
178 : // Generated with:
179 : //
180 : // openssl genpkey -algorithm ed25519 -out ed25519-priv.pem
181 : // openssl pkey -in ed25519-priv.pem -pubout -out ed25519-pub.pem
182 : const TEST_PUB_KEY_ED25519: &[u8] = br#"
183 : -----BEGIN PUBLIC KEY-----
184 : MCowBQYDK2VwAyEARYwaNBayR+eGI0iXB4s3QxE3Nl2g1iWbr6KtLWeVD/w=
185 : -----END PUBLIC KEY-----
186 : "#;
187 :
188 : const TEST_PRIV_KEY_ED25519: &[u8] = br#"
189 : -----BEGIN PRIVATE KEY-----
190 : MC4CAQAwBQYDK2VwBCIEID/Drmc1AA6U/znNRWpF3zEGegOATQxfkdWxitcOMsIH
191 : -----END PRIVATE KEY-----
192 : "#;
193 :
194 : #[test]
195 2 : fn test_decode() {
196 2 : let expected_claims = Claims {
197 2 : tenant_id: Some(TenantId::from_str("3d1f7595b468230304e0b73cecbcb081").unwrap()),
198 2 : scope: Scope::Tenant,
199 2 : };
200 2 :
201 2 : // A test token containing the following payload, signed using TEST_PRIV_KEY_ED25519:
202 2 : //
203 2 : // ```
204 2 : // {
205 2 : // "scope": "tenant",
206 2 : // "tenant_id": "3d1f7595b468230304e0b73cecbcb081",
207 2 : // "iss": "neon.controlplane",
208 2 : // "iat": 1678442479
209 2 : // }
210 2 : // ```
211 2 : //
212 2 : let encoded_eddsa = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InRlbmFudCIsInRlbmFudF9pZCI6IjNkMWY3NTk1YjQ2ODIzMDMwNGUwYjczY2VjYmNiMDgxIiwiaXNzIjoibmVvbi5jb250cm9scGxhbmUiLCJpYXQiOjE2Nzg0NDI0Nzl9.rNheBnluMJNgXzSTTJoTNIGy4P_qe0JUHl_nVEGuDCTgHOThPVr552EnmKccrCKquPeW3c2YUk0Y9Oh4KyASAw";
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 : #[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 : }
|