Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bumpversion.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tool.bumpversion]
current_version = "1.18.3+260822"
current_version = "1.19.0+260822"
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)\\+(?P<build>\\d+)"
serialize = ["{major}.{minor}.{patch}+{build}"]
search = "{current_version}"
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ FROM devel_build as devel_shell
# Copy everything to the image
COPY . /app
WORKDIR /app
RUN apt-get update && apt-get install -y zsh jq \
RUN apt-get update && apt-get install -y zsh jq vim \
&& sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" \
&& echo "source /root/.profile" >>/root/.zshrc \
&& pip3 install git-up \
Expand Down
10 changes: 10 additions & 0 deletions Dockerfile_pycharm
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
###
# Separate file Since PyCharms builder doesn't seem to take --target argument into account
# You need to have build the devel_shell target first, see README.rst
#
# Set python exectuable path to /.venv/bin/python
###
FROM rasenmaeher_api:devel_shell as pycharm
RUN source /root/.profile \
&& uv sync \
&& true
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ Creating a development container
Build image, create container and start it::

docker build --ssh default --target devel_shell -t rasenmaeher_api:devel_shell .
docker create --name rasenmaeher_api_devel -v `pwd`":/app" -it `echo $DOCKER_SSHAGENT` rasenmaeher_api:devel_shell
docker create --name rasenmaeher_api_devel -v `pwd`":/app" -it `echo $DOCKER_SSHAGENT` -v /var/run/docker.sock:/var/run/docker.sock rasenmaeher_api:devel_shell
docker start -i rasenmaeher_api_devel

pre-commit considerations
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "rasenmaeher_api"
version = "1.18.3+260822"
version = "1.19.0+260822"
description = "python-rasenmaeher-api"
authors = [
{ name = "Aciid", email = "703382+Aciid@users.noreply.github.com" },
Expand Down
2 changes: 1 addition & 1 deletion src/rasenmaeher_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""python-rasenmaeher-api"""

__version__ = "1.18.3+260822" # NOTE Use `bump-my-version` to bump versions correctly
__version__ = "1.19.0+260822" # NOTE Use `bump-my-version` to bump versions correctly
4 changes: 3 additions & 1 deletion src/rasenmaeher_api/db/people.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ async def create_with_cert(
if csrpem:
newperson.csrfile.write_text(csrpem, encoding="utf-8")
else:
ckp = await async_create_keypair(newperson.privkeyfile, newperson.pubkeyfile)
ckp = await async_create_keypair(
newperson.privkeyfile, newperson.pubkeyfile, ktype=cnf.cert_key_type
)
csrpem = await async_create_client_csr(ckp, newperson.csrfile, newperson.certsubject)
certpem = (await sign_csr(csrpem)).replace("\\n", "\n")
newperson.certfile.write_text(certpem)
Expand Down
2 changes: 1 addition & 1 deletion src/rasenmaeher_api/mtlsinit.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ async def mtls_init() -> None:
# Check the privkey again to avoid overwriting.
if not privkeypath.exists():
LOGGER.info("No mTLS client cert yet, creating it, this will take a moment")
keypair = await async_create_keypair(privkeypath, pubkeypath)
keypair = await async_create_keypair(privkeypath, pubkeypath, ktype=config.cert_key_type)
LOGGER.debug("Creating mTLS client CSR")
csrpem = await async_create_client_csr(keypair, csrpath, {"CN": config.mtls_client_cert_cn})
if not certpath.exists():
Expand Down
1 change: 1 addition & 0 deletions src/rasenmaeher_api/rmsettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def log_level_int(self) -> int:
ocsprest_host: str = "http://127.0.0.1"
ocsprest_port: str = "8887"
cfssl_timeout: float = 2.5
cert_key_type: str = "RSA"

# Cert-Manager configuration (used when cert_backend == CERT_MANAGER)
cert_manager_namespace: str = "opendefence-system"
Expand Down
26 changes: 14 additions & 12 deletions src/rasenmaeher_api/testhelpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Helpers for testing, unit and manual"""

import logging
import secrets

from .db import Enrollment, LoginCode, Person

Expand All @@ -13,6 +14,7 @@ async def create_test_users() -> tuple[list[str], list[str]]:
"""
work_ids: list[str] = []
jwt_tokens: list[str] = []
suffix = f"_{secrets.token_hex(4)}"

# Create "anon_admin", this is also done in /firstuser/add-admin if one not exists yet
# anon_admin is only used to "approve" the newly created admin users. Aka user for "anon_admin_session".
Expand All @@ -25,35 +27,35 @@ async def create_test_users() -> tuple[list[str], list[str]]:

# CREATE USER secondadmin
# approved, admin role, approved by anon_admin
_enrollment_pyteststuff = await Enrollment.create_for_callsign(callsign="pyteststuff", pool=None, extra={})
_enrollment_pyteststuff = await Enrollment.create_for_callsign(callsign=f"pyteststuff{suffix}", pool=None, extra={})
_user_pyteststuff = await _enrollment_pyteststuff.approve(approver=_anon_admin_user)
_ = await _user_pyteststuff.assign_role(role="admin")
_jwt_pyteststuff = await LoginCode.create_for_claims(claims={"sub": "pyteststuff"})
work_ids.append("pyteststuff")
_jwt_pyteststuff = await LoginCode.create_for_claims(claims={"sub": f"pyteststuff{suffix}"})
work_ids.append(f"pyteststuff{suffix}")
jwt_tokens.append(_jwt_pyteststuff)

# CREATE USER secondadmin
# approved, admin role, approved by pyteststuff
_enrollment_secondadmin = await Enrollment.create_for_callsign(callsign="secondadmin", pool=None, extra={})
_enrollment_secondadmin = await Enrollment.create_for_callsign(callsign=f"secondadmin{suffix}", pool=None, extra={})
_user_secondadmin = await _enrollment_secondadmin.approve(approver=_user_pyteststuff)
_ = await _user_secondadmin.assign_role(role="admin")
_jwt_secondadmin = await LoginCode.create_for_claims(claims={"sub": "secondadmin"})
work_ids.append("secondadmin")
_jwt_secondadmin = await LoginCode.create_for_claims(claims={"sub": f"secondadmin{suffix}"})
work_ids.append(f"secondadmin{suffix}")
jwt_tokens.append(_jwt_secondadmin)

# CREATE USER kissa
# approved, approved by secondadmin
_enrollment_kissa = await Enrollment.create_for_callsign(callsign="kissa", pool=None, extra={})
_enrollment_kissa = await Enrollment.create_for_callsign(callsign=f"kissa{suffix}", pool=None, extra={})
_ = await _enrollment_kissa.approve(approver=_user_secondadmin)
_jwt_kissa = await LoginCode.create_for_claims(claims={"sub": "kissa"})
work_ids.append("kissa")
_jwt_kissa = await LoginCode.create_for_claims(claims={"sub": f"kissa{suffix}"})
work_ids.append(f"kissa{suffix}")
jwt_tokens.append(_jwt_kissa)

# CREATE USER koira
# not approved
_ = await Enrollment.create_for_callsign(callsign="koira", pool=None, extra={})
_jwt_koira = await LoginCode.create_for_claims(claims={"sub": "koira"})
work_ids.append("koira")
_ = await Enrollment.create_for_callsign(callsign=f"koira{suffix}", pool=None, extra={})
_jwt_koira = await LoginCode.create_for_claims(claims={"sub": f"koira{suffix}"})
work_ids.append(f"koira{suffix}")
jwt_tokens.append(_jwt_koira)

# TODO CREATE POOL FOR secondadmin
Expand Down
12 changes: 6 additions & 6 deletions tests/cert/cert_manager/ocsp/test_responder.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,11 @@ async def test_good_user(
make_leaf: LeafFactory,
make_request: RequestFactory,
respond: Responder,
ginosession: None,
dbinit_func,
nice_tmpdir: str,
) -> None:
"""Active user -> GOOD, signature verifies, both hash algos work"""
_ = ginosession
_ = dbinit_func
leaf = make_leaf("GOODUSER")
add_person(leaf.serial_number, tmp=nice_tmpdir)
for algo in (hashes.SHA1(), hashes.SHA256()):
Expand All @@ -77,11 +77,11 @@ async def test_revoked_user(
make_leaf: LeafFactory,
make_request: RequestFactory,
respond: Responder,
ginosession: None,
dbinit_func,
nice_tmpdir: str,
) -> None:
"""Deleted user -> REVOKED with reason and time"""
_ = ginosession, installed_signer
_ = dbinit_func, installed_signer
leaf = make_leaf("REVOKEDUSER")
deleted = datetime.now(UTC)
add_person(leaf.serial_number, deleted=deleted, revoke_reason="key_compromise", tmp=nice_tmpdir)
Expand All @@ -99,10 +99,10 @@ async def test_nonce_echo(
make_leaf: LeafFactory,
make_request: RequestFactory,
respond: Responder,
ginosession: None,
dbinit_func,
) -> None:
"""Nonce comes back byte-for-byte; oversize nonce is malformed"""
_ = ginosession, installed_signer
_ = dbinit_func, installed_signer
leaf = make_leaf("NONCELEAF")
nonce = os.urandom(16)
resp = await respond(make_request(leaf, nonce=nonce))
Expand Down
24 changes: 12 additions & 12 deletions tests/cert/cert_manager/ocsp/test_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ async def test_product_cert_valid_cn(
make_leaf: LeafFactory,
make_request: RequestFactory,
respond: Responder,
ginosession: None,
dbinit_func,
) -> None:
"""Recorded product certs with manifest CNs (and rmapi's own CN) -> GOOD"""
_ = ginosession, installed_signer
_ = dbinit_func, installed_signer
for common_name in (VALID_PRODUCT_CN, switchme_to_singleton_call.mtls_client_cert_cn):
leaf = make_leaf(common_name)
add_issued(leaf.serial_number, common_name)
Expand All @@ -45,11 +45,11 @@ async def test_issued_cert_active_user_cn(
make_leaf: LeafFactory,
make_request: RequestFactory,
respond: Responder,
ginosession: None,
dbinit_func,
nice_tmpdir: str,
) -> None:
"""Per-user cert signed via product path (e.g. TAK) with an active owner -> GOOD"""
_ = ginosession, installed_signer
_ = dbinit_func, installed_signer
callsign = f"atakuser_{uuid.uuid4()}"
add_person(tmp=nice_tmpdir, callsign=callsign) # active owner, different (main) cert serial
leaf = make_leaf(callsign)
Expand All @@ -65,11 +65,11 @@ async def test_issued_cert_revoked_user_cn(
make_leaf: LeafFactory,
make_request: RequestFactory,
respond: Responder,
ginosession: None,
dbinit_func,
nice_tmpdir: str,
) -> None:
"""Per-user cert signed via product path whose owner is revoked -> REVOKED with owner's reason"""
_ = ginosession, installed_signer
_ = dbinit_func, installed_signer
callsign = f"atakuser_{uuid.uuid4()}"
add_person(tmp=nice_tmpdir, callsign=callsign, deleted=datetime.now(UTC), revoke_reason="privilege_withdrawn")
leaf = make_leaf(callsign)
Expand All @@ -85,10 +85,10 @@ async def test_issued_cert_no_owner_cn(
make_leaf: LeafFactory,
make_request: RequestFactory,
respond: Responder,
ginosession: None,
dbinit_func,
) -> None:
"""Recorded cert whose CN matches no Person and no product -> GOOD (we issued it)"""
_ = ginosession, installed_signer
_ = dbinit_func, installed_signer
leaf = make_leaf("orphan.example.invalid")
add_issued(leaf.serial_number, "orphan.example.invalid")
resp = await respond(make_request(leaf))
Expand All @@ -101,24 +101,24 @@ async def test_unknown_serial(
make_leaf: LeafFactory,
make_request: RequestFactory,
respond: Responder,
ginosession: None,
dbinit_func,
) -> None:
"""Serial in neither table -> UNKNOWN"""
_ = ginosession, installed_signer
_ = dbinit_func, installed_signer
leaf = make_leaf("NEVERRECORDED")
resp = await respond(make_request(leaf))
assert resp.response_status == ocsp.OCSPResponseStatus.SUCCESSFUL
assert resp.certificate_status == ocsp.OCSPCertStatus.UNKNOWN


@pytest.mark.asyncio(loop_scope="session")
async def test_product_cn_lookup_failure_falls_back(monkeypatch: pytest.MonkeyPatch, ginosession: None) -> None:
async def test_product_cn_lookup_failure_falls_back(monkeypatch: pytest.MonkeyPatch, dbinit_func) -> None:
"""A broken manifest lookup degrades to an empty product list instead of raising.

valid_product_cns reads the kraftwerk manifest, which is absent in some deployments.
Without the fallback every OCSP query for a recorded cert would become an internal error.
"""
_ = ginosession
_ = dbinit_func
serial = x509.random_serial_number()
with EngineWrapper.get_session() as session:
session.add(IssuedCert(serial=str(serial), cn="orphan.example.invalid"))
Expand Down
8 changes: 4 additions & 4 deletions tests/cert/cert_manager/ocsp/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ async def test_http_post_and_get(
installed_signer: SigningCA,
make_leaf: LeafFactory,
make_request: RequestFactory,
ginosession: None,
dbinit_func,
ocsp_client: TestClient,
) -> None:
"""POST body and GET base64-path forms return equivalent responses with cache headers"""
_ = ginosession, installed_signer
_ = dbinit_func, installed_signer
leaf = make_leaf("HTTPLEAF")
reqder = make_request(leaf)

Expand All @@ -57,11 +57,11 @@ async def test_http_nonce_no_store(
installed_signer: SigningCA,
make_leaf: LeafFactory,
make_request: RequestFactory,
ginosession: None,
dbinit_func,
ocsp_client: TestClient,
) -> None:
"""Nonced responses must not be cached"""
_ = ginosession, installed_signer
_ = dbinit_func, installed_signer
leaf = make_leaf("NONCEHTTP")
reqder = make_request(leaf, nonce=os.urandom(8))
resp = await ocsp_client.post("/api/v1/ocsp", data=reqder)
Expand Down
31 changes: 21 additions & 10 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,15 @@ async def tms_wait() -> None:
return


# FIXME rename, we do not use Gino
@pytest_asyncio.fixture(scope="function")
async def ginosession() -> AsyncGenerator[None, None]:
async def dbinit_func() -> AsyncGenerator[None, None]:
"""make sure db is bound etc"""
await init_db()
yield


@pytest_asyncio.fixture(scope="session")
async def dbinit_sess() -> AsyncGenerator[None, None]:
"""make sure db is bound etc"""
await init_db()
yield
Expand All @@ -69,15 +75,20 @@ async def taskmaster() -> AsyncGenerator[None, None]:
LOGGER.warning("Taskmaster wait timed out")


@pytest.fixture(scope="session")
def app_instance(session_env_config: None) -> FastAPI:
@pytest.fixture(scope="session", params=["RSA", "ED25519"])
def app_instance(
session_env_config: None, monkeysession: pytest.MonkeyPatch, request: pytest.FixtureRequest
) -> FastAPI:
"""Singleton app instance"""
_ = session_env_config
# We need to import this *after* messing with env or manifest based routes break
from rasenmaeher_api.web.application import get_app
with monkeysession.context() as mpatch:
mpatch.setenv("RM_CERT_KEY_TYPE", str(request.param))
mpatch.setattr(switchme_to_singleton_call, "cert_key_type", str(request.param))
# We need to import this *after* messing with env or manifest based routes break
from rasenmaeher_api.web.application import get_app

app = get_app()
return app
app = get_app()
return app


@pytest.fixture(scope="session", autouse=True)
Expand Down Expand Up @@ -354,13 +365,13 @@ async def tilauspalvelu_jwt_user_koira_client(


@pytest_asyncio.fixture(scope="session")
async def test_user_secrets(session_env_config: None) -> tuple[list[str], list[str]]:
async def test_user_secrets(app_instance: FastAPI) -> tuple[list[str], list[str]]:
"""Create a few test users and work ids returns
list of work-ids and their corresponding "hashes"

First one has "enrollment" special role
"""
_ = session_env_config
_ = app_instance
return await create_test_users()


Expand Down
16 changes: 3 additions & 13 deletions tests/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,7 @@ x-cfssl_env: &cfssl_env

services:
cfssl:
#TODO: Change back to main
image: ghcr.io/pvarki/cfssl-api:1.0.7-260427-pr24-20260506t1433
build:
context: https://github.com/pvarki/docker-rasenmaeher-cfssl.git#ba2ae8454c9b33d20b46fd4fc39dcb66b3afd2c0
dockerfile: Dockerfile
target: api
image: ${PVARKI_DOCKER_REPO:-ghcr.io/}pvarki/cfssl-api:1.0.7-260612
environment:
<<: *cfssl_env
ports:
Expand All @@ -45,12 +40,7 @@ services:
start_period: 5s

ocsprest:
#TODO: Change back to main
image: ghcr.io/pvarki/ocsprest-server:1.0.7-260427-pr24-20260506t1433
build:
context: https://github.com/pvarki/docker-rasenmaeher-cfssl.git#ba2ae8454c9b33d20b46fd4fc39dcb66b3afd2c0
dockerfile: Dockerfile
target: ocsprest
image: ${PVARKI_DOCKER_REPO:-ghcr.io/}pvarki/ocsprest-server:1.0.7-260612
networks:
- canet
environment:
Expand Down Expand Up @@ -145,7 +135,7 @@ services:
condition: service_healthy

postgres:
image: ${HUB_DOCKER_REPO:-docker.io/}postgres:12
image: ${PVARKI_DOCKER_REPO:-ghcr.io/}pvarki/postgis-server:15.3.5-260524
volumes:
- ./pg_init:/docker-entrypoint-initdb.d
- pg_data:/var/lib/postgresql/data
Expand Down
Loading
Loading