Compare commits

...

4 Commits

Author SHA1 Message Date
Conrad Ludgate
dc8ca6aaa1 fix dbname 2024-10-29 07:55:14 +00:00
Conrad Ludgate
af50fd76b7 fix user 2024-10-29 07:22:07 +00:00
Conrad Ludgate
da16233f64 fixup 2024-10-28 18:41:07 +00:00
Conrad Ludgate
80466bdca2 remove postgres auth backend from proxy tests 2024-10-28 18:29:45 +00:00
2 changed files with 117 additions and 52 deletions

View File

@@ -40,6 +40,7 @@ from _pytest.fixtures import FixtureRequest
from psycopg2.extensions import connection as PgConnection from psycopg2.extensions import connection as PgConnection
from psycopg2.extensions import cursor as PgCursor from psycopg2.extensions import cursor as PgCursor
from psycopg2.extensions import make_dsn, parse_dsn from psycopg2.extensions import make_dsn, parse_dsn
from pytest_httpserver import HTTPServer
from urllib3.util.retry import Retry from urllib3.util.retry import Retry
from fixtures import overlayfs from fixtures import overlayfs
@@ -3098,10 +3099,6 @@ class NeonProxy(PgProtocol):
class AuthBackend(abc.ABC): class AuthBackend(abc.ABC):
"""All auth backends must inherit from this class""" """All auth backends must inherit from this class"""
@property
def default_conn_url(self) -> Optional[str]:
return None
@abc.abstractmethod @abc.abstractmethod
def extra_args(self) -> list[str]: def extra_args(self) -> list[str]:
pass pass
@@ -3115,7 +3112,7 @@ class NeonProxy(PgProtocol):
*["--allow-self-signed-compute", "true"], *["--allow-self-signed-compute", "true"],
] ]
class Console(AuthBackend): class ControlPlane(AuthBackend):
def __init__(self, endpoint: str, fixed_rate_limit: Optional[int] = None): def __init__(self, endpoint: str, fixed_rate_limit: Optional[int] = None):
self.endpoint = endpoint self.endpoint = endpoint
self.fixed_rate_limit = fixed_rate_limit self.fixed_rate_limit = fixed_rate_limit
@@ -3139,21 +3136,6 @@ class NeonProxy(PgProtocol):
] ]
return args return args
@dataclass(frozen=True)
class Postgres(AuthBackend):
pg_conn_url: str
@property
def default_conn_url(self) -> Optional[str]:
return self.pg_conn_url
def extra_args(self) -> list[str]:
return [
# Postgres auth backend params
*["--auth-backend", "postgres"],
*["--auth-endpoint", self.pg_conn_url],
]
def __init__( def __init__(
self, self,
neon_binpath: Path, neon_binpath: Path,
@@ -3168,7 +3150,7 @@ class NeonProxy(PgProtocol):
): ):
host = "127.0.0.1" host = "127.0.0.1"
domain = "proxy.localtest.me" # resolves to 127.0.0.1 domain = "proxy.localtest.me" # resolves to 127.0.0.1
super().__init__(dsn=auth_backend.default_conn_url, host=domain, port=proxy_port) super().__init__(host=domain, port=proxy_port)
self.domain = domain self.domain = domain
self.host = host self.host = host
@@ -3422,20 +3404,39 @@ def static_proxy(
port_distributor: PortDistributor, port_distributor: PortDistributor,
neon_binpath: Path, neon_binpath: Path,
test_output_dir: Path, test_output_dir: Path,
httpserver: HTTPServer,
) -> Iterator[NeonProxy]: ) -> Iterator[NeonProxy]:
"""Neon proxy that routes directly to vanilla postgres.""" """Neon proxy that routes directly to vanilla postgres and a mocked cplane HTTP API."""
port = vanilla_pg.default_options["port"] port = vanilla_pg.default_options["port"]
host = vanilla_pg.default_options["host"] host = vanilla_pg.default_options["host"]
dbname = vanilla_pg.default_options["dbname"] dbname = vanilla_pg.default_options["dbname"]
auth_endpoint = f"postgres://proxy:password@{host}:{port}/{dbname}"
# For simplicity, we use the same user for both `--auth-endpoint` and `safe_psql`
vanilla_pg.start() vanilla_pg.start()
vanilla_pg.safe_psql("create user proxy with login superuser password 'password'") vanilla_pg.safe_psql("create user proxy with login superuser password 'password'")
vanilla_pg.safe_psql("CREATE SCHEMA IF NOT EXISTS neon_control_plane") [(rolpassword,)] = vanilla_pg.safe_psql(
vanilla_pg.safe_psql( "select rolpassword from pg_catalog.pg_authid where rolname = 'proxy'"
"CREATE TABLE neon_control_plane.endpoints (endpoint_id VARCHAR(255) PRIMARY KEY, allowed_ips VARCHAR(255))" )
# return local postgres addr on ProxyWakeCompute.
httpserver.expect_request("/cplane/proxy_wake_compute").respond_with_json(
{
"address": f"{host}:{port}",
"aux": {
"endpoint_id": "ep-foo-bar-1234",
"branch_id": "br-foo-bar",
"project_id": "foo-bar",
},
}
)
# return local postgres addr on ProxyWakeCompute.
httpserver.expect_request("/cplane/proxy_get_role_secret").respond_with_json(
{
"role_secret": rolpassword,
"allowed_ips": None,
"project_id": "foo-bar",
}
) )
proxy_port = port_distributor.get_port() proxy_port = port_distributor.get_port()
@@ -3450,8 +3451,12 @@ def static_proxy(
http_port=http_port, http_port=http_port,
mgmt_port=mgmt_port, mgmt_port=mgmt_port,
external_http_port=external_http_port, external_http_port=external_http_port,
auth_backend=NeonProxy.Postgres(auth_endpoint), auth_backend=NeonProxy.ControlPlane(httpserver.url_for("/cplane")),
) as proxy: ) as proxy:
proxy.default_options["user"] = "proxy"
proxy.default_options["password"] = "password"
proxy.default_options["dbname"] = dbname
proxy.start() proxy.start()
yield proxy yield proxy

