mirror of
https://github.com/neondatabase/neon.git
synced 2025-12-22 21:59:59 +00:00
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:
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)"
|
||||||
|
```
|
||||||
|
|||||||
3
docker-compose/compute_wrapper/private-key.pem
Normal file
3
docker-compose/compute_wrapper/private-key.pem
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MC4CAQAwBQYDK2VwBCIEIOmnRbzt2AJ0d+S3aU1hiYOl/tXpvz1FmWBfwHYBgOma
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
BIN
docker-compose/compute_wrapper/public-key.der
Normal file
BIN
docker-compose/compute_wrapper/public-key.der
Normal file
Binary file not shown.
3
docker-compose/compute_wrapper/public-key.pem
Normal file
3
docker-compose/compute_wrapper/public-key.pem
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MCowBQYDK2VwAyEADY0al/U0bgB3+9fUGk+3PKWnsck9OyxN5DjHIN6Xep0=
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
@@ -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="
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user