TLA Line data Source code
1 : // For details about authentication see docs/authentication.md
2 :
3 : use serde;
4 : use std::fs;
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 : use serde_with::{serde_as, DisplayFromStr};
13 :
14 : use crate::id::TenantId;
15 :
16 : /// Algorithm to use. We require EdDSA.
17 : const STORAGE_TOKEN_ALGORITHM: Algorithm = Algorithm::EdDSA;
18 :
19 CBC 614 : #[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 : }
33 :
34 : /// JWT payload. See docs/authentication.md for the format
35 : #[serde_as]
36 1788 : #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
37 : pub struct Claims {
38 : #[serde(default)]
39 : #[serde_as(as = "Option<DisplayFromStr>")]
40 : pub tenant_id: Option<TenantId>,
41 : pub scope: Scope,
42 : }
43 :
44 : impl Claims {
45 81 : pub fn new(tenant_id: Option<TenantId>, scope: Scope) -> Self {
46 81 : Self { tenant_id, scope }
47 81 : }
48 : }
49 :
50 : pub struct JwtAuth {
51 : decoding_key: DecodingKey,
52 : validation: Validation,
53 : }
54 :
55 : impl JwtAuth {
56 68 : pub fn new(decoding_key: DecodingKey) -> Self {
57 68 : let mut validation = Validation::default();
58 68 : validation.algorithms = vec![STORAGE_TOKEN_ALGORITHM];
59 68 : // The default 'required_spec_claims' is 'exp'. But we don't want to require
60 68 : // expiration.
61 68 : validation.required_spec_claims = [].into();
62 68 : Self {
63 68 : decoding_key,
64 68 : validation,
65 68 : }
66 68 : }
67 :
68 66 : pub fn from_key_path(key_path: &Utf8Path) -> Result<Self> {
69 66 : let public_key = fs::read(key_path)?;
70 66 : Ok(Self::new(DecodingKey::from_ed_pem(&public_key)?))
71 66 : }
72 :
73 307 : pub fn decode(&self, token: &str) -> Result<TokenData<Claims>> {
74 307 : Ok(decode(token, &self.decoding_key, &self.validation)?)
75 307 : }
76 : }
77 :
78 : impl std::fmt::Debug for JwtAuth {
79 UBC 0 : fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 0 : f.debug_struct("JwtAuth")
81 0 : .field("validation", &self.validation)
82 0 : .finish()
83 0 : }
84 : }
85 :
86 : // this function is used only for testing purposes in CLI e g generate tokens during init
87 CBC 82 : pub fn encode_from_key_file(claims: &Claims, key_data: &[u8]) -> Result<String> {
88 82 : let key = EncodingKey::from_ed_pem(key_data)?;
89 82 : Ok(encode(&Header::new(STORAGE_TOKEN_ALGORITHM), claims, &key)?)
90 82 : }
91 :
92 : #[cfg(test)]
93 : mod tests {
94 : use super::*;
95 : use std::str::FromStr;
96 :
97 : // Generated with:
98 : //
99 : // openssl genpkey -algorithm ed25519 -out ed25519-priv.pem
100 : // openssl pkey -in ed25519-priv.pem -pubout -out ed25519-pub.pem
101 : const TEST_PUB_KEY_ED25519: &[u8] = br#"
102 : -----BEGIN PUBLIC KEY-----
103 : MCowBQYDK2VwAyEARYwaNBayR+eGI0iXB4s3QxE3Nl2g1iWbr6KtLWeVD/w=
104 : -----END PUBLIC KEY-----
105 : "#;
106 :
107 : const TEST_PRIV_KEY_ED25519: &[u8] = br#"
108 : -----BEGIN PRIVATE KEY-----
109 : MC4CAQAwBQYDK2VwBCIEID/Drmc1AA6U/znNRWpF3zEGegOATQxfkdWxitcOMsIH
110 : -----END PRIVATE KEY-----
111 : "#;
112 :
113 1 : #[test]
114 1 : fn test_decode() -> Result<(), anyhow::Error> {
115 1 : let expected_claims = Claims {
116 1 : tenant_id: Some(TenantId::from_str("3d1f7595b468230304e0b73cecbcb081")?),
117 1 : scope: Scope::Tenant,
118 1 : };
119 1 :
120 1 : // A test token containing the following payload, signed using TEST_PRIV_KEY_ED25519:
121 1 : //
122 1 : // ```
123 1 : // {
124 1 : // "scope": "tenant",
125 1 : // "tenant_id": "3d1f7595b468230304e0b73cecbcb081",
126 1 : // "iss": "neon.controlplane",
127 1 : // "exp": 1709200879,
128 1 : // "iat": 1678442479
129 1 : // }
130 1 : // ```
131 1 : //
132 1 : let encoded_eddsa = "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6InRlbmFudCIsInRlbmFudF9pZCI6IjNkMWY3NTk1YjQ2ODIzMDMwNGUwYjczY2VjYmNiMDgxIiwiaXNzIjoibmVvbi5jb250cm9scGxhbmUiLCJleHAiOjE3MDkyMDA4NzksImlhdCI6MTY3ODQ0MjQ3OX0.U3eA8j-uU-JnhzeO3EDHRuXLwkAUFCPxtGHEgw6p7Ccc3YRbFs2tmCdbD9PZEXP-XsxSeBQi1FY0YPcT3NXADw";
133 :
134 : // Check it can be validated with the public key
135 1 : let auth = JwtAuth::new(DecodingKey::from_ed_pem(TEST_PUB_KEY_ED25519)?);
136 1 : let claims_from_token = auth.decode(encoded_eddsa)?.claims;
137 1 : assert_eq!(claims_from_token, expected_claims);
138 :
139 1 : Ok(())
140 1 : }
141 :
142 1 : #[test]
143 1 : fn test_encode() -> Result<(), anyhow::Error> {
144 1 : let claims = Claims {
145 1 : tenant_id: Some(TenantId::from_str("3d1f7595b468230304e0b73cecbcb081")?),
146 1 : scope: Scope::Tenant,
147 : };
148 :
149 1 : let encoded = encode_from_key_file(&claims, TEST_PRIV_KEY_ED25519)?;
150 :
151 : // decode it back
152 1 : let auth = JwtAuth::new(DecodingKey::from_ed_pem(TEST_PUB_KEY_ED25519)?);
153 1 : let decoded = auth.decode(&encoded)?;
154 :
155 1 : assert_eq!(decoded.claims, claims);
156 :
157 1 : Ok(())
158 1 : }
159 : }
|