Skip to content

Commit b738981

Browse files
authored
Merge pull request #304 from minvws/feature-flagged-routes
updated swagger and added gated routes
2 parents 8854790 + 742d987 commit b738981

7 files changed

Lines changed: 672 additions & 563 deletions

File tree

app.conf.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ loglevel=debug
33
mtls_override_cert=
44
# Enable the /test routes
55
enable_test_routes=True
6+
# Enable the exchange service routes (/exchange/*, /receive)
7+
enable_exchange_services_routes=True
68

79
[database]
810
dsn=postgresql+psycopg://postgres:postgres@postgres:5432/postgres

app.test.conf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
loglevel=debug
44
mtls_override_cert=
55
enable_test_routes=True
6+
enable_exchange_services_routes=True
67

78
[database]
89
dsn=postgresql+psycopg://postgres:postgres@localhost/testing

app/application.py

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,93 @@
1717
from app.auth import get_auth_ctx
1818

1919

20+
API_DESCRIPTION = """
21+
The Pseudoniemendienst (PRS) lets parties exchange data about a person without
22+
sharing their BSN. Instead of a BSN, parties exchange **RIDs** and **pseudonyms**
23+
that are scoped to a recipient organization and scope.
24+
25+
A recipient organization is always identified by a URA in the form
26+
`ura:<8 digits>` (e.g. `ura:90000036`).
27+
28+
The endpoints are grouped into the sections below. Most sections are protected by
29+
mutual TLS (mTLS); the calling organization and, where relevant, its public key
30+
are derived from the client certificate.
31+
"""
32+
33+
# Section (tag) metadata shown in the Swagger UI / OpenAPI schema. The order here
34+
# determines the order in which the sections are rendered.
35+
TAGS_METADATA = [
36+
{
37+
"name": "Service Information",
38+
"description": (
39+
"Public, unauthenticated endpoints reporting the service version and "
40+
"health status. Useful for load balancers, monitoring, and smoke tests."
41+
),
42+
},
43+
{
44+
"name": "Organizational Services",
45+
"description": (
46+
"Manage recipient organizations. An organization is identified by its "
47+
"URA and has a `max_key_usage` (`bsn`, `rp`, or `irp`) that caps which "
48+
"pseudonym types it is allowed to exchange."
49+
),
50+
},
51+
{
52+
"name": "Key Registration Services",
53+
"description": (
54+
"Register and manage the public keys that pseudonyms and RIDs are "
55+
"encrypted to. The organization and its public key are derived from the "
56+
"mTLS client certificate, so they are not part of the request body."
57+
),
58+
},
59+
{
60+
"name": "Key Version Services",
61+
"description": (
62+
"Manage the HSM key versions used to derive pseudonyms. Multiple "
63+
"versions can be active at once to support key rotation, where older "
64+
"versions remain available alongside the latest one."
65+
),
66+
},
67+
{
68+
"name": "OPRF Services",
69+
"description": (
70+
"Evaluate a blinded personal identifier using the Oblivious "
71+
"Pseudo-Random Function and return a JWE (encrypted to the recipient's "
72+
"public key) containing the evaluation for the active key version(s)."
73+
),
74+
},
75+
]
76+
77+
# Section (tag) metadata for the exchange routes. Only included in the OpenAPI
78+
# schema when `enable_exchange_services_routes` is set, matching when these routes
79+
# are mounted.
80+
EXCHANGE_TAGS_METADATA = [
81+
{
82+
"name": "Exchange Services",
83+
"description": (
84+
"Exchange a personal ID for a pseudonym or RID targeted at a recipient "
85+
"organization/scope, and redeem a previously issued RID for a pseudonym "
86+
"(or the BSN, when permitted by both the RID usage and the "
87+
"organization's `max_key_usage`)."
88+
),
89+
},
90+
]
91+
92+
# Section (tag) metadata for the test/helper routes. Only included in the OpenAPI
93+
# schema when `enable_test_routes` is set, matching when these routes are mounted.
94+
TEST_TAGS_METADATA = [
95+
{
96+
"name": "OPRF Testing Services",
97+
"description": (
98+
"Helper endpoints for testing and debugging the OPRF and JWE flows "
99+
"(client-side blinding, receiver finalization, JWE decoding, pseudonym "
100+
"reversal, and mTLS introspection). These are only mounted when "
101+
"`enable_test_routes` is set and must not be enabled in production."
102+
),
103+
},
104+
]
105+
106+
20107
def get_uvicorn_params() -> dict[str, Any]:
21108
config = get_config()
22109

@@ -72,13 +159,20 @@ def setup_logging() -> None:
72159
def setup_fastapi() -> FastAPI:
73160
config = get_config()
74161

162+
openapi_tags = list(TAGS_METADATA)
163+
if config.app.enable_exchange_services_routes:
164+
openapi_tags += EXCHANGE_TAGS_METADATA
165+
if config.app.enable_test_routes:
166+
openapi_tags += TEST_TAGS_METADATA
167+
75168
fastapi = (
76169
FastAPI(
77170
docs_url=config.uvicorn.docs_url,
78171
redoc_url=config.uvicorn.redoc_url,
79172
title="Pseudoniemendienst API",
80173
summary="API for the Pseudoniemendienst",
81-
description="Provides endpoints for OPRF, key management, organization management, and RID exchange.",
174+
description=API_DESCRIPTION,
175+
openapi_tags=openapi_tags,
82176
root_path=config.uvicorn.root_path,
83177
)
84178
if config.uvicorn.swagger_enabled
@@ -98,9 +192,10 @@ def setup_fastapi() -> FastAPI:
98192
oprf_router,
99193
key_router,
100194
hsm_key_version_router,
101-
exchange_router,
102195
org_router,
103196
]
197+
if config.app.enable_exchange_services_routes:
198+
routers.append(exchange_router)
104199
if config.app.enable_test_routes:
105200
routers.append(test_oprf_router)
106201

app/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class ConfigApp(BaseModel):
2323
loglevel: LogLevel = Field(default=LogLevel.info)
2424
mtls_override_cert: str | None = Field(default=None)
2525
enable_test_routes: bool = Field(default=False)
26+
enable_exchange_services_routes: bool = Field(default=True)
2627

2728

2829
class ConfigDatabase(BaseModel):

app/routers/default.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@
1919
"""
2020

2121

22-
@router.get("/")
22+
@router.get(
23+
"/",
24+
summary="Service banner and version information",
25+
tags=["Service Information"],
26+
)
2327
def index() -> Response:
2428
content = LOGO
2529

@@ -34,7 +38,11 @@ def index() -> Response:
3438
return PlainTextResponse(content)
3539

3640

37-
@router.get("/version.json")
41+
@router.get(
42+
"/version.json",
43+
summary="Service version as JSON",
44+
tags=["Service Information"],
45+
)
3846
def version_json() -> Response:
3947
try:
4048
with open(Path(__file__).parent.parent.parent / "version.json", "r") as file:

app/routers/health.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ def ok_or_error(value: bool) -> str:
1212
return "ok" if value else "error"
1313

1414

15-
@router.get("/health")
15+
@router.get(
16+
"/health",
17+
summary="Health check",
18+
tags=["Service Information"],
19+
)
1620
def health() -> dict[str, Any]:
1721
return {
1822
"status": "ok",

0 commit comments

Comments
 (0)