1use std::collections::HashMap;
16use std::future::Future;
17use std::str::FromStr;
18use std::sync::Arc;
19use std::time::Instant;
20
21use bytes::Bytes;
22use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode, decode_header};
23use parking_lot::Mutex;
24use risingwave_common::config::HbaEntry;
25use risingwave_common::id::DatabaseId;
26use risingwave_common::types::DataType;
27use risingwave_common::util::runtime::BackgroundShutdownRuntime;
28use risingwave_common::util::tokio_util::sync::CancellationToken;
29use risingwave_sqlparser::ast::Statement;
30use serde::Deserialize;
31use thiserror_ext::AsReport;
32
33use crate::error::{PsqlError, PsqlResult};
34use crate::ldap_auth::LdapAuthenticator;
35use crate::net::{AddressRef, Listener, TcpKeepalive};
36use crate::pg_field_descriptor::PgFieldDescriptor;
37use crate::pg_message::TransactionStatus;
38use crate::pg_protocol::{ConnectionContext, PgByteStream, PgProtocol};
39use crate::pg_response::{PgResponse, ValuesStream};
40use crate::types::Format;
41
42pub type BoxedError = Box<dyn std::error::Error + Send + Sync>;
43type ProcessId = i32;
44type SecretKey = i32;
45pub type SessionId = (ProcessId, SecretKey);
46
47pub trait SessionManager: Send + Sync + 'static {
50 type Error: Into<BoxedError>;
51 type Session: Session<Error = Self::Error>;
52
53 fn create_dummy_session(
56 &self,
57 database_id: DatabaseId,
58 ) -> Result<Arc<Self::Session>, Self::Error>;
59
60 fn connect(
61 &self,
62 database: &str,
63 user_name: &str,
64 peer_addr: AddressRef,
65 ) -> Result<Arc<Self::Session>, Self::Error>;
66
67 fn cancel_queries_in_session(&self, session_id: SessionId);
68
69 fn cancel_creating_jobs_in_session(&self, session_id: SessionId);
70
71 fn end_session(&self, session: &Self::Session);
72
73 fn shutdown(&self) -> impl Future<Output = ()> + Send {
75 async {}
76 }
77}
78
79pub trait Session: Send + Sync {
82 type Error: Into<BoxedError>;
83 type ValuesStream: ValuesStream;
84 type PreparedStatement: Send + Clone + 'static;
85 type Portal: Send + Clone + std::fmt::Display + 'static;
86
87 fn run_one_query(
90 self: Arc<Self>,
91 stmt: Statement,
92 format: Format,
93 ) -> impl Future<Output = Result<PgResponse<Self::ValuesStream>, Self::Error>> + Send;
94
95 fn parse(
96 self: Arc<Self>,
97 sql: Option<Statement>,
98 params_types: Vec<Option<DataType>>,
99 ) -> impl Future<Output = Result<Self::PreparedStatement, Self::Error>> + Send;
100
101 fn next_notice(self: &Arc<Self>) -> impl Future<Output = String> + Send;
105
106 fn bind(
107 self: Arc<Self>,
108 prepare_statement: Self::PreparedStatement,
109 params: Vec<Option<Bytes>>,
110 param_formats: Vec<Format>,
111 result_formats: Vec<Format>,
112 ) -> Result<Self::Portal, Self::Error>;
113
114 fn execute(
115 self: Arc<Self>,
116 portal: Self::Portal,
117 ) -> impl Future<Output = Result<PgResponse<Self::ValuesStream>, Self::Error>> + Send;
118
119 fn describe_statement(
120 self: Arc<Self>,
121 prepare_statement: Self::PreparedStatement,
122 ) -> Result<(Vec<DataType>, Vec<PgFieldDescriptor>), Self::Error>;
123
124 fn describe_portal(
125 self: Arc<Self>,
126 portal: Self::Portal,
127 ) -> Result<Vec<PgFieldDescriptor>, Self::Error>;
128
129 fn user_authenticator(&self) -> &UserAuthenticator;
130
131 fn id(&self) -> SessionId;
132
133 fn get_config(&self, key: &str) -> Result<String, Self::Error>;
134
135 fn set_config(&self, key: &str, value: String) -> Result<String, Self::Error>;
136
137 fn transaction_status(&self) -> TransactionStatus;
138
139 fn init_exec_context(&self, sql: Arc<str>) -> ExecContextGuard;
140
141 fn check_idle_in_transaction_timeout(&self) -> PsqlResult<()>;
142
143 fn user(&self) -> String;
144}
145
146pub struct ExecContext {
149 pub running_sql: Arc<str>,
150 pub last_instant: Instant,
152 pub last_idle_instant: Arc<Mutex<Option<Instant>>>,
154}
155
156pub struct ExecContextGuard(#[expect(dead_code)] Arc<ExecContext>);
159
160impl ExecContextGuard {
161 pub fn new(exec_context: Arc<ExecContext>) -> Self {
162 Self(exec_context)
163 }
164}
165
166impl Drop for ExecContext {
167 fn drop(&mut self) {
168 *self.last_idle_instant.lock() = Some(Instant::now());
169 }
170}
171
172#[derive(Debug, Clone)]
173pub enum UserAuthenticator {
174 None,
176 ClearText(Vec<u8>),
178 Md5WithSalt {
180 encrypted_password: Vec<u8>,
181 salt: [u8; 4],
182 },
183 OAuth {
184 metadata: HashMap<String, String>,
185 cluster_id: String,
186 },
187 Ldap(String, HbaEntry),
188}
189
190#[derive(Debug, Deserialize)]
194struct Jwks {
195 keys: Vec<Jwk>,
196}
197
198#[derive(Debug, Deserialize)]
201struct Jwk {
202 kty: Option<String>, kid: Option<String>, alg: Option<String>, n: Option<String>, e: Option<String>, }
208
209const ALLOWED_JWT_ALGORITHMS: &[Algorithm] = &[
218 Algorithm::RS256,
219 Algorithm::RS384,
220 Algorithm::RS512,
221 Algorithm::PS256,
222 Algorithm::PS384,
223 Algorithm::PS512,
224];
225const RSA_JWK_KEY_TYPE: &str = "RSA";
226const OAUTH_AUDIENCE_KEY: &str = "audience";
228
229async fn validate_jwt(
230 jwt: &str,
231 jwks_url: &str,
232 issuer: &str,
233 audience: &str,
234 metadata: &HashMap<String, String>,
235) -> Result<bool, BoxedError> {
236 let jwks: Jwks = reqwest::get(jwks_url).await?.json().await?;
237 validate_jwt_with_jwks(jwt, &jwks, issuer, audience, metadata)
238}
239
240fn audience_from_cluster_id(cluster_id: &str) -> String {
241 format!("urn:risingwave:cluster:{}", cluster_id)
242}
243
244fn resolve_oauth_audience(
245 configured: Option<String>,
246 cluster_id: &str,
247) -> Result<String, BoxedError> {
248 let Some(configured_audience) = configured else {
249 return Ok(audience_from_cluster_id(cluster_id));
250 };
251
252 let audience = configured_audience.trim();
253 if audience.is_empty() {
254 return Err(format!("OAuth option `{OAUTH_AUDIENCE_KEY}` cannot be empty").into());
255 }
256 Ok(audience.to_owned())
257}
258
259fn validate_jwt_with_jwks(
260 jwt: &str,
261 jwks: &Jwks,
262 issuer: &str,
263 audience: &str,
264 metadata: &HashMap<String, String>,
265) -> Result<bool, BoxedError> {
266 let header = decode_header(jwt)?;
267
268 let kid = header.kid.ok_or("JWT header missing 'kid' field")?;
270
271 if !ALLOWED_JWT_ALGORITHMS.contains(&header.alg) {
280 return Err(format!("JWT alg {:?} is not allowed", header.alg).into());
281 }
282
283 let jwk = jwks
288 .keys
289 .iter()
290 .filter(|jwk| jwk.kid.as_deref() == Some(kid.as_str()))
291 .find(|jwk| {
292 jwk.kty.as_deref() == Some(RSA_JWK_KEY_TYPE)
293 && jwk.n.is_some()
294 && jwk.e.is_some()
295 && match jwk.alg.as_deref() {
296 Some(alg) => Algorithm::from_str(alg).is_ok_and(|alg| alg == header.alg),
297 None => true,
298 }
299 })
300 .ok_or_else(|| format!("No compatible RSA key found in JWKS for kid: '{}'", kid))?;
301
302 let n = jwk.n.as_deref().expect("RSA candidate must have a modulus");
304 let e = jwk
305 .e
306 .as_deref()
307 .expect("RSA candidate must have an exponent");
308 let decoding_key = DecodingKey::from_rsa_components(n, e)?;
309 let mut validation = Validation::new(header.alg);
310 validation.set_issuer(&[issuer]);
311 validation.set_audience(&[audience]);
312 validation.set_required_spec_claims(&["exp", "iss", "aud"]);
313 let token_data = decode::<HashMap<String, serde_json::Value>>(jwt, &decoding_key, &validation)?;
314
315 if !metadata.iter().all(
317 |(k, v)| matches!(token_data.claims.get(k), Some(serde_json::Value::String(s)) if s == v),
318 ) {
319 return Err("metadata in jwt does not match with metadata declared with user".into());
320 }
321 Ok(true)
322}
323
324impl UserAuthenticator {
325 pub async fn authenticate(&self, password: &[u8]) -> PsqlResult<()> {
326 let success = match self {
327 UserAuthenticator::None => true,
328 UserAuthenticator::ClearText(text) => password == text,
329 UserAuthenticator::Md5WithSalt {
330 encrypted_password, ..
331 } => encrypted_password == password,
332 UserAuthenticator::OAuth {
333 metadata,
334 cluster_id,
335 } => {
336 let mut metadata = metadata.clone();
337 let jwks_url = metadata.remove("jwks_url").unwrap();
338 let issuer = metadata.remove("issuer").unwrap();
339 let audience =
340 resolve_oauth_audience(metadata.remove(OAUTH_AUDIENCE_KEY), cluster_id)
341 .map_err(PsqlError::StartupError)?;
342 validate_jwt(
343 &String::from_utf8_lossy(password),
344 &jwks_url,
345 &issuer,
346 &audience,
347 &metadata,
348 )
349 .await
350 .map_err(PsqlError::StartupError)?
351 }
352 UserAuthenticator::Ldap(user_name, hba_entry) => {
353 let ldap_auth = LdapAuthenticator::new(hba_entry)?;
354 let password_str = String::from_utf8_lossy(password).into_owned();
356 ldap_auth.authenticate(user_name, &password_str).await?
357 }
358 };
359 if !success {
360 return Err(PsqlError::PasswordError);
361 }
362 Ok(())
363 }
364}
365
366pub async fn pg_serve(
370 addr: &str,
371 tcp_keepalive: TcpKeepalive,
372 session_mgr: Arc<impl SessionManager>,
373 context: ConnectionContext,
374 shutdown: CancellationToken,
375) -> Result<(), BoxedError> {
376 let listener = Listener::bind(addr).await?;
377 tracing::info!(addr, "server started");
378
379 let acceptor_runtime = BackgroundShutdownRuntime::from({
380 let mut builder = tokio::runtime::Builder::new_multi_thread();
381 builder.worker_threads(1);
382 builder
383 .thread_name("rw-acceptor")
384 .enable_all()
385 .build()
386 .unwrap()
387 });
388
389 #[cfg(not(madsim))]
390 let worker_runtime = tokio::runtime::Handle::current();
391 #[cfg(madsim)]
392 let worker_runtime = tokio::runtime::Builder::new_multi_thread().build().unwrap();
393 let session_mgr_clone = session_mgr.clone();
394 let f = async move {
395 loop {
396 let conn_ret = listener.accept(&tcp_keepalive).await;
397 match conn_ret {
398 Ok((stream, peer_addr)) => {
399 tracing::info!(%peer_addr, "accept connection");
400 worker_runtime.spawn(handle_connection(
401 stream,
402 session_mgr_clone.clone(),
403 Arc::new(peer_addr),
404 context.clone(),
405 ));
406 }
407
408 Err(e) => {
409 tracing::error!(error = %e.as_report(), "failed to accept connection",);
410 }
411 }
412 }
413 };
414 acceptor_runtime.spawn(f);
415
416 shutdown.cancelled().await;
418
419 drop(acceptor_runtime);
421 session_mgr.shutdown().await;
423
424 Ok(())
425}
426
427pub async fn handle_connection<S, SM>(
428 stream: S,
429 session_mgr: Arc<SM>,
430 peer_addr: AddressRef,
431 context: ConnectionContext,
432) where
433 S: PgByteStream,
434 SM: SessionManager,
435{
436 PgProtocol::new(stream, session_mgr, peer_addr, context)
437 .run()
438 .await;
439}
440#[cfg(test)]
441mod tests {
442 use std::sync::Arc;
443 use std::time::{Duration, Instant};
444
445 use bytes::Bytes;
446 use futures::StreamExt;
447 use futures::stream::BoxStream;
448 use risingwave_common::id::DatabaseId;
449 use risingwave_common::types::DataType;
450 use risingwave_common::util::tokio_util::sync::CancellationToken;
451 use risingwave_sqlparser::ast::Statement;
452 use tokio_postgres::NoTls;
453
454 use crate::error::PsqlResult;
455 use crate::memory_manager::MessageMemoryManager;
456 use crate::pg_field_descriptor::PgFieldDescriptor;
457 use crate::pg_message::TransactionStatus;
458 use crate::pg_protocol::ConnectionContext;
459 use crate::pg_response::{PgResponse, RowSetResult, StatementType};
460 use crate::pg_server::{
461 BoxedError, ExecContext, ExecContextGuard, Session, SessionId, SessionManager,
462 UserAuthenticator, pg_serve,
463 };
464 use crate::types;
465 use crate::types::Row;
466
467 struct MockSessionManager {}
468 struct MockSession {}
469
470 const STREAMING_TEST_QUERY: &str = "SELECT 'pgwire_streaming_test'";
471
472 impl SessionManager for MockSessionManager {
473 type Error = BoxedError;
474 type Session = MockSession;
475
476 fn create_dummy_session(
477 &self,
478 _database_id: DatabaseId,
479 ) -> Result<Arc<Self::Session>, Self::Error> {
480 unimplemented!()
481 }
482
483 fn connect(
484 &self,
485 _database: &str,
486 _user_name: &str,
487 _peer_addr: crate::net::AddressRef,
488 ) -> Result<Arc<Self::Session>, Self::Error> {
489 Ok(Arc::new(MockSession {}))
490 }
491
492 fn cancel_queries_in_session(&self, _session_id: SessionId) {
493 todo!()
494 }
495
496 fn cancel_creating_jobs_in_session(&self, _session_id: SessionId) {
497 todo!()
498 }
499
500 fn end_session(&self, _session: &Self::Session) {}
501 }
502
503 impl Session for MockSession {
504 type Error = BoxedError;
505 type Portal = String;
506 type PreparedStatement = String;
507 type ValuesStream = BoxStream<'static, RowSetResult>;
508
509 async fn run_one_query(
510 self: Arc<Self>,
511 _stmt: Statement,
512 _format: types::Format,
513 ) -> Result<PgResponse<BoxStream<'static, RowSetResult>>, Self::Error> {
514 Ok(PgResponse::builder(StatementType::SELECT)
515 .values(
516 futures::stream::iter(vec![Ok(vec![Row::new(vec![Some(Bytes::new())])])])
517 .boxed(),
518 vec![
519 PgFieldDescriptor::new("".to_owned(), 1043, -1);
522 1
523 ],
524 )
525 .into())
526 }
527
528 async fn parse(
529 self: Arc<Self>,
530 sql: Option<Statement>,
531 _params_types: Vec<Option<DataType>>,
532 ) -> Result<String, Self::Error> {
533 Ok(sql.map(|stmt| stmt.to_string()).unwrap_or_default())
534 }
535
536 fn bind(
537 self: Arc<Self>,
538 prepare_statement: String,
539 _params: Vec<Option<Bytes>>,
540 _param_formats: Vec<types::Format>,
541 _result_formats: Vec<types::Format>,
542 ) -> Result<String, Self::Error> {
543 Ok(prepare_statement)
544 }
545
546 async fn execute(
547 self: Arc<Self>,
548 portal: String,
549 ) -> Result<PgResponse<BoxStream<'static, RowSetResult>>, Self::Error> {
550 if portal == STREAMING_TEST_QUERY {
551 let first_row = futures::stream::once(async {
552 Ok(vec![Row::new(vec![Some(Bytes::from(vec![
553 b'x';
554 128 * 1024
555 ]))])])
556 });
557 let remaining_rows = futures::stream::pending();
558 return Ok(PgResponse::builder(StatementType::SELECT)
559 .values(
560 first_row.chain(remaining_rows).boxed(),
561 vec![PgFieldDescriptor::new("".to_owned(), 1043, -1)],
562 )
563 .into());
564 }
565
566 Ok(PgResponse::builder(StatementType::SELECT)
567 .values(
568 futures::stream::iter(vec![Ok(vec![Row::new(vec![Some(Bytes::new())])])])
569 .boxed(),
570 vec![
571 PgFieldDescriptor::new("".to_owned(), 1043, -1);
574 1
575 ],
576 )
577 .into())
578 }
579
580 fn describe_statement(
581 self: Arc<Self>,
582 _statement: String,
583 ) -> Result<(Vec<DataType>, Vec<PgFieldDescriptor>), Self::Error> {
584 Ok((
585 vec![],
586 vec![PgFieldDescriptor::new("".to_owned(), 1043, -1)],
587 ))
588 }
589
590 fn describe_portal(
591 self: Arc<Self>,
592 _portal: String,
593 ) -> Result<Vec<PgFieldDescriptor>, Self::Error> {
594 Ok(vec![PgFieldDescriptor::new("".to_owned(), 1043, -1)])
595 }
596
597 fn user_authenticator(&self) -> &UserAuthenticator {
598 &UserAuthenticator::None
599 }
600
601 fn id(&self) -> SessionId {
602 (0, 0)
603 }
604
605 fn get_config(&self, key: &str) -> Result<String, Self::Error> {
606 match key {
607 "timezone" => Ok("UTC".to_owned()),
608 _ => Err(format!("Unknown config key: {key}").into()),
609 }
610 }
611
612 fn set_config(&self, _key: &str, _value: String) -> Result<String, Self::Error> {
613 Ok("".to_owned())
614 }
615
616 async fn next_notice(self: &Arc<Self>) -> String {
617 std::future::pending().await
618 }
619
620 fn transaction_status(&self) -> TransactionStatus {
621 TransactionStatus::Idle
622 }
623
624 fn init_exec_context(&self, sql: Arc<str>) -> ExecContextGuard {
625 let exec_context = Arc::new(ExecContext {
626 running_sql: sql,
627 last_instant: Instant::now(),
628 last_idle_instant: Default::default(),
629 });
630 ExecContextGuard::new(exec_context)
631 }
632
633 fn check_idle_in_transaction_timeout(&self) -> PsqlResult<()> {
634 Ok(())
635 }
636
637 fn user(&self) -> String {
638 "mock".to_owned()
639 }
640 }
641
642 async fn do_test_query(bind_addr: impl Into<String>, pg_config: impl Into<String>) {
643 let bind_addr = bind_addr.into();
644 let pg_config = pg_config.into();
645
646 let session_mgr = MockSessionManager {};
647 tokio::spawn(async move {
648 pg_serve(
649 &bind_addr,
650 socket2::TcpKeepalive::new(),
651 Arc::new(session_mgr),
652 ConnectionContext {
653 tls_config: None,
654 redact_sql_option_keywords: None,
655 message_memory_manager: MessageMemoryManager::new(u64::MAX, u64::MAX, u64::MAX)
656 .into(),
657 stream_flush_threshold_bytes: 64 * 1024,
658 },
659 CancellationToken::new(), )
661 .await
662 });
663 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
665
666 let (client, connection) = tokio_postgres::connect(&pg_config, NoTls).await.unwrap();
668
669 tokio::spawn(async move {
672 if let Err(e) = connection.await {
673 eprintln!("connection error: {}", e);
674 }
675 });
676
677 let rows = client
678 .simple_query("SELECT ''")
679 .await
680 .expect("Error executing query");
681 assert_eq!(rows.len(), 2);
683
684 let rows = client
685 .query("SELECT ''", &[])
686 .await
687 .expect("Error executing query");
688 assert_eq!(rows.len(), 1);
689 }
690
691 #[tokio::test]
692 async fn test_query_tcp() {
693 do_test_query("127.0.0.1:10000", "host=localhost port=10000").await;
694 }
695
696 #[cfg(not(madsim))]
697 #[tokio::test]
698 async fn test_query_unix() {
699 let port: i16 = 10000;
700 let dir = tempfile::TempDir::new().unwrap();
701 let sock = dir.path().join(format!(".s.PGSQL.{port}"));
702
703 do_test_query(
704 format!("unix:{}", sock.to_str().unwrap()),
705 format!("host={} port={}", dir.path().to_str().unwrap(), port),
706 )
707 .await;
708 }
709
710 #[cfg(not(madsim))]
711 #[tokio::test]
712 async fn test_large_query_row_is_streamed_before_query_finishes() {
713 let port: i16 = 10001;
714 let dir = tempfile::TempDir::new().unwrap();
715 let sock = dir.path().join(format!(".s.PGSQL.{port}"));
716 let bind_addr = format!("unix:{}", sock.to_str().unwrap());
717 let pg_config = format!("host={} port={port}", dir.path().to_str().unwrap());
718 let cancellation = CancellationToken::new();
719
720 let server_cancellation = cancellation.clone();
721 tokio::spawn(async move {
722 pg_serve(
723 &bind_addr,
724 socket2::TcpKeepalive::new(),
725 Arc::new(MockSessionManager {}),
726 ConnectionContext {
727 tls_config: None,
728 redact_sql_option_keywords: None,
729 message_memory_manager: MessageMemoryManager::new(u64::MAX, u64::MAX, u64::MAX)
730 .into(),
731 stream_flush_threshold_bytes: 64 * 1024,
732 },
733 server_cancellation,
734 )
735 .await
736 });
737 tokio::time::sleep(Duration::from_millis(100)).await;
738
739 let (client, connection) = tokio_postgres::connect(&pg_config, NoTls).await.unwrap();
740 let connection_handle = tokio::spawn(connection);
741 let params: &[&str] = &[];
742 let rows = client
743 .query_raw(STREAMING_TEST_QUERY, params)
744 .await
745 .unwrap();
746 futures::pin_mut!(rows);
747
748 let first_row = tokio::time::timeout(Duration::from_secs(5), rows.next())
749 .await
750 .expect("first row should be sent before the result stream finishes")
751 .unwrap()
752 .unwrap();
753 assert_eq!(first_row.get::<_, &str>(0).len(), 128 * 1024);
754
755 drop(client);
756 cancellation.cancel();
757 connection_handle.abort();
758 }
759
760 mod jwt_validation_tests {
761 use std::collections::HashMap;
762 use std::time::{SystemTime, UNIX_EPOCH};
763
764 use base64::Engine;
765 use jsonwebtoken::{Algorithm, EncodingKey, Header};
766 use rsa::pkcs1::EncodeRsaPrivateKey;
767 use rsa::traits::PublicKeyParts;
768 use rsa::{RsaPrivateKey, RsaPublicKey};
769 use serde_json::json;
770
771 use crate::pg_server::{Jwk, Jwks, resolve_oauth_audience, validate_jwt_with_jwks};
772
773 fn create_test_rsa_keys() -> (RsaPrivateKey, RsaPublicKey) {
774 let mut rng = rand::thread_rng();
775 let private_key = RsaPrivateKey::new(&mut rng, 2048).expect("failed to generate a key");
776 let public_key = RsaPublicKey::from(&private_key);
777 (private_key, public_key)
778 }
779
780 fn create_test_jwks(public_key: &RsaPublicKey, kid: &str, alg: Option<&str>) -> Jwks {
781 let n = base64::engine::general_purpose::URL_SAFE_NO_PAD
782 .encode(public_key.n().to_bytes_be());
783 let e = base64::engine::general_purpose::URL_SAFE_NO_PAD
784 .encode(public_key.e().to_bytes_be());
785
786 Jwks {
787 keys: vec![Jwk {
788 kty: Some("RSA".to_owned()),
789 kid: Some(kid.to_owned()),
790 alg: alg.map(ToOwned::to_owned),
791 n: Some(n),
792 e: Some(e),
793 }],
794 }
795 }
796
797 fn create_jwt_token(
798 private_key: &RsaPrivateKey,
799 kid: &str,
800 algorithm: Algorithm,
801 issuer: &str,
802 audience: Option<&str>,
803 exp: u64,
804 additional_claims: HashMap<String, serde_json::Value>,
805 ) -> String {
806 let mut header = Header::new(algorithm);
807 header.kid = Some(kid.to_owned());
808
809 let mut claims = json!({
810 "iss": issuer,
811 "exp": exp,
812 });
813
814 if let Some(aud) = audience {
815 claims["aud"] = json!(aud);
816 }
817
818 for (key, value) in additional_claims {
819 claims[key] = value;
820 }
821
822 let encoding_key = EncodingKey::from_rsa_pem(
823 private_key
824 .to_pkcs1_pem(rsa::pkcs1::LineEnding::LF)
825 .unwrap()
826 .as_bytes(),
827 )
828 .unwrap();
829
830 jsonwebtoken::encode(&header, &claims, &encoding_key).unwrap()
831 }
832
833 fn get_future_timestamp() -> u64 {
834 SystemTime::now()
835 .duration_since(UNIX_EPOCH)
836 .unwrap()
837 .as_secs()
838 + 3600 }
840
841 fn get_past_timestamp() -> u64 {
842 SystemTime::now()
843 .duration_since(UNIX_EPOCH)
844 .unwrap()
845 .as_secs()
846 - 3600 }
848
849 #[test]
850 fn test_jwt_with_invalid_audience() {
851 let (private_key, public_key) = create_test_rsa_keys();
852 let jwks = create_test_jwks(&public_key, "test-kid", Some("RS256"));
853
854 let metadata = HashMap::new();
855
856 let jwt = create_jwt_token(
857 &private_key,
858 "test-kid",
859 Algorithm::RS256,
860 "https://test-issuer.com",
861 Some("urn:risingwave:cluster:wrong-cluster-id"),
862 get_future_timestamp(),
863 HashMap::new(),
864 );
865
866 let result = validate_jwt_with_jwks(
867 &jwt,
868 &jwks,
869 "https://test-issuer.com",
870 "urn:risingwave:cluster:test-cluster-id",
871 &metadata,
872 );
873
874 let error = result.unwrap_err();
875 assert!(error.to_string().contains("InvalidAudience"));
876 }
877
878 #[test]
879 fn test_jwt_with_configured_audience() {
880 let (private_key, public_key) = create_test_rsa_keys();
881 let jwks = create_test_jwks(&public_key, "test-kid", Some("RS256"));
882
883 let mut additional_claims = HashMap::new();
884 additional_claims.insert("aud".to_owned(), json!(["urn:sn:cloud:o-for6u"]));
885 let jwt = create_jwt_token(
886 &private_key,
887 "test-kid",
888 Algorithm::RS256,
889 "https://test-issuer.com",
890 None,
891 get_future_timestamp(),
892 additional_claims,
893 );
894
895 let result = validate_jwt_with_jwks(
896 &jwt,
897 &jwks,
898 "https://test-issuer.com",
899 "urn:sn:cloud:o-for6u",
900 &HashMap::new(),
901 );
902
903 assert!(result.unwrap());
904 }
905
906 #[test]
907 fn test_empty_configured_audience() {
908 let error =
909 resolve_oauth_audience(Some(" ".to_owned()), "test-cluster-id").unwrap_err();
910 assert!(error.to_string().contains("cannot be empty"));
911 }
912
913 #[test]
914 fn test_default_audience_from_cluster_id() {
915 let audience = resolve_oauth_audience(None, "test-cluster-id").unwrap();
916 assert_eq!(audience, "urn:risingwave:cluster:test-cluster-id");
917 }
918
919 #[test]
920 fn test_jwt_with_missing_audience() {
921 let (private_key, public_key) = create_test_rsa_keys();
922 let jwks = create_test_jwks(&public_key, "test-kid", Some("RS256"));
923
924 let metadata = HashMap::new();
925
926 let jwt = create_jwt_token(
927 &private_key,
928 "test-kid",
929 Algorithm::RS256,
930 "https://test-issuer.com",
931 None, get_future_timestamp(),
933 HashMap::new(),
934 );
935
936 let result = validate_jwt_with_jwks(
937 &jwt,
938 &jwks,
939 "https://test-issuer.com",
940 "urn:risingwave:cluster:test-cluster-id",
941 &metadata,
942 );
943
944 let error = result.unwrap_err();
945 assert!(error.to_string().contains("Missing required claim: aud"));
946 }
947
948 #[test]
949 fn test_jwt_with_invalid_issuer() {
950 let (private_key, public_key) = create_test_rsa_keys();
951 let jwks = create_test_jwks(&public_key, "test-kid", Some("RS256"));
952
953 let metadata = HashMap::new();
954
955 let jwt = create_jwt_token(
956 &private_key,
957 "test-kid",
958 Algorithm::RS256,
959 "https://wrong-issuer.com",
960 Some("urn:risingwave:cluster:test-cluster-id"),
961 get_future_timestamp(),
962 HashMap::new(),
963 );
964
965 let result = validate_jwt_with_jwks(
966 &jwt,
967 &jwks,
968 "https://test-issuer.com",
969 "urn:risingwave:cluster:test-cluster-id",
970 &metadata,
971 );
972
973 let error = result.unwrap_err();
974 assert!(error.to_string().contains("InvalidIssuer"));
975 }
976
977 #[test]
978 fn test_jwt_with_kid_not_found_in_jwks() {
979 let (private_key, public_key) = create_test_rsa_keys();
980 let jwks = create_test_jwks(&public_key, "different-kid", Some("RS256"));
981
982 let metadata = HashMap::new();
983
984 let jwt = create_jwt_token(
985 &private_key,
986 "missing-kid",
987 Algorithm::RS256,
988 "https://test-issuer.com",
989 Some("urn:risingwave:cluster:test-cluster-id"),
990 get_future_timestamp(),
991 HashMap::new(),
992 );
993
994 let result = validate_jwt_with_jwks(
995 &jwt,
996 &jwks,
997 "https://test-issuer.com",
998 "urn:risingwave:cluster:test-cluster-id",
999 &metadata,
1000 );
1001
1002 let error = result.unwrap_err();
1003 assert!(
1004 error
1005 .to_string()
1006 .contains("No compatible RSA key found in JWKS for kid: 'missing-kid'")
1007 );
1008 }
1009
1010 #[test]
1011 fn test_jwt_with_empty_jwks_reports_no_matching_key() {
1012 let (private_key, _) = create_test_rsa_keys();
1013 let jwks = Jwks { keys: vec![] };
1014
1015 let jwt = create_jwt_token(
1016 &private_key,
1017 "missing-kid",
1018 Algorithm::RS256,
1019 "https://test-issuer.com",
1020 Some("urn:risingwave:cluster:test-cluster-id"),
1021 get_future_timestamp(),
1022 HashMap::new(),
1023 );
1024
1025 let result = validate_jwt_with_jwks(
1026 &jwt,
1027 &jwks,
1028 "https://test-issuer.com",
1029 "urn:risingwave:cluster:test-cluster-id",
1030 &HashMap::new(),
1031 );
1032
1033 let error = result.unwrap_err();
1034 assert!(
1035 error
1036 .to_string()
1037 .contains("No compatible RSA key found in JWKS for kid: 'missing-kid'")
1038 );
1039 }
1040
1041 #[test]
1042 fn test_jwt_with_expired_token() {
1043 let (private_key, public_key) = create_test_rsa_keys();
1044 let jwks = create_test_jwks(&public_key, "test-kid", Some("RS256"));
1045
1046 let metadata = HashMap::new();
1047
1048 let jwt = create_jwt_token(
1049 &private_key,
1050 "test-kid",
1051 Algorithm::RS256,
1052 "https://test-issuer.com",
1053 Some("urn:risingwave:cluster:test-cluster-id"),
1054 get_past_timestamp(), HashMap::new(),
1056 );
1057
1058 let result = validate_jwt_with_jwks(
1059 &jwt,
1060 &jwks,
1061 "https://test-issuer.com",
1062 "urn:risingwave:cluster:test-cluster-id",
1063 &metadata,
1064 );
1065
1066 let error = result.unwrap_err();
1067 assert!(error.to_string().contains("ExpiredSignature"));
1068 }
1069
1070 #[test]
1071 fn test_jwt_with_invalid_signature() {
1072 let (_, public_key) = create_test_rsa_keys();
1073 let (wrong_private_key, _) = create_test_rsa_keys(); let jwks = create_test_jwks(&public_key, "test-kid", Some("RS256"));
1075
1076 let metadata = HashMap::new();
1077
1078 let jwt = create_jwt_token(
1080 &wrong_private_key,
1081 "test-kid",
1082 Algorithm::RS256,
1083 "https://test-issuer.com",
1084 Some("urn:risingwave:cluster:test-cluster-id"),
1085 get_future_timestamp(),
1086 HashMap::new(),
1087 );
1088
1089 let result = validate_jwt_with_jwks(
1090 &jwt,
1091 &jwks,
1092 "https://test-issuer.com",
1093 "urn:risingwave:cluster:test-cluster-id",
1094 &metadata,
1095 );
1096
1097 let error = result.unwrap_err();
1098 assert!(error.to_string().contains("InvalidSignature"));
1099 }
1100
1101 #[test]
1102 fn test_metadata_validation_success() {
1103 let (private_key, public_key) = create_test_rsa_keys();
1104 let jwks = create_test_jwks(&public_key, "test-kid", Some("RS256"));
1105
1106 let mut metadata = HashMap::new();
1107 metadata.insert("role".to_owned(), "admin".to_owned());
1108 metadata.insert("department".to_owned(), "security".to_owned());
1109
1110 let mut claims = HashMap::new();
1111 claims.insert("role".to_owned(), json!("admin"));
1112 claims.insert("department".to_owned(), json!("security"));
1113 claims.insert("extra_claim".to_owned(), json!("ignored")); let jwt = create_jwt_token(
1116 &private_key,
1117 "test-kid",
1118 Algorithm::RS256,
1119 "https://test-issuer.com",
1120 Some("urn:risingwave:cluster:test-cluster-id"),
1121 get_future_timestamp(),
1122 claims,
1123 );
1124
1125 let result = validate_jwt_with_jwks(
1126 &jwt,
1127 &jwks,
1128 "https://test-issuer.com",
1129 "urn:risingwave:cluster:test-cluster-id",
1130 &metadata,
1131 );
1132
1133 assert!(result.unwrap());
1134 }
1135
1136 #[test]
1137 fn test_metadata_validation_failure() {
1138 let (private_key, public_key) = create_test_rsa_keys();
1139 let jwks = create_test_jwks(&public_key, "test-kid", Some("RS256"));
1140
1141 let mut metadata = HashMap::new();
1142 metadata.insert("role".to_owned(), "admin".to_owned());
1143 metadata.insert("department".to_owned(), "security".to_owned());
1144
1145 let mut claims = HashMap::new();
1146 claims.insert("role".to_owned(), json!("user")); claims.insert("department".to_owned(), json!("security"));
1148
1149 let jwt = create_jwt_token(
1150 &private_key,
1151 "test-kid",
1152 Algorithm::RS256,
1153 "https://test-issuer.com",
1154 Some("urn:risingwave:cluster:test-cluster-id"),
1155 get_future_timestamp(),
1156 claims,
1157 );
1158
1159 let result = validate_jwt_with_jwks(
1160 &jwt,
1161 &jwks,
1162 "https://test-issuer.com",
1163 "urn:risingwave:cluster:test-cluster-id",
1164 &metadata,
1165 );
1166
1167 let error = result.unwrap_err();
1168 assert_eq!(
1169 error.to_string(),
1170 "metadata in jwt does not match with metadata declared with user"
1171 );
1172 }
1173
1174 #[test]
1175 fn test_jwt_with_jwk_missing_alg_succeeds() {
1176 let (private_key, public_key) = create_test_rsa_keys();
1177 let jwks = create_test_jwks(&public_key, "test-kid", None);
1178
1179 let jwt = create_jwt_token(
1180 &private_key,
1181 "test-kid",
1182 Algorithm::RS256,
1183 "https://test-issuer.com",
1184 Some("urn:risingwave:cluster:test-cluster-id"),
1185 get_future_timestamp(),
1186 HashMap::new(),
1187 );
1188
1189 let result = validate_jwt_with_jwks(
1190 &jwt,
1191 &jwks,
1192 "https://test-issuer.com",
1193 "urn:risingwave:cluster:test-cluster-id",
1194 &HashMap::new(),
1195 );
1196
1197 assert!(result.unwrap());
1198 }
1199
1200 #[test]
1201 fn test_jwt_with_mixed_jwks_ignores_non_matching_ec_key() {
1202 let (private_key, public_key) = create_test_rsa_keys();
1203 let mut jwks = create_test_jwks(&public_key, "rsa-kid", Some("RS256"));
1204 jwks.keys.push(Jwk {
1205 kty: Some("EC".to_owned()),
1206 kid: Some("ec-kid".to_owned()),
1207 alg: Some("ES256".to_owned()),
1208 n: None,
1209 e: None,
1210 });
1211
1212 let jwt = create_jwt_token(
1213 &private_key,
1214 "rsa-kid",
1215 Algorithm::RS256,
1216 "https://test-issuer.com",
1217 Some("urn:risingwave:cluster:test-cluster-id"),
1218 get_future_timestamp(),
1219 HashMap::new(),
1220 );
1221
1222 let result = validate_jwt_with_jwks(
1223 &jwt,
1224 &jwks,
1225 "https://test-issuer.com",
1226 "urn:risingwave:cluster:test-cluster-id",
1227 &HashMap::new(),
1228 );
1229
1230 assert!(result.unwrap());
1231 }
1232
1233 #[test]
1234 fn test_jwks_deserializes_ec_key_without_kid() {
1235 let jwks: Jwks = serde_json::from_value(json!({
1236 "keys": [
1237 {
1238 "alg": "ES256",
1239 "kty": "EC",
1240 "crv": "P-256",
1241 "x": "x-coordinate",
1242 "y": "y-coordinate"
1243 },
1244 {
1245 "kid": "rsa-kid",
1246 "alg": "RS256",
1247 "kty": "RSA",
1248 "n": "modulus",
1249 "e": "AQAB"
1250 }
1251 ]
1252 }))
1253 .unwrap();
1254
1255 assert_eq!(jwks.keys.len(), 2);
1256 assert!(jwks.keys[0].kid.is_none());
1257 assert_eq!(jwks.keys[1].kid.as_deref(), Some("rsa-kid"));
1258 assert_eq!(jwks.keys[1].n.as_deref(), Some("modulus"));
1259 }
1260
1261 #[test]
1262 fn test_jwt_uses_compatible_rsa_key_when_ec_key_has_same_kid() {
1263 let (private_key, public_key) = create_test_rsa_keys();
1264 let mut jwks = create_test_jwks(&public_key, "shared-kid", Some("RS256"));
1265 jwks.keys.insert(
1266 0,
1267 Jwk {
1268 kty: Some("EC".to_owned()),
1269 kid: Some("shared-kid".to_owned()),
1270 alg: Some("ES256".to_owned()),
1271 n: None,
1272 e: None,
1273 },
1274 );
1275
1276 let jwt = create_jwt_token(
1277 &private_key,
1278 "shared-kid",
1279 Algorithm::RS256,
1280 "https://test-issuer.com",
1281 Some("urn:risingwave:cluster:test-cluster-id"),
1282 get_future_timestamp(),
1283 HashMap::new(),
1284 );
1285
1286 let result = validate_jwt_with_jwks(
1287 &jwt,
1288 &jwks,
1289 "https://test-issuer.com",
1290 "urn:risingwave:cluster:test-cluster-id",
1291 &HashMap::new(),
1292 );
1293
1294 assert!(result.unwrap());
1295 }
1296
1297 #[test]
1298 fn test_jwt_with_jwk_missing_alg_rejects_disallowed_header_alg() {
1299 let (_, public_key) = create_test_rsa_keys();
1300 let jwks = create_test_jwks(&public_key, "test-kid", None);
1301
1302 let mut header = Header::new(Algorithm::HS256);
1307 header.kid = Some("test-kid".to_owned());
1308 let claims = json!({
1309 "iss": "https://test-issuer.com",
1310 "aud": "urn:risingwave:cluster:test-cluster-id",
1311 "exp": get_future_timestamp(),
1312 });
1313 let jwt = jsonwebtoken::encode(
1314 &header,
1315 &claims,
1316 &EncodingKey::from_secret(b"attacker-chosen"),
1317 )
1318 .unwrap();
1319
1320 let result = validate_jwt_with_jwks(
1321 &jwt,
1322 &jwks,
1323 "https://test-issuer.com",
1324 "urn:risingwave:cluster:test-cluster-id",
1325 &HashMap::new(),
1326 );
1327
1328 let error = result.unwrap_err();
1329 assert!(
1330 error.to_string().contains("is not allowed"),
1331 "unexpected error: {}",
1332 error
1333 );
1334 }
1335
1336 #[test]
1337 fn test_jwt_alg_mismatch_between_header_and_jwk() {
1338 let (private_key, public_key) = create_test_rsa_keys();
1339 let jwks = create_test_jwks(&public_key, "test-kid", Some("RS384"));
1341
1342 let jwt = create_jwt_token(
1343 &private_key,
1344 "test-kid",
1345 Algorithm::RS256,
1346 "https://test-issuer.com",
1347 Some("urn:risingwave:cluster:test-cluster-id"),
1348 get_future_timestamp(),
1349 HashMap::new(),
1350 );
1351
1352 let result = validate_jwt_with_jwks(
1353 &jwt,
1354 &jwks,
1355 "https://test-issuer.com",
1356 "urn:risingwave:cluster:test-cluster-id",
1357 &HashMap::new(),
1358 );
1359
1360 let error = result.unwrap_err();
1361 assert_eq!(
1362 error.to_string(),
1363 "No compatible RSA key found in JWKS for kid: 'test-kid'"
1364 );
1365 }
1366 }
1367}