View File

@@ -6,20 +6,27 @@ from fixtures.neon_fixtures import (
NeonProxy, NeonProxy,
VanillaPostgres, VanillaPostgres,
) )
from pytest_httpserver import HTTPServer
TABLE_NAME = "neon_control_plane.endpoints" TABLE_NAME = "neon_control_plane.endpoints"
# Proxy uses the same logic for psql and websockets. def test_proxy_psql_not_allowed_ips(
@pytest.mark.asyncio static_proxy: NeonProxy,
async def test_proxy_psql_allowed_ips(static_proxy: NeonProxy, vanilla_pg: VanillaPostgres): vanilla_pg: VanillaPostgres,
# Shouldn't be able to connect to this project httpserver: HTTPServer,
vanilla_pg.safe_psql( ):
f"INSERT INTO {TABLE_NAME} (endpoint_id, allowed_ips) VALUES ('private-project', '8.8.8.8')" [(rolpassword,)] = vanilla_pg.safe_psql(
"select rolpassword from pg_catalog.pg_authid where rolname = 'proxy'"
) )
# Should be able to connect to this project
vanilla_pg.safe_psql( # Shouldn't be able to connect to this project
f"INSERT INTO {TABLE_NAME} (endpoint_id, allowed_ips) VALUES ('generic-project', '::1,127.0.0.1')" httpserver.expect_request("/cplane/proxy_get_role_secret").respond_with_json(
{
"role_secret": rolpassword,
"allowed_ips": ["8.8.8.8"],
"project_id": "foo-bar",
}
) )
def check_cannot_connect(**kwargs): def check_cannot_connect(**kwargs):
@@ -37,6 +44,25 @@ async def test_proxy_psql_allowed_ips(static_proxy: NeonProxy, vanilla_pg: Vanil
# with SNI # with SNI
check_cannot_connect(query="select 1", host="private-project.localtest.me") check_cannot_connect(query="select 1", host="private-project.localtest.me")
def test_proxy_psql_allowed_ips(
static_proxy: NeonProxy,
vanilla_pg: VanillaPostgres,
httpserver: HTTPServer,
):
[(rolpassword,)] = vanilla_pg.safe_psql(
"select rolpassword from pg_catalog.pg_authid where rolname = 'proxy'"
)
# Should be able to connect to this project
httpserver.expect_request("/cplane/proxy_get_role_secret").respond_with_json(
{
"role_secret": rolpassword,
"allowed_ips": ["::1", "127.0.0.1"],
"project_id": "foo-bar",
}
)
# no SNI, deprecated `options=project` syntax (before we had several endpoint in project) # no SNI, deprecated `options=project` syntax (before we had several endpoint in project)
out = static_proxy.safe_psql(query="select 1", sslsni=0, options="project=generic-project") out = static_proxy.safe_psql(query="select 1", sslsni=0, options="project=generic-project")
assert out[0][0] == 1 assert out[0][0] == 1
@@ -50,27 +76,61 @@ async def test_proxy_psql_allowed_ips(static_proxy: NeonProxy, vanilla_pg: Vanil
assert out[0][0] == 1 assert out[0][0] == 1
@pytest.mark.asyncio def test_proxy_http_not_allowed_ips(
async def test_proxy_http_allowed_ips(static_proxy: NeonProxy, vanilla_pg: VanillaPostgres): static_proxy: NeonProxy,
static_proxy.safe_psql("create user http_auth with password 'http' superuser") vanilla_pg: VanillaPostgres,
httpserver: HTTPServer,
):
vanilla_pg.safe_psql("create user http_auth with password 'http' superuser")
# Shouldn't be able to connect to this project [(rolpassword,)] = vanilla_pg.safe_psql(
vanilla_pg.safe_psql( "select rolpassword from pg_catalog.pg_authid where rolname = 'http_auth'"
f"INSERT INTO {TABLE_NAME} (endpoint_id, allowed_ips) VALUES ('proxy', '8.8.8.8')"
) )
def query(status: int, query: str, *args): httpserver.expect_oneshot_request("/cplane/proxy_get_role_secret").respond_with_json(
{
"role_secret": rolpassword,
"allowed_ips": ["8.8.8.8"],
"project_id": "foo-bar",
}
)
with httpserver.wait() as waiting:
static_proxy.http_query( static_proxy.http_query(
query, "select 1;",
args, [],
user="http_auth", user="http_auth",
password="http", password="http",
expected_code=status, expected_code=400,
) )
assert waiting.result
query(400, "select 1;") # ip address is not allowed
# Should be able to connect to this project def test_proxy_http_allowed_ips(
vanilla_pg.safe_psql( static_proxy: NeonProxy,
f"UPDATE {TABLE_NAME} SET allowed_ips = '8.8.8.8,127.0.0.1' WHERE endpoint_id = 'proxy'" vanilla_pg: VanillaPostgres,
httpserver: HTTPServer,
):
vanilla_pg.safe_psql("create user http_auth with password 'http' superuser")
[(rolpassword,)] = vanilla_pg.safe_psql(
"select rolpassword from pg_catalog.pg_authid where rolname = 'http_auth'"
) )
query(200, "select 1;") # should work now
httpserver.expect_oneshot_request("/cplane/proxy_get_role_secret").respond_with_json(
{
"role_secret": rolpassword,
"allowed_ips": ["8.8.8.8", "127.0.0.1"],
"project_id": "foo-bar",
}
)
with httpserver.wait() as waiting:
static_proxy.http_query(
"select 1;",
[],
user="http_auth",
password="http",
expected_code=200,
)
assert waiting.result