LCOV - differential code coverage report
Current view: top level - libs/utils/src - auth.rs (source / functions) Coverage Total Hit UBC CBC
Current: f6946e90941b557c917ac98cd5a7e9506d180f3e.info Lines: 92.6 % 68 63 5 63
Current Date: 2023-10-19 02:04:12 Functions: 60.5 % 43 26 17 26
Baseline: c8637f37369098875162f194f92736355783b050.info
Baseline Date: 2023-10-18 20:25:20

           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                 : }
        

Generated by: LCOV version 2.1-beta