diff --git a/Cargo.lock b/Cargo.lock index 5d2cdcea27..5c9170b7de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1416,6 +1416,7 @@ name = "control_plane" version = "0.1.0" dependencies = [ "anyhow", + "base64 0.13.1", "camino", "clap", "comfy-table", @@ -1425,10 +1426,13 @@ dependencies = [ "humantime", "humantime-serde", "hyper 0.14.30", + "jsonwebtoken", "nix 0.27.1", "once_cell", "pageserver_api", "pageserver_client", + "pem", + "pkcs8 0.10.2", "postgres_backend", "postgres_connection", "regex", @@ -1437,6 +1441,7 @@ dependencies = [ "scopeguard", "serde", "serde_json", + "sha2", "storage_broker", "thiserror 1.0.69", "tokio", @@ -2817,6 +2822,7 @@ dependencies = [ "hyper 0.14.30", "itertools 0.10.5", "jemalloc_pprof", + "jsonwebtoken", "metrics", "once_cell", "pprof", @@ -4269,6 +4275,7 @@ dependencies = [ "hyper 0.14.30", "indoc", "itertools 0.10.5", + "jsonwebtoken", "md5", "metrics", "nix 0.27.1", @@ -5685,9 +5692,9 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.13" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ac5d832aa16abd7d1def883a8545280c20a60f523a370aa3a9617c2b8550ee" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", @@ -5988,6 +5995,7 @@ dependencies = [ "humantime", "hyper 0.14.30", "itertools 0.10.5", + "jsonwebtoken", "metrics", "once_cell", "pageserver_api", @@ -7872,6 +7880,7 @@ dependencies = [ "metrics", "nix 0.27.1", "once_cell", + "pem", "pin-project-lite", "postgres_connection", "pprof", @@ -8460,6 +8469,7 @@ dependencies = [ "once_cell", "p256 0.13.2", "parquet", + "pkcs8 0.10.2", "prettyplease", "proc-macro2", "prost 0.13.3", diff --git a/Cargo.toml b/Cargo.toml index d957fa9070..8fac3bb46c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -141,7 +141,9 @@ parking_lot = "0.12" parquet = { version = "53", default-features = false, features = ["zstd"] } parquet_derive = "53" pbkdf2 = { version = "0.12.1", features = ["simple", "std"] } +pem = "3.0.3" pin-project-lite = "0.2" +pkcs8 = "0.10.2" pprof = { version = "0.14", features = ["criterion", "flamegraph", "frame-pointer", "prost-codec"] } procfs = "0.16" prometheus = {version = "0.13", default-features=false, features = ["process"]} # removes protobuf dependency diff --git a/compute_tools/src/http/middleware/authorize.rs b/compute_tools/src/http/middleware/authorize.rs index f221752c38..f1137de0ab 100644 --- a/compute_tools/src/http/middleware/authorize.rs +++ b/compute_tools/src/http/middleware/authorize.rs @@ -54,8 +54,8 @@ impl AsyncAuthorizeRequest for Authorize { Box::pin(async move { let request_id = request.extract_parts::().await.unwrap(); - // TODO: Remove this stanza after teaching neon_local and the - // regression tests to use a JWT + JWKS. + // TODO(tristan957): Remove this stanza after teaching neon_local + // and the regression tests to use a JWT + JWKS. // // https://github.com/neondatabase/neon/issues/11316 if cfg!(feature = "testing") { @@ -112,6 +112,8 @@ impl Authorize { token: &str, validation: &Validation, ) -> Result> { + debug_assert!(!jwks.keys.is_empty()); + debug!("verifying token {}", token); for jwk in jwks.keys.iter() { diff --git a/control_plane/Cargo.toml b/control_plane/Cargo.toml index 162c49ec7c..a0ea216d9c 100644 --- a/control_plane/Cargo.toml +++ b/control_plane/Cargo.toml @@ -6,13 +6,17 @@ license.workspace = true [dependencies] anyhow.workspace = true +base64.workspace = true camino.workspace = true clap.workspace = true comfy-table.workspace = true futures.workspace = true humantime.workspace = true +jsonwebtoken.workspace = true nix.workspace = true once_cell.workspace = true +pem.workspace = true +pkcs8.workspace = true humantime-serde.workspace = true hyper0.workspace = true regex.workspace = true @@ -20,6 +24,7 @@ reqwest = { workspace = true, features = ["blocking", "json"] } scopeguard.workspace = true serde.workspace = true serde_json.workspace = true +sha2.workspace = true thiserror.workspace = true toml.workspace = true toml_edit.workspace = true diff --git a/control_plane/src/bin/neon_local.rs b/control_plane/src/bin/neon_local.rs index db9715dc62..950b264163 100644 --- a/control_plane/src/bin/neon_local.rs +++ b/control_plane/src/bin/neon_local.rs @@ -552,6 +552,7 @@ enum EndpointCmd { Start(EndpointStartCmdArgs), Reconfigure(EndpointReconfigureCmdArgs), Stop(EndpointStopCmdArgs), + GenerateJwt(EndpointGenerateJwtCmdArgs), } #[derive(clap::Args)] @@ -699,6 +700,13 @@ struct EndpointStopCmdArgs { mode: String, } +#[derive(clap::Args)] +#[clap(about = "Generate a JWT for an endpoint")] +struct EndpointGenerateJwtCmdArgs { + #[clap(help = "Postgres endpoint id")] + endpoint_id: String, +} + #[derive(clap::Subcommand)] #[clap(about = "Manage neon_local branch name mappings")] enum MappingsCmd { @@ -1528,6 +1536,16 @@ async fn handle_endpoint(subcmd: &EndpointCmd, env: &local_env::LocalEnv) -> Res .with_context(|| format!("postgres endpoint {endpoint_id} is not found"))?; endpoint.stop(&args.mode, args.destroy)?; } + EndpointCmd::GenerateJwt(args) => { + let endpoint_id = &args.endpoint_id; + let endpoint = cplane + .endpoints + .get(endpoint_id) + .with_context(|| format!("postgres endpoint {endpoint_id} is not found"))?; + let jwt = endpoint.generate_jwt()?; + + println!("{jwt}"); + } } Ok(()) diff --git a/control_plane/src/endpoint.rs b/control_plane/src/endpoint.rs index 2fa7a62f8f..0fe6975a6e 100644 --- a/control_plane/src/endpoint.rs +++ b/control_plane/src/endpoint.rs @@ -42,22 +42,29 @@ use std::path::PathBuf; use std::process::Command; use std::str::FromStr; use std::sync::Arc; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::time::{Duration, Instant}; use anyhow::{Context, Result, anyhow, bail}; -use compute_api::requests::ConfigurationRequest; +use compute_api::requests::{ComputeClaims, ConfigurationRequest}; use compute_api::responses::{ - ComputeConfig, ComputeCtlConfig, ComputeStatus, ComputeStatusResponse, + ComputeConfig, ComputeCtlConfig, ComputeStatus, ComputeStatusResponse, TlsConfig, }; use compute_api::spec::{ Cluster, ComputeAudit, ComputeFeature, ComputeMode, ComputeSpec, Database, PgIdent, RemoteExtSpec, Role, }; +use jsonwebtoken::jwk::{ + AlgorithmParameters, CommonParameters, EllipticCurve, Jwk, JwkSet, KeyAlgorithm, KeyOperations, + OctetKeyPairParameters, OctetKeyPairType, PublicKeyUse, +}; use nix::sys::signal::{Signal, kill}; use pageserver_api::shard::ShardStripeSize; +use pem::Pem; +use pkcs8::der::Decode; use reqwest::header::CONTENT_TYPE; use safekeeper_api::membership::SafekeeperGeneration; use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; use tracing::debug; use url::Host; use utils::id::{NodeId, TenantId, TimelineId}; @@ -82,6 +89,7 @@ pub struct EndpointConf { drop_subscriptions_before_start: bool, features: Vec, cluster: Option, + compute_ctl_config: ComputeCtlConfig, } // @@ -137,6 +145,36 @@ impl ComputeControlPlane { .unwrap_or(self.base_port) } + /// Create a JSON Web Key Set. This ideally matches the way we create a JWKS + /// from the production control plane. + fn create_jwks_from_pem(pem: Pem) -> Result { + let document = pkcs8::Document::from_der(&pem.into_contents())?; + + let mut hasher = Sha256::new(); + hasher.update(&document); + let key_hash = hasher.finalize(); + + Ok(JwkSet { + keys: vec![Jwk { + common: CommonParameters { + public_key_use: Some(PublicKeyUse::Signature), + key_operations: Some(vec![KeyOperations::Verify]), + key_algorithm: Some(KeyAlgorithm::EdDSA), + key_id: Some(base64::encode_config(key_hash, base64::URL_SAFE_NO_PAD)), + x509_url: None::, + x509_chain: None::>, + x509_sha1_fingerprint: None::, + x509_sha256_fingerprint: None::, + }, + algorithm: AlgorithmParameters::OctetKeyPair(OctetKeyPairParameters { + key_type: OctetKeyPairType::OctetKeyPair, + curve: EllipticCurve::Ed25519, + x: base64::encode_config(&document, base64::URL_SAFE_NO_PAD), + }), + }], + }) + } + #[allow(clippy::too_many_arguments)] pub fn new_endpoint( &mut self, @@ -154,6 +192,10 @@ impl ComputeControlPlane { let pg_port = pg_port.unwrap_or_else(|| self.get_port()); let external_http_port = external_http_port.unwrap_or_else(|| self.get_port() + 1); let internal_http_port = internal_http_port.unwrap_or_else(|| external_http_port + 1); + let compute_ctl_config = ComputeCtlConfig { + jwks: Self::create_jwks_from_pem(self.env.read_public_key()?)?, + tls: None::, + }; let ep = Arc::new(Endpoint { endpoint_id: endpoint_id.to_owned(), pg_address: SocketAddr::new(IpAddr::from(Ipv4Addr::LOCALHOST), pg_port), @@ -181,6 +223,7 @@ impl ComputeControlPlane { reconfigure_concurrency: 1, features: vec![], cluster: None, + compute_ctl_config: compute_ctl_config.clone(), }); ep.create_endpoint_dir()?; @@ -200,6 +243,7 @@ impl ComputeControlPlane { reconfigure_concurrency: 1, features: vec![], cluster: None, + compute_ctl_config, })?, )?; std::fs::write( @@ -242,7 +286,6 @@ impl ComputeControlPlane { /////////////////////////////////////////////////////////////////////////////// -#[derive(Debug)] pub struct Endpoint { /// used as the directory name endpoint_id: String, @@ -271,6 +314,9 @@ pub struct Endpoint { features: Vec, // Cluster settings cluster: Option, + + /// The compute_ctl config for the endpoint's compute. + compute_ctl_config: ComputeCtlConfig, } #[derive(PartialEq, Eq)] @@ -333,6 +379,7 @@ impl Endpoint { drop_subscriptions_before_start: conf.drop_subscriptions_before_start, features: conf.features, cluster: conf.cluster, + compute_ctl_config: conf.compute_ctl_config, }) } @@ -580,6 +627,13 @@ impl Endpoint { Ok(safekeeper_connstrings) } + /// Generate a JWT with the correct claims. + pub fn generate_jwt(&self) -> Result { + self.env.generate_auth_token(&ComputeClaims { + compute_id: self.endpoint_id.clone(), + }) + } + #[allow(clippy::too_many_arguments)] pub async fn start( &self, @@ -706,7 +760,7 @@ impl Endpoint { ComputeConfig { spec: Some(spec), - compute_ctl_config: ComputeCtlConfig::default(), + compute_ctl_config: self.compute_ctl_config.clone(), } }; @@ -774,16 +828,7 @@ impl Endpoint { ]) // TODO: It would be nice if we generated compute IDs with the same // algorithm as the real control plane. - .args([ - "--compute-id", - &format!( - "compute-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() - ), - ]) + .args(["--compute-id", &self.endpoint_id]) .stdin(std::process::Stdio::null()) .stderr(logfile.try_clone()?) .stdout(logfile); @@ -881,6 +926,7 @@ impl Endpoint { self.external_http_address.port() ), ) + .bearer_auth(self.generate_jwt()?) .send() .await?; @@ -957,6 +1003,7 @@ impl Endpoint { self.external_http_address.port() )) .header(CONTENT_TYPE.as_str(), "application/json") + .bearer_auth(self.generate_jwt()?) .body( serde_json::to_string(&ConfigurationRequest { spec, diff --git a/control_plane/src/local_env.rs b/control_plane/src/local_env.rs index fa10abe91a..b7906e5f81 100644 --- a/control_plane/src/local_env.rs +++ b/control_plane/src/local_env.rs @@ -12,6 +12,7 @@ use std::{env, fs}; use anyhow::{Context, bail}; use clap::ValueEnum; +use pem::Pem; use postgres_backend::AuthType; use reqwest::Url; use serde::{Deserialize, Serialize}; @@ -56,6 +57,7 @@ pub struct LocalEnv { // used to issue tokens during e.g pg start pub private_key_path: PathBuf, + /// Path to environment's public key pub public_key_path: PathBuf, pub broker: NeonBroker, @@ -758,11 +760,11 @@ impl LocalEnv { // this function is used only for testing purposes in CLI e g generate tokens during init pub fn generate_auth_token(&self, claims: &S) -> anyhow::Result { - let private_key_path = self.get_private_key_path(); - let key_data = fs::read(private_key_path)?; - encode_from_key_file(claims, &key_data) + let key = self.read_private_key()?; + encode_from_key_file(claims, &key) } + /// Get the path to the private key. pub fn get_private_key_path(&self) -> PathBuf { if self.private_key_path.is_absolute() { self.private_key_path.to_path_buf() @@ -771,6 +773,29 @@ impl LocalEnv { } } + /// Get the path to the public key. + pub fn get_public_key_path(&self) -> PathBuf { + if self.public_key_path.is_absolute() { + self.public_key_path.to_path_buf() + } else { + self.base_data_dir.join(&self.public_key_path) + } + } + + /// Read the contents of the private key file. + pub fn read_private_key(&self) -> anyhow::Result { + let private_key_path = self.get_private_key_path(); + let pem = pem::parse(fs::read(private_key_path)?)?; + Ok(pem) + } + + /// Read the contents of the public key file. + pub fn read_public_key(&self) -> anyhow::Result { + let public_key_path = self.get_public_key_path(); + let pem = pem::parse(fs::read(public_key_path)?)?; + Ok(pem) + } + /// Materialize the [`NeonLocalInitConf`] to disk. Called during [`neon_local init`]. pub fn init(conf: NeonLocalInitConf, force: &InitForceMode) -> anyhow::Result<()> { let base_path = base_path(); @@ -956,6 +981,7 @@ fn generate_auth_keys(private_key_path: &Path, public_key_path: &Path) -> anyhow String::from_utf8_lossy(&keygen_output.stderr) ); } + // Extract the public key from the private key file // // openssl pkey -in auth_private_key.pem -pubout -out auth_public_key.pem @@ -972,6 +998,7 @@ fn generate_auth_keys(private_key_path: &Path, public_key_path: &Path) -> anyhow String::from_utf8_lossy(&keygen_output.stderr) ); } + Ok(()) } diff --git a/control_plane/src/storage_controller.rs b/control_plane/src/storage_controller.rs index a4b56ae5c0..62ad5fa8d6 100644 --- a/control_plane/src/storage_controller.rs +++ b/control_plane/src/storage_controller.rs @@ -18,6 +18,7 @@ use pageserver_api::models::{ }; use pageserver_api::shard::TenantShardId; use pageserver_client::mgmt_api::ResponseErrorMessageExt; +use pem::Pem; use postgres_backend::AuthType; use reqwest::{Certificate, Method}; use serde::de::DeserializeOwned; @@ -34,8 +35,8 @@ use crate::local_env::{LocalEnv, NeonStorageControllerConf}; pub struct StorageController { env: LocalEnv, - private_key: Option>, - public_key: Option, + private_key: Option, + public_key: Option, client: reqwest::Client, config: NeonStorageControllerConf, @@ -116,7 +117,9 @@ impl StorageController { AuthType::Trust => (None, None), AuthType::NeonJWT => { let private_key_path = env.get_private_key_path(); - let private_key = fs::read(private_key_path).expect("failed to read private key"); + let private_key = + pem::parse(fs::read(private_key_path).expect("failed to read private key")) + .expect("failed to parse PEM file"); // If pageserver auth is enabled, this implicitly enables auth for this service, // using the same credentials. @@ -138,9 +141,13 @@ impl StorageController { .expect("Empty key dir") .expect("Error reading key dir"); - std::fs::read_to_string(dent.path()).expect("Can't read public key") + pem::parse(std::fs::read_to_string(dent.path()).expect("Can't read public key")) + .expect("Failed to parse PEM file") } else { - std::fs::read_to_string(&public_key_path).expect("Can't read public key") + pem::parse( + std::fs::read_to_string(&public_key_path).expect("Can't read public key"), + ) + .expect("Failed to parse PEM file") }; (Some(private_key), Some(public_key)) } diff --git a/libs/compute_api/src/responses.rs b/libs/compute_api/src/responses.rs index 353949736b..b7d6b7ca34 100644 --- a/libs/compute_api/src/responses.rs +++ b/libs/compute_api/src/responses.rs @@ -160,7 +160,7 @@ pub struct CatalogObjects { pub databases: Vec, } -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct ComputeCtlConfig { /// Set of JSON web keys that the compute can use to authenticate /// communication from the control plane. @@ -179,7 +179,7 @@ impl Default for ComputeCtlConfig { } } -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct TlsConfig { pub key_path: String, pub cert_path: String, diff --git a/libs/http-utils/Cargo.toml b/libs/http-utils/Cargo.toml index 5f6578f76e..ab9380089b 100644 --- a/libs/http-utils/Cargo.toml +++ b/libs/http-utils/Cargo.toml @@ -14,6 +14,7 @@ futures.workspace = true hyper0.workspace = true itertools.workspace = true jemalloc_pprof.workspace = true +jsonwebtoken.workspace = true once_cell.workspace = true pprof.workspace = true regex.workspace = true diff --git a/libs/http-utils/src/endpoint.rs b/libs/http-utils/src/endpoint.rs index 5588f6d87e..64147f2dd0 100644 --- a/libs/http-utils/src/endpoint.rs +++ b/libs/http-utils/src/endpoint.rs @@ -8,6 +8,7 @@ use bytes::{Bytes, BytesMut}; use hyper::header::{AUTHORIZATION, CONTENT_DISPOSITION, CONTENT_TYPE, HeaderName}; use hyper::http::HeaderValue; use hyper::{Body, Method, Request, Response}; +use jsonwebtoken::TokenData; use metrics::{Encoder, IntCounter, TextEncoder, register_int_counter}; use once_cell::sync::Lazy; use pprof::ProfilerGuardBuilder; @@ -618,7 +619,7 @@ pub fn auth_middleware( })?; let token = parse_token(header_value)?; - let data = auth.decode(token).map_err(|err| { + let data: TokenData = auth.decode(token).map_err(|err| { warn!("Authentication error: {err}"); // Rely on From for ApiError impl err diff --git a/libs/utils/Cargo.toml b/libs/utils/Cargo.toml index fd2fa63fd0..7b1dc56071 100644 --- a/libs/utils/Cargo.toml +++ b/libs/utils/Cargo.toml @@ -29,6 +29,7 @@ futures = { workspace = true } jsonwebtoken.workspace = true nix = { workspace = true, features = ["ioctl"] } once_cell.workspace = true +pem.workspace = true pin-project-lite.workspace = true regex.workspace = true serde.workspace = true diff --git a/libs/utils/src/auth.rs b/libs/utils/src/auth.rs index db4fc5685c..de3a964d23 100644 --- a/libs/utils/src/auth.rs +++ b/libs/utils/src/auth.rs @@ -11,7 +11,8 @@ use camino::Utf8Path; use jsonwebtoken::{ Algorithm, DecodingKey, EncodingKey, Header, TokenData, Validation, decode, encode, }; -use serde::{Deserialize, Serialize}; +use pem::Pem; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; use crate::id::TenantId; @@ -73,7 +74,10 @@ impl SwappableJwtAuth { pub fn swap(&self, jwt_auth: JwtAuth) { self.0.swap(Arc::new(jwt_auth)); } - pub fn decode(&self, token: &str) -> std::result::Result, AuthError> { + pub fn decode( + &self, + token: &str, + ) -> std::result::Result, AuthError> { self.0.load().decode(token) } } @@ -148,7 +152,10 @@ impl JwtAuth { /// The function tries the stored decoding keys in succession, /// and returns the first yielding a successful result. /// If there is no working decoding key, it returns the last error. - pub fn decode(&self, token: &str) -> std::result::Result, AuthError> { + pub fn decode( + &self, + token: &str, + ) -> std::result::Result, AuthError> { let mut res = None; for decoding_key in &self.decoding_keys { res = Some(decode(token, decoding_key, &self.validation)); @@ -173,8 +180,8 @@ impl std::fmt::Debug for JwtAuth { } // this function is used only for testing purposes in CLI e g generate tokens during init -pub fn encode_from_key_file(claims: &S, key_data: &[u8]) -> Result { - let key = EncodingKey::from_ed_pem(key_data)?; +pub fn encode_from_key_file(claims: &S, pem: &Pem) -> Result { + let key = EncodingKey::from_ed_der(pem.contents()); Ok(encode(&Header::new(STORAGE_TOKEN_ALGORITHM), claims, &key)?) } @@ -188,13 +195,13 @@ mod tests { // // openssl genpkey -algorithm ed25519 -out ed25519-priv.pem // openssl pkey -in ed25519-priv.pem -pubout -out ed25519-pub.pem - const TEST_PUB_KEY_ED25519: &[u8] = br#" + const TEST_PUB_KEY_ED25519: &str = r#" -----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEARYwaNBayR+eGI0iXB4s3QxE3Nl2g1iWbr6KtLWeVD/w= -----END PUBLIC KEY----- "#; - const TEST_PRIV_KEY_ED25519: &[u8] = br#" + const TEST_PRIV_KEY_ED25519: &str = r#" -----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEID/Drmc1AA6U/znNRWpF3zEGegOATQxfkdWxitcOMsIH -----END PRIVATE KEY----- @@ -222,9 +229,9 @@ MC4CAQAwBQYDK2VwBCIEID/Drmc1AA6U/znNRWpF3zEGegOATQxfkdWxitcOMsIH // Check it can be validated with the public key let auth = JwtAuth::new(vec![ - DecodingKey::from_ed_pem(TEST_PUB_KEY_ED25519).unwrap(), + DecodingKey::from_ed_pem(TEST_PUB_KEY_ED25519.as_bytes()).unwrap(), ]); - let claims_from_token = auth.decode(encoded_eddsa).unwrap().claims; + let claims_from_token: Claims = auth.decode(encoded_eddsa).unwrap().claims; assert_eq!(claims_from_token, expected_claims); } @@ -235,13 +242,14 @@ MC4CAQAwBQYDK2VwBCIEID/Drmc1AA6U/znNRWpF3zEGegOATQxfkdWxitcOMsIH scope: Scope::Tenant, }; - let encoded = encode_from_key_file(&claims, TEST_PRIV_KEY_ED25519).unwrap(); + let pem = pem::parse(TEST_PRIV_KEY_ED25519).unwrap(); + let encoded = encode_from_key_file(&claims, &pem).unwrap(); // decode it back let auth = JwtAuth::new(vec![ - DecodingKey::from_ed_pem(TEST_PUB_KEY_ED25519).unwrap(), + DecodingKey::from_ed_pem(TEST_PUB_KEY_ED25519.as_bytes()).unwrap(), ]); - let decoded = auth.decode(&encoded).unwrap(); + let decoded: TokenData = auth.decode(&encoded).unwrap(); assert_eq!(decoded.claims, claims); } diff --git a/pageserver/Cargo.toml b/pageserver/Cargo.toml index 74f3fce6e5..5c5bab0642 100644 --- a/pageserver/Cargo.toml +++ b/pageserver/Cargo.toml @@ -35,6 +35,7 @@ humantime.workspace = true humantime-serde.workspace = true hyper0.workspace = true itertools.workspace = true +jsonwebtoken.workspace = true md5.workspace = true nix.workspace = true # hack to get the number of worker threads tokio uses diff --git a/pageserver/src/page_service.rs b/pageserver/src/page_service.rs index 7a62d8049b..560ac75f4a 100644 --- a/pageserver/src/page_service.rs +++ b/pageserver/src/page_service.rs @@ -15,6 +15,7 @@ use async_compression::tokio::write::GzipEncoder; use bytes::Buf; use futures::FutureExt; use itertools::Itertools; +use jsonwebtoken::TokenData; use once_cell::sync::OnceCell; use pageserver_api::config::{ PageServicePipeliningConfig, PageServicePipeliningConfigPipelined, @@ -2837,7 +2838,7 @@ where ) -> Result<(), QueryError> { // this unwrap is never triggered, because check_auth_jwt only called when auth_type is NeonJWT // which requires auth to be present - let data = self + let data: TokenData = self .auth .as_ref() .unwrap() diff --git a/safekeeper/Cargo.toml b/safekeeper/Cargo.toml index 965aa7504b..a0ba69aa34 100644 --- a/safekeeper/Cargo.toml +++ b/safekeeper/Cargo.toml @@ -27,6 +27,7 @@ humantime.workspace = true http.workspace = true hyper0.workspace = true itertools.workspace = true +jsonwebtoken.workspace = true futures.workspace = true once_cell.workspace = true parking_lot.workspace = true diff --git a/safekeeper/src/handler.rs b/safekeeper/src/handler.rs index 5ca3d1b7c2..b54bee8bfb 100644 --- a/safekeeper/src/handler.rs +++ b/safekeeper/src/handler.rs @@ -6,6 +6,7 @@ use std::str::{self, FromStr}; use std::sync::Arc; use anyhow::Context; +use jsonwebtoken::TokenData; use pageserver_api::models::ShardParameters; use pageserver_api::shard::{ShardIdentity, ShardStripeSize}; use postgres_backend::{PostgresBackend, QueryError}; @@ -278,7 +279,7 @@ impl postgres_backend::Handler .auth .as_ref() .expect("auth_type is configured but .auth of handler is missing"); - let data = auth + let data: TokenData = auth .decode(str::from_utf8(jwt_response).context("jwt response is not UTF-8")?) .map_err(|e| QueryError::Unauthorized(e.0))?; diff --git a/workspace_hack/Cargo.toml b/workspace_hack/Cargo.toml index b548a2a88a..2c37cebc27 100644 --- a/workspace_hack/Cargo.toml +++ b/workspace_hack/Cargo.toml @@ -70,6 +70,7 @@ num-traits = { version = "0.2", features = ["i128", "libm"] } once_cell = { version = "1" } p256 = { version = "0.13", features = ["jwk"] } parquet = { version = "53", default-features = false, features = ["zstd"] } +pkcs8 = { version = "0.10", default-features = false, features = ["pem", "std"] } prost = { version = "0.13", features = ["no-recursion-limit", "prost-derive"] } rand = { version = "0.8", features = ["small_rng"] } regex = { version = "1" }