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::id::TenantId;
14 :
15 : /// Algorithm to use. We require EdDSA.
16 : const STORAGE_TOKEN_ALGORITHM: Algorithm = Algorithm::EdDSA;
17 :
18 2 : #[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 6 : #[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 : pub struct JwtAuth {
94 : decoding_keys: Vec<DecodingKey>,
95 : validation: Validation,
96 : }
97 :
98 : impl JwtAuth {
99 2 : pub fn new(decoding_keys: Vec<DecodingKey>) -> Self {
100 2 : let mut validation = Validation::default();
101 2 : validation.algorithms = vec![STORAGE_TOKEN_ALGORITHM];
102 2 : // The default 'required_spec_claims' is 'exp'. But we don't want to require
103 2 : // expiration.
104 2 : validation.required_spec_claims = [].into();
105 2 : Self {
106 2 : decoding_keys,
107 2 : validation,
108 2 : }
109 2 : }
110 :
111 0 : pub fn from_key_path(key_path: &Utf8Path) -> Result<Self> {
112 0 : let metadata = key_path.metadata()?;
113 0 : let decoding_keys = if metadata.is_dir() {
114 0 : let mut keys = Vec::new();
115 0 : for entry in fs::read_dir(key_path)? {
116 0 : let path = entry?.path();
117 0 : if !path.is_file() {
118 : // Ignore directories (don't recurse)
119 0 : continue;
120 0 : }
121 0 : let public_key = fs::read(path)?;
122 0 : keys.push(DecodingKey::from_ed_pem(&public_key)?);
123 : }
124 0 : keys
125 0 : } else if metadata.is_file() {
126 0 : let public_key = fs::read(key_path)?;
127 0 : vec![DecodingKey::from_ed_pem(&public_key)?]
128 : } else {
129 0 : anyhow::bail!("path is neither a directory or a file")
130 : };
131 0 : if decoding_keys.is_empty() {
132 0 : anyhow::bail!("Configured for JWT auth with zero decoding keys. All JWT gated requests would be rejected.");
133 0 : }
134 0 : Ok(Self::new(decoding_keys))
135 0 : }
136 :
137 0 : pub fn from_key(key: String) -> Result<Self> {
138 0 : Ok(Self::new(vec![DecodingKey::from_ed_pem(key.as_bytes())?]))
139 0 : }
140 :
141 : /// Attempt to decode the token with the internal decoding keys.
142 : ///
143 : /// The function tries the stored decoding keys in succession,
144 : /// and returns the first yielding a successful result.
145 : /// If there is no working decoding key, it returns the last error.
146 2 : pub fn decode(&self, token: &str) -> std::result::Result<TokenData<Claims>, AuthError> {
147 2 : let mut res = None;
148 2 : for decoding_key in &self.decoding_keys {
149 2 : res = Some(decode(token, decoding_key, &self.validation));
150 2 : if let Some(Ok(res)) = res {
151 2 : return Ok(res);
152 0 : }
153 : }
154 0 : if let Some(res) = res {
155 0 : res.map_err(|e| AuthError(Cow::Owned(e.to_string())))
156 : } else {
157 0 : Err(AuthError(Cow::Borrowed("no JWT decoding keys configured")))
158 : }
159 2 : }
160 : }
161 :
162 : impl std::fmt::Debug for JwtAuth {
163 0 : fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 0 : f.debug_struct("JwtAuth")
165 0 : .field("validation", &self.validation)
166 0 : .finish()
167 0 : }
168 : }
169 :
170 : // this function is used only for testing purposes in CLI e g generate tokens during init
171 1 : pub fn encode_from_key_file(claims: &Claims, key_data: &[u8]) -> Result<String> {
172 1 : let key = EncodingKey::from_ed_pem(key_data)?;
173 1 : Ok(encode(&Header::new(STORAGE_TOKEN_ALGORITHM), claims, &key)?)
174 1 : }
175 :
176 : #[cfg(test)]
177 : mod tests {
178 : use super::*;
179 : use std::str::FromStr;
180 :
181 : // Generated with:
182 : //
183 : // openssl genpkey -algorithm ed25519 -out ed25519-priv.pem
184 : // openssl pkey -in ed25519-priv.pem -pubout -out ed25519-pub.pem
185 : const TEST_PUB_KEY_ED25519: &[u8] = br#"
186 : -----BEGIN PUBLIC KEY-----
187 : MCowBQYDK2VwAyEARYwaNBayR+eGI0iXB4s3QxE3Nl2g1iWbr6KtLWeVD/w=
188 : -----END PUBLIC KEY-----
189 : "#;
190 :
191 : const TEST_PRIV_KEY_ED25519: &[u8] = br#"
192 : -----BEGIN PRIVATE KEY-----
193 : MC4CAQAwBQYDK2VwBCIEID/Drmc1AA6U/znNRWpF3zEGegOATQxfkdWxitcOMsIH
194 : -----END PRIVATE KEY-----
195 : "#;
196 :
197 : #[test]
198 1 : fn test_decode() {
199 1 : let expected_claims = Claims {
200 1 : tenant_id: Some(TenantId::from_str("3d1f7595b468230304e0b73cecbcb081").unwrap()),
201 1 : scope: Scope::Tenant,
202 1 : };
203 1 :
204 1 : // A test token containing the following payload, signed using TEST_PRIV_KEY_ED25519:
205 1 : //
206 1 : // ```
207 1 : // {
208 1 : // "scope": "tenant",
209 1 : // "tenant_id": "3d1f7595b468230304e0b73cecbcb081",
210 1 : // "iss": "neon.controlplane",
211 1 : // "iat": 1678442479
212 1 : // }
213 1 : // ```
214 1 : //
215 1 : let encoded_eddsa = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InRlbmFudCIsInRlbmFudF9pZCI6IjNkMWY3NTk1YjQ2ODIzMDMwNGUwYjczY2VjYmNiMDgxIiwiaXNzIjoibmVvbi5jb250cm9scGxhbmUiLCJpYXQiOjE2Nzg0NDI0Nzl9.rNheBnluMJNgXzSTTJoTNIGy4P_qe0JUHl_nVEGuDCTgHOThPVr552EnmKccrCKquPeW3c2YUk0Y9Oh4KyASAw";
216 1 :
217 1 : // Check it can be validated with the public key
218 1 : let auth = JwtAuth::new(vec![DecodingKey::from_ed_pem(TEST_PUB_KEY_ED25519).unwrap()]);
219 1 : let claims_from_token = auth.decode(encoded_eddsa).unwrap().claims;
220 1 : assert_eq!(claims_from_token, expected_claims);
221 1 : }
222 :
223 : #[test]
224 1 : fn test_encode() {
225 1 : let claims = Claims {
226 1 : tenant_id: Some(TenantId::from_str("3d1f7595b468230304e0b73cecbcb081").unwrap()),
227 1 : scope: Scope::Tenant,
228 1 : };
229 1 :
230 1 : let encoded = encode_from_key_file(&claims, TEST_PRIV_KEY_ED25519).unwrap();
231 1 :
232 1 : // decode it back
233 1 : let auth = JwtAuth::new(vec![DecodingKey::from_ed_pem(TEST_PUB_KEY_ED25519).unwrap()]);
234 1 : let decoded = auth.decode(&encoded).unwrap();
235 1 :
236 1 : assert_eq!(decoded.claims, claims);
237 1 : }
238 : }
|