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