postjevsql.git / tests / api_key.rs
1//! The API key comes from exactly one place: `TYPESAFE_API_KEY` in the
2//! server's environment, or the superuser-only `jev.api_key_file`. Both
3//! set is an error, not a precedence rule that silently picks one.
4
5use support::mock_jev::{MockJev, Reply};
6use support::{jev_instance_env, noul};
7
8const SELECT: &str = "SELECT jev_prob(t, 'Urgent?') FROM tickets t";
9
10fn key_file(contents: &str) -> tempfile::NamedTempFile {
11    let file = tempfile::NamedTempFile::new().unwrap();
12    std::fs::write(file.path(), contents).unwrap();
13    file
14}
15
16#[tokio::test(flavor = "multi_thread")]
17async fn the_key_file_is_used() {
18    let mock = MockJev::start(|_| Reply::json(200, noul(0.95))).await;
19    let file = key_file("file-key\n");
20    let path = file.path().to_str().unwrap();
21    let (_pg, client) = jev_instance_env(&mock, &[("jev.api_key_file", path)], &[]).await;
22
23    client.query_one(SELECT, &[]).await.expect("answered");
24    assert_eq!(mock.requests()[0].headers["authorization"], "Bearer file-key");
25}
26
27#[tokio::test(flavor = "multi_thread")]
28async fn two_sources_is_an_error() {
29    let mock = MockJev::start(|_| Reply::json(200, noul(0.95))).await;
30    let file = key_file("file-key");
31    let path = file.path().to_str().unwrap();
32    let (_pg, client) =
33        jev_instance_env(&mock, &[("jev.api_key_file", path)], &[("TYPESAFE_API_KEY", "env-key")]).await;
34
35    let err = client.query_one(SELECT, &[]).await.expect_err("ambiguous");
36    let message = err.as_db_error().unwrap().message().to_string();
37    assert!(message.contains("TYPESAFE_API_KEY") && message.contains("jev.api_key_file"), "{message}");
38    assert!(!message.contains("file-key") && !message.contains("env-key"), "the key never appears: {message}");
39    assert!(mock.requests().is_empty());
40}
41
42#[tokio::test(flavor = "multi_thread")]
43async fn no_source_is_an_error() {
44    let mock = MockJev::start(|_| Reply::json(200, noul(0.95))).await;
45    let (_pg, client) = jev_instance_env(&mock, &[], &[]).await;
46
47    let err = client.query_one(SELECT, &[]).await.expect_err("no key");
48    assert_eq!(err.as_db_error().unwrap().code(), &tokio_postgres::error::SqlState::INVALID_AUTHORIZATION_SPECIFICATION);
49    let message = err.as_db_error().unwrap().message().to_string();
50    assert!(message.contains("TYPESAFE_API_KEY") && message.contains("jev.api_key_file"), "{message}");
51}
52
53#[tokio::test(flavor = "multi_thread")]
54async fn only_a_superuser_sets_the_key_file() {
55    let mock = MockJev::start(|_| Reply::json(200, noul(0.95))).await;
56    let (_pg, client) = jev_instance_env(&mock, &[], &[("TYPESAFE_API_KEY", "k")]).await;
57    client
58        .batch_execute("CREATE ROLE app LOGIN; SET ROLE app;")
59        .await
60        .unwrap();
61    let err = client.batch_execute("SET jev.api_key_file = '/etc/passwd'").await.expect_err("refused");
62    assert_eq!(err.as_db_error().unwrap().code(), &tokio_postgres::error::SqlState::INSUFFICIENT_PRIVILEGE);
63}