Remove compute_ctl authorization bypass if testing feature was enable (#11596)

We want to exercise the authorization middleware in our regression
tests.

Signed-off-by: Tristan Partin <tristan@neon.tech>
This commit is contained in:
Tristan Partin
2025-04-16 12:54:51 -05:00
committed by GitHub
parent 4af0b9b387
commit c002236145
11 changed files with 80 additions and 23 deletions

View File

@@ -6,4 +6,5 @@ pub(crate) mod request_id;
pub(crate) use json::Json; pub(crate) use json::Json;
pub(crate) use path::Path; pub(crate) use path::Path;
pub(crate) use query::Query; pub(crate) use query::Query;
#[allow(unused)]
pub(crate) use request_id::RequestId; pub(crate) use request_id::RequestId;

View File

@@ -13,7 +13,7 @@ use jsonwebtoken::{Algorithm, DecodingKey, TokenData, Validation, jwk::JwkSet};
use tower_http::auth::AsyncAuthorizeRequest; use tower_http::auth::AsyncAuthorizeRequest;
use tracing::{debug, warn}; use tracing::{debug, warn};
use crate::http::{JsonResponse, extract::RequestId}; use crate::http::JsonResponse;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(in crate::http) struct Authorize { pub(in crate::http) struct Authorize {
@@ -52,18 +52,6 @@ impl AsyncAuthorizeRequest<Body> for Authorize {
let validation = self.validation.clone(); let validation = self.validation.clone();
Box::pin(async move { Box::pin(async move {
let request_id = request.extract_parts::<RequestId>().await.unwrap();
// 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") {
warn!(%request_id, "Skipping compute_ctl authorization check");
return Ok(request);
}
let TypedHeader(Authorization(bearer)) = request let TypedHeader(Authorization(bearer)) = request
.extract_parts::<TypedHeader<Authorization<Bearer>>>() .extract_parts::<TypedHeader<Authorization<Bearer>>>()
.await .await

View File

@@ -1544,7 +1544,7 @@ async fn handle_endpoint(subcmd: &EndpointCmd, env: &local_env::LocalEnv) -> Res
.with_context(|| format!("postgres endpoint {endpoint_id} is not found"))?; .with_context(|| format!("postgres endpoint {endpoint_id} is not found"))?;
let jwt = endpoint.generate_jwt()?; let jwt = endpoint.generate_jwt()?;
println!("{jwt}"); print!("{jwt}");
} }
} }

View File

@@ -1,4 +1,3 @@
# Example docker compose configuration # Example docker compose configuration
The configuration in this directory is used for testing Neon docker images: it is The configuration in this directory is used for testing Neon docker images: it is
@@ -8,3 +7,13 @@ you can experiment with a miniature Neon system, use `cargo neon` rather than co
This configuration does not start the storage controller, because the controller This configuration does not start the storage controller, because the controller
needs a way to reconfigure running computes, and no such thing exists in this setup. needs a way to reconfigure running computes, and no such thing exists in this setup.
## Generating the JWKS for a compute
```shell
openssl genpkey -algorithm Ed25519 -out private-key.pem
openssl pkey -in private-key.pem -pubout -out public-key.pem
openssl pkey -pubin -inform pem -in public-key.pem -pubout -outform der -out public-key.der
key="$(xxd -plain -cols 32 -s -32 public-key.der)"
key_id="$(printf '%s' "$key" | sha256sum | awk '{ print $1 }' | basenc --base64url --wrap=0)"
x="$(printf '%s' "$key" | basenc --base64url --wrap=0)"
```

View File

@@ -0,0 +1,3 @@
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIOmnRbzt2AJ0d+S3aU1hiYOl/tXpvz1FmWBfwHYBgOma
-----END PRIVATE KEY-----

Binary file not shown.

View File

@@ -0,0 +1,3 @@
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEADY0al/U0bgB3+9fUGk+3PKWnsck9OyxN5DjHIN6Xep0=
-----END PUBLIC KEY-----

View File

@@ -142,7 +142,19 @@
}, },
"compute_ctl_config": { "compute_ctl_config": {
"jwks": { "jwks": {
"keys": [] "keys": [
{
"use": "sig",
"key_ops": [
"verify"
],
"alg": "EdDSA",
"kid": "ZGIxMzAzOGY0YWQwODk2ODU1MTk1NzMxMDFkYmUyOWU2NzZkOWNjNjMyMGRkZGJjOWY0MjdjYWVmNzE1MjUyOAo=",
"kty": "OKP",
"crv": "Ed25519",
"x": "MGQ4ZDFhOTdmNTM0NmUwMDc3ZmJkN2Q0MWE0ZmI3M2NhNWE3YjFjOTNkM2IyYzRkZTQzOGM3MjBkZTk3N2E5ZAo="
}
]
} }
} }
} }

View File

@@ -1,33 +1,59 @@
from __future__ import annotations from __future__ import annotations
import urllib.parse import urllib.parse
from typing import TYPE_CHECKING, final
import requests import requests
from requests.adapters import HTTPAdapter from requests.adapters import HTTPAdapter
from requests.auth import AuthBase
from typing_extensions import override
from fixtures.log_helper import log from fixtures.log_helper import log
if TYPE_CHECKING:
from requests import PreparedRequest
@final
class BearerAuth(AuthBase):
"""
Auth implementation for bearer authorization in HTTP requests through the
requests HTTP client library.
"""
def __init__(self, jwt: str):
self.__jwt = jwt
@override
def __call__(self, request: PreparedRequest) -> PreparedRequest:
request.headers["Authorization"] = "Bearer " + self.__jwt
return request
@final
class EndpointHttpClient(requests.Session): class EndpointHttpClient(requests.Session):
def __init__( def __init__(
self, self,
external_port: int, external_port: int,
internal_port: int, internal_port: int,
jwt: str,
): ):
super().__init__() super().__init__()
self.external_port: int = external_port self.external_port: int = external_port
self.internal_port: int = internal_port self.internal_port: int = internal_port
self.auth = BearerAuth(jwt)
self.mount("http://", HTTPAdapter()) self.mount("http://", HTTPAdapter())
def dbs_and_roles(self): def dbs_and_roles(self):
res = self.get(f"http://localhost:{self.external_port}/dbs_and_roles") res = self.get(f"http://localhost:{self.external_port}/dbs_and_roles", auth=self.auth)
res.raise_for_status() res.raise_for_status()
return res.json() return res.json()
def database_schema(self, database: str): def database_schema(self, database: str):
res = self.get( res = self.get(
f"http://localhost:{self.external_port}/database_schema?database={urllib.parse.quote(database, safe='')}" f"http://localhost:{self.external_port}/database_schema?database={urllib.parse.quote(database, safe='')}",
auth=self.auth,
) )
res.raise_for_status() res.raise_for_status()
return res.text return res.text
@@ -58,13 +84,13 @@ class EndpointHttpClient(requests.Session):
# Current compute status. # Current compute status.
def status(self): def status(self):
res = self.get(f"http://localhost:{self.external_port}/status") res = self.get(f"http://localhost:{self.external_port}/status", auth=self.auth)
res.raise_for_status() res.raise_for_status()
return res.json() return res.json()
# Compute startup-related metrics. # Compute startup-related metrics.
def metrics_json(self): def metrics_json(self):
res = self.get(f"http://localhost:{self.external_port}/metrics.json") res = self.get(f"http://localhost:{self.external_port}/metrics.json", auth=self.auth)
res.raise_for_status() res.raise_for_status()
return res.json() return res.json()

View File

@@ -535,6 +535,18 @@ class NeonLocalCli(AbstractNeonCli):
res.check_returncode() res.check_returncode()
return res return res
def endpoint_generate_jwt(self, endpoint_id: str) -> str:
"""
Generate a JWT for making requests to the endpoint's external HTTP
server.
"""
args = ["endpoint", "generate-jwt", endpoint_id]
cmd = self.raw_cli(args)
cmd.check_returncode()
return cmd.stdout
def endpoint_start( def endpoint_start(
self, self,
endpoint_id: str, endpoint_id: str,

View File

@@ -4110,13 +4110,14 @@ class Endpoint(PgProtocol, LogUtils):
# try and stop the same process twice, as stop() is called by test teardown and # try and stop the same process twice, as stop() is called by test teardown and
# potentially by some __del__ chains in other threads. # potentially by some __del__ chains in other threads.
self._running = threading.Semaphore(0) self._running = threading.Semaphore(0)
self.__jwt: str | None = None
def http_client( def http_client(self, retries: Retry | None = None) -> EndpointHttpClient:
self, auth_token: str | None = None, retries: Retry | None = None assert self.__jwt is not None
) -> EndpointHttpClient:
return EndpointHttpClient( return EndpointHttpClient(
external_port=self.external_http_port, external_port=self.external_http_port,
internal_port=self.internal_http_port, internal_port=self.internal_http_port,
jwt=self.__jwt,
) )
def create( def create(
@@ -4200,6 +4201,8 @@ class Endpoint(PgProtocol, LogUtils):
self.config(config_lines) self.config(config_lines)
self.__jwt = self.env.neon_cli.endpoint_generate_jwt(self.endpoint_id)
return self return self
def start( def start(