Files
neon/test_runner/regress/test_auth.py
Joonas Koivunen d9dcbffac3 python: allow using allowed_errors.py (#7719)
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.
2024-05-13 15:16:23 +03:00

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)