LCOV - code coverage report
Current view: top level - libs/utils/src - auth.rs (source / functions) Coverage Total Hit
Test: 8ac049b474321fdc72ddcb56d7165153a1a900e8.info Lines: 92.6 % 68 63
Test Date: 2023-09-06 10:18:01 Functions: 60.5 % 43 26

            Line data    Source code
       1              : // For details about authentication see docs/authentication.md
       2              : 
       3              : use serde;
       4              : use std::fs;
       5              : use std::path::Path;
       6              : 
       7              : use anyhow::Result;
       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          574 : #[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         1668 : #[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: &Path) -> 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          287 :     pub fn decode(&self, token: &str) -> Result<TokenData<Claims>> {
      74          287 :         Ok(decode(token, &self.decoding_key, &self.validation)?)
      75          287 :     }
      76              : }
      77              : 
      78              : impl std::fmt::Debug for JwtAuth {
      79            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           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