mirror of
https://github.com/neondatabase/neon.git
synced 2026-03-11 12:20:38 +00:00
Compare commits
4 Commits
relkind_ca
...
remove-pos
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc8ca6aaa1 | ||
|
|
af50fd76b7 | ||
|
|
da16233f64 | ||
|
|
80466bdca2 |
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user