mirror of
https://github.com/neondatabase/neon.git
synced 2025-12-23 06:09:59 +00:00
See #7718. Fix it by renaming all `types.py` to `common_types.py`. Additionally, add an advert for using `allowed_errors.py` to test any added regex.
249 lines
9.5 KiB
Python
249 lines
9.5 KiB
Python
import os
|
|
from contextlib import closing
|
|
from pathlib import Path
|
|
|
|
import psycopg2
|
|
import pytest
|
|
from fixtures.common_types import TenantId, TimelineId
|
|
from fixtures.neon_fixtures import (
|
|
NeonEnv,
|
|
NeonEnvBuilder,
|
|
PgProtocol,
|
|
)
|
|
from fixtures.pageserver.http import PageserverApiException, PageserverHttpClient
|
|
|
|
|
|
def assert_client_authorized(env: NeonEnv, http_client: PageserverHttpClient):
|
|
http_client.timeline_create(
|
|
pg_version=env.pg_version,
|
|
tenant_id=env.initial_tenant,
|
|
new_timeline_id=TimelineId.generate(),
|
|
ancestor_timeline_id=env.initial_timeline,
|
|
)
|
|
|
|
|
|
def assert_client_not_authorized(env: NeonEnv, http_client: PageserverHttpClient):
|
|
with pytest.raises(
|
|
PageserverApiException,
|
|
match="Forbidden: JWT authentication error",
|
|
):
|
|
assert_client_authorized(env, http_client)
|
|
|
|
|
|
def test_pageserver_auth(neon_env_builder: NeonEnvBuilder):
|
|
neon_env_builder.auth_enabled = True
|
|
env = neon_env_builder.init_start()
|
|
|
|
ps = env.pageserver
|
|
|
|
tenant_token = env.auth_keys.generate_tenant_token(env.initial_tenant)
|
|
tenant_http_client = env.pageserver.http_client(tenant_token)
|
|
invalid_tenant_token = env.auth_keys.generate_tenant_token(TenantId.generate())
|
|
invalid_tenant_http_client = env.pageserver.http_client(invalid_tenant_token)
|
|
|
|
pageserver_token = env.auth_keys.generate_pageserver_token()
|
|
pageserver_http_client = env.pageserver.http_client(pageserver_token)
|
|
|
|
# this does not invoke auth check and only decodes jwt and checks it for validity
|
|
# check both tokens
|
|
ps.safe_psql("set FOO", password=tenant_token)
|
|
ps.safe_psql("set FOO", password=pageserver_token)
|
|
|
|
# tenant can create branches
|
|
assert_client_authorized(env, tenant_http_client)
|
|
|
|
# console can create branches for tenant
|
|
assert_client_authorized(env, pageserver_http_client)
|
|
|
|
# fail to create branch using token with different tenant_id
|
|
with pytest.raises(PageserverApiException, match="Forbidden: JWT authentication error"):
|
|
assert_client_authorized(env, invalid_tenant_http_client)
|
|
|
|
# create tenant using management token
|
|
env.pageserver.tenant_create(TenantId.generate(), auth_token=pageserver_token)
|
|
|
|
# fail to create tenant using tenant token
|
|
with pytest.raises(
|
|
PageserverApiException,
|
|
match="Forbidden: JWT authentication error",
|
|
):
|
|
env.pageserver.tenant_create(TenantId.generate(), auth_token=tenant_token)
|
|
|
|
|
|
def test_compute_auth_to_pageserver(neon_env_builder: NeonEnvBuilder):
|
|
neon_env_builder.auth_enabled = True
|
|
neon_env_builder.num_safekeepers = 3
|
|
env = neon_env_builder.init_start()
|
|
|
|
branch = "test_compute_auth_to_pageserver"
|
|
env.neon_cli.create_branch(branch)
|
|
endpoint = env.endpoints.create_start(branch)
|
|
|
|
with closing(endpoint.connect()) as conn:
|
|
with conn.cursor() as cur:
|
|
# we rely upon autocommit after each statement
|
|
# as waiting for acceptors happens there
|
|
cur.execute("CREATE TABLE t(key int primary key, value text)")
|
|
cur.execute("INSERT INTO t SELECT generate_series(1,100000), 'payload'")
|
|
cur.execute("SELECT sum(key) FROM t")
|
|
assert cur.fetchone() == (5000050000,)
|
|
|
|
|
|
def test_pageserver_multiple_keys(neon_env_builder: NeonEnvBuilder):
|
|
neon_env_builder.auth_enabled = True
|
|
env = neon_env_builder.init_start()
|
|
env.pageserver.allowed_errors.extend(
|
|
[".*Authentication error: InvalidSignature.*", ".*Unauthorized: malformed jwt token.*"]
|
|
)
|
|
|
|
pageserver_token_old = env.auth_keys.generate_pageserver_token()
|
|
pageserver_http_client_old = env.pageserver.http_client(pageserver_token_old)
|
|
|
|
pageserver_http_client_old.reload_auth_validation_keys()
|
|
|
|
# This test is to ensure that the pageserver supports multiple keys.
|
|
# The neon_local tool generates one key pair at a hardcoded path by default.
|
|
# As a preparation for our test, move the public key of the key pair into a
|
|
# directory at the same location as the hardcoded path by:
|
|
# 1. moving the file at `configured_pub_key_path` to a temporary location
|
|
# 2. creating a new directory at `configured_pub_key_path`
|
|
# 3. moving the file from the temporary location into the newly created directory
|
|
configured_pub_key_path = Path(env.repo_dir) / "auth_public_key.pem"
|
|
os.rename(configured_pub_key_path, Path(env.repo_dir) / "auth_public_key.pem.file")
|
|
os.mkdir(configured_pub_key_path)
|
|
os.rename(
|
|
Path(env.repo_dir) / "auth_public_key.pem.file",
|
|
configured_pub_key_path / "auth_public_key_old.pem",
|
|
)
|
|
|
|
# Add a new key pair
|
|
# This invalidates env.auth_keys and makes them be regenerated
|
|
env.regenerate_keys_at(
|
|
Path("auth_private_key.pem"), Path("auth_public_key.pem/auth_public_key_new.pem")
|
|
)
|
|
|
|
# Reload the keys on the pageserver side
|
|
pageserver_http_client_old.reload_auth_validation_keys()
|
|
|
|
# We can continue doing things using the old token
|
|
assert_client_authorized(env, pageserver_http_client_old)
|
|
|
|
pageserver_token_new = env.auth_keys.generate_pageserver_token()
|
|
pageserver_http_client_new = env.pageserver.http_client(pageserver_token_new)
|
|
|
|
# The new token also works
|
|
assert_client_authorized(env, pageserver_http_client_new)
|
|
|
|
# Remove the old token and reload
|
|
os.remove(Path(env.repo_dir) / "auth_public_key.pem" / "auth_public_key_old.pem")
|
|
pageserver_http_client_old.reload_auth_validation_keys()
|
|
|
|
# Reloading fails now with the old token, but the new token still works
|
|
assert_client_not_authorized(env, pageserver_http_client_old)
|
|
assert_client_authorized(env, pageserver_http_client_new)
|
|
|
|
|
|
def test_pageserver_key_reload(neon_env_builder: NeonEnvBuilder):
|
|
neon_env_builder.auth_enabled = True
|
|
env = neon_env_builder.init_start()
|
|
env.pageserver.allowed_errors.extend(
|
|
[".*Authentication error: InvalidSignature.*", ".*Unauthorized: malformed jwt token.*"]
|
|
)
|
|
pageserver_token_old = env.auth_keys.generate_pageserver_token()
|
|
pageserver_http_client_old = env.pageserver.http_client(pageserver_token_old)
|
|
|
|
pageserver_http_client_old.reload_auth_validation_keys()
|
|
|
|
# Regenerate the keys
|
|
env.regenerate_keys_at(Path("auth_private_key.pem"), Path("auth_public_key.pem"))
|
|
|
|
# Reload the keys on the pageserver side
|
|
pageserver_http_client_old.reload_auth_validation_keys()
|
|
|
|
# Next attempt fails as we use the old auth token
|
|
with pytest.raises(
|
|
PageserverApiException,
|
|
match="Forbidden: JWT authentication error",
|
|
):
|
|
pageserver_http_client_old.reload_auth_validation_keys()
|
|
|
|
# same goes for attempts trying to create a timeline
|
|
assert_client_not_authorized(env, pageserver_http_client_old)
|
|
|
|
pageserver_token_new = env.auth_keys.generate_pageserver_token()
|
|
pageserver_http_client_new = env.pageserver.http_client(pageserver_token_new)
|
|
|
|
# timeline creation works with the new token
|
|
assert_client_authorized(env, pageserver_http_client_new)
|
|
|
|
# reloading also works with the new token
|
|
pageserver_http_client_new.reload_auth_validation_keys()
|
|
|
|
|
|
@pytest.mark.parametrize("auth_enabled", [False, True])
|
|
def test_auth_failures(neon_env_builder: NeonEnvBuilder, auth_enabled: bool):
|
|
neon_env_builder.auth_enabled = auth_enabled
|
|
env = neon_env_builder.init_start()
|
|
|
|
branch = f"test_auth_failures_auth_enabled_{auth_enabled}"
|
|
timeline_id = env.neon_cli.create_branch(branch)
|
|
env.endpoints.create_start(branch)
|
|
|
|
tenant_token = env.auth_keys.generate_tenant_token(env.initial_tenant)
|
|
invalid_tenant_token = env.auth_keys.generate_tenant_token(TenantId.generate())
|
|
pageserver_token = env.auth_keys.generate_pageserver_token()
|
|
safekeeper_token = env.auth_keys.generate_safekeeper_token()
|
|
|
|
def check_connection(
|
|
pg_protocol: PgProtocol, command: str, expect_success: bool, **conn_kwargs
|
|
):
|
|
def op():
|
|
with closing(pg_protocol.connect(**conn_kwargs)) as conn:
|
|
with conn.cursor() as cur:
|
|
cur.execute(command)
|
|
|
|
if expect_success:
|
|
op()
|
|
else:
|
|
with pytest.raises(psycopg2.Error):
|
|
op()
|
|
|
|
def check_pageserver(expect_success: bool, **conn_kwargs):
|
|
check_connection(
|
|
env.pageserver,
|
|
f"get_last_record_rlsn {env.initial_tenant} {timeline_id}",
|
|
expect_success,
|
|
**conn_kwargs,
|
|
)
|
|
|
|
check_pageserver(not auth_enabled)
|
|
if auth_enabled:
|
|
check_pageserver(True, password=tenant_token)
|
|
|
|
env.pageserver.allowed_errors.append(".*Tenant id mismatch. Permission denied.*")
|
|
check_pageserver(False, password=invalid_tenant_token)
|
|
|
|
check_pageserver(True, password=pageserver_token)
|
|
|
|
env.pageserver.allowed_errors.append(".*JWT scope '.+' is ineligible for Pageserver auth.*")
|
|
check_pageserver(False, password=safekeeper_token)
|
|
|
|
def check_safekeeper(expect_success: bool, **conn_kwargs):
|
|
check_connection(
|
|
PgProtocol(
|
|
host="localhost",
|
|
port=env.safekeepers[0].port.pg,
|
|
options=f"ztenantid={env.initial_tenant} ztimelineid={timeline_id}",
|
|
),
|
|
"IDENTIFY_SYSTEM",
|
|
expect_success,
|
|
**conn_kwargs,
|
|
)
|
|
|
|
check_safekeeper(not auth_enabled)
|
|
if auth_enabled:
|
|
check_safekeeper(True, password=tenant_token)
|
|
check_safekeeper(False, password=invalid_tenant_token)
|
|
check_safekeeper(False, password=pageserver_token)
|
|
check_safekeeper(True, password=safekeeper_token)
|