Skip to content

Commit

Permalink
Merge branch 'main' into save_issued_certificates
Browse files Browse the repository at this point in the history
  • Loading branch information
jschlyter committed Jan 7, 2025
2 parents 3c41069 + 1e6f701 commit 0cdbf9b
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 888 deletions.
7 changes: 1 addition & 6 deletions nodeman/internal_ca.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,7 @@
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID

from nodeman.x509 import (
CertificateAuthorityClient,
CertificateInformation,
PrivateKey,
verify_x509_csr_signature,
)
from nodeman.x509 import CertificateAuthorityClient, CertificateInformation, PrivateKey, verify_x509_csr_signature


class InternalCertificateAuthority(CertificateAuthorityClient):
Expand Down
14 changes: 12 additions & 2 deletions nodeman/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@
from pydantic.types import AwareDatetime

from .db_models import TapirNode
from .jose import PrivateJwk, PrivateSymmetric, PublicJwk, PublicJwks, public_key_factory
from .jose import (
PrivateJwk,
PrivateSymmetric,
PublicJwk,
PublicJwks,
public_key_factory,
)
from .settings import MqttUrl

MAX_REQUEST_AGE = 300
Expand Down Expand Up @@ -81,7 +87,7 @@ def validate_pem_bundle(cls, v: str):
return v


class NodeConfiguration(NodeCertificate):
class NodeConfiguration(BaseModel):
name: str = Field(title="Node name", examples=["node.example.com"])
mqtt_broker: MqttUrl = Field(title="MQTT Broker", examples=["mqtts://broker.example.com"])
mqtt_topics: dict[str, str] = Field(
Expand All @@ -90,3 +96,7 @@ class NodeConfiguration(NodeCertificate):
examples=[{"edm": "configuration/node.example.com/edm", "pop": "configuration/node.example.com/pop"}],
)
trusted_jwks: PublicJwks = Field(title="Trusted JWKS")


class NodeEnrollmentResult(NodeConfiguration, NodeCertificate):
pass
60 changes: 52 additions & 8 deletions nodeman/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
NodeCertificate,
NodeCollection,
NodeConfiguration,
NodeEnrollmentResult,
NodeInformation,
PublicKeyFormat,
RenewalRequest,
Expand All @@ -36,6 +37,9 @@
nodes_public_key_queries = meter.create_counter(
"nodes.public_key_queries", description="The number of node public keys queried"
)
node_configurations_requested = meter.create_counter(
"nodes.configurations", description="The number of node configurations requested"
)

router = APIRouter()

Expand All @@ -48,6 +52,15 @@ def find_node(name: str) -> TapirNode:
raise HTTPException(status.HTTP_404_NOT_FOUND)


def create_node_configuration(name: str, request: Request) -> NodeConfiguration:
return NodeConfiguration(
name=name,
mqtt_broker=request.app.settings.nodes.mqtt_broker,
mqtt_topics=request.app.settings.nodes.mqtt_topics,
trusted_jwks=request.app.trusted_jwks,
)


@router.post(
"/api/v1/node",
status_code=status.HTTP_201_CREATED,
Expand Down Expand Up @@ -159,7 +172,8 @@ async def get_node_public_key(
content = JWK(**node.public_key).export_to_pem().decode()
case PublicKeyFormat.JWK:
with tracer.start_as_current_span("get_public_key_jwk"):
content = json.dumps(node.public_key)
jwk_dict = {**node.public_key, "kid": name}
content = json.dumps(jwk_dict)
except ValueError as exc:
raise HTTPException(status.HTTP_406_NOT_ACCEPTABLE) from exc

Expand Down Expand Up @@ -198,7 +212,7 @@ def delete_node(name: str, username: Annotated[str, Depends(get_current_username
@router.post(
"/api/v1/node/{name}/enroll",
responses={
status.HTTP_200_OK: {"model": NodeConfiguration},
status.HTTP_200_OK: {"model": NodeEnrollmentResult},
status.HTTP_404_NOT_FOUND: {},
},
tags=["client"],
Expand All @@ -207,7 +221,7 @@ def delete_node(name: str, username: Annotated[str, Depends(get_current_username
async def enroll_node(
name: str,
request: Request,
) -> NodeConfiguration:
) -> NodeEnrollmentResult:
"""Enroll new node"""

node = find_node(name)
Expand Down Expand Up @@ -268,11 +282,8 @@ async def enroll_node(

nodes_enrolled.add(1)

return NodeConfiguration(
name=name,
mqtt_broker=request.app.settings.nodes.mqtt_broker,
mqtt_topics=request.app.settings.nodes.mqtt_topics,
trusted_jwks=request.app.trusted_jwks,
return NodeEnrollmentResult(
**create_node_configuration(name=name, request=request).model_dump(),
x509_certificate=node_certificate.x509_certificate,
x509_ca_certificate=node_certificate.x509_ca_certificate,
x509_certificate_serial_number=node_certificate.x509_certificate_serial_number,
Expand Down Expand Up @@ -331,3 +342,36 @@ async def renew_node(
nodes_renewed.add(1)

return res


@router.get(
"/api/v1/node/{name}/configuration",
responses={
status.HTTP_200_OK: {"model": NodeConfiguration},
status.HTTP_404_NOT_FOUND: {},
},
tags=["client"],
response_model_exclude_none=True,
)
async def get_node_configuration(
name: str,
request: Request,
response: Response,
) -> NodeConfiguration:
"""Get node configuration"""

node = find_node(name)

if not node.activated:
logging.debug("Node %s not activated", name, extra={"nodename": name})
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Node not activated")

res = create_node_configuration(name=name, request=request)

node_configurations_requested.add(1)

# Cache response for 5 minutes
max_age = request.app.settings.nodes.configuration_ttl
response.headers["Cache-Control"] = f"public, max-age={max_age}"

return res
26 changes: 22 additions & 4 deletions nodeman/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,23 @@
from typing import Annotated, Self

from argon2 import PasswordHasher
from pydantic import AnyHttpUrl, BaseModel, Field, FilePath, StringConstraints, UrlConstraints, model_validator
from pydantic_core import Url
from pydantic_settings import BaseSettings, PydanticBaseSettingsSource, SettingsConfigDict, TomlConfigSettingsSource

from dnstapir.opentelemetry import OtlpSettings
from pydantic import (
AnyHttpUrl,
BaseModel,
Field,
FilePath,
StringConstraints,
UrlConstraints,
model_validator,
)
from pydantic_core import Url
from pydantic_settings import (
BaseSettings,
PydanticBaseSettingsSource,
SettingsConfigDict,
TomlConfigSettingsSource,
)

MqttUrl = Annotated[
Url,
Expand Down Expand Up @@ -45,6 +57,12 @@ class NodesSettings(BaseModel):
trusted_jwks: FilePath | None = Field(default=None)
mqtt_broker: MqttUrl = Field(default="mqtt://localhost")
mqtt_topics: dict[str, str] = Field(default={})
configuration_ttl: int = Field(
default=300,
gt=0,
le=86400,
description="Configuration cache TTL in seconds",
)


class User(BaseModel):
Expand Down
Loading

0 comments on commit 0cdbf9b

Please sign in to comment.