Skip to content

refactor: de-dup JWT verification, DB lifecycle, and keypair generation#59

Open
PenguinzTech wants to merge 1 commit into
feature/manager-security-hardeningfrom
chore/dedup-reusable-code
Open

refactor: de-dup JWT verification, DB lifecycle, and keypair generation#59
PenguinzTech wants to merge 1 commit into
feature/manager-security-hardeningfrom
chore/dedup-reusable-code

Conversation

@PenguinzTech

@PenguinzTech PenguinzTech commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Stacked on #58. Seventh branch in the chain. Reusable-code pass — no behavior changes; every affected suite re-verified.

De-duplication

  • dns-server — one JWT verifier: the identical ES256/RS256 verification contract (alg allowlist, iss/aud, required exp/iat/tenant, fail-closed) duplicated in selective_router.py and resilience.py now lives in app/utils/jwt_verify.py (verify_squawk_jwt). Authorization (zone visibility / team rules) stays with the callers. The 24 @patch-based auth-bypass regression tests run unchanged.
  • manager — DB lifecycle: client_config_service.py had 12 copies of db = self._get_db() … finally: db.close(); replaced with a _with_db decorator that injects the connection and guarantees close (bodies untouched → low-risk diff).
  • manager — keypair generation: new app/utils/crypto.generate_ephemeral_es256_keypair() consumed by config.py (Dev+Test blocks) and test helpers (conftest jwt_keypair, domain-JWT tests). client_config_service keeps a self-contained copy by design — it's imported bare by the acceptance harness where app.* resolves to a different package (rationale documented inline; discovered the hard way when the shared import broke all 25 acceptance tests).

Also fixes

  • 2 stale dns-server acceptance tests missed by the feat: asymmetric signing for deployment-domain JWTs #54 asymmetric domain-JWT migration (still decoding with the removed HS256 jwt_secret attribute); the expired-token test now uses a genuinely expired ES256 token instead of exercising alg rejection.
  • Pre-existing tests/test_installer.py::test_check_admin_windows failure: patching ctypes.windll can't resolve on Linux — mock attached with create=True.

Verification

manager 142, dns-server 58, client-config acceptance 25, root unit 67 — all passing. flake8/pyflakes clean.

🤖 Generated with Claude Code

Summary by Sourcery

Deduplicate JWT verification, DB connection lifecycle, and ES256 keypair generation across manager and dns-server while keeping behavior unchanged and fixing related test issues.

New Features:

  • Introduce a shared Squawk JWT verification helper in dns-server for all authorization paths.
  • Add a shared ES256 keypair generation utility in the manager service for dev/test and standalone usage.

Bug Fixes:

  • Update dns-server client config API tests to verify ES256/RS256 domain tokens instead of legacy HS256 secrets.
  • Ensure expired domain JWT tests use genuinely expired ES256 tokens signed with the manager keypair.
  • Fix Windows admin-check installer test by safely mocking ctypes.windll so it runs on non-Windows platforms.

Enhancements:

  • Centralize manager DB connection lifecycle via a decorator that injects and reliably closes connections, replacing repeated get/close blocks.
  • Document and retain a self-contained ES256 keypair generator inside client_config_service to support the acceptance-test harness import model.

Tests:

  • Refresh manager and dns-server JWT-related tests and fixtures to consume the new ES256 keypair helper and shared verification behavior.

Reusable-code pass (no behavior changes; every suite re-verified):

- dns-server: extract the duplicated ES256/RS256 verification contract
  (alg allowlist, iss/aud, required exp/iat/tenant, fail-closed) from
  selective_router.py and resilience.py into app/utils/jwt_verify.py
  (verify_squawk_jwt). Authorization (zone visibility/team rules) stays
  with the callers. Existing 24 @patch-based regression tests unchanged.

- manager client_config_service: replace 12 copies of the
  'db = self._get_db() ... finally: db.close()' lifecycle with a _with_db
  decorator that injects the connection and guarantees close.

- manager keypair generation: new app/utils/crypto.generate_ephemeral_
  es256_keypair() used by config.py (Dev+Test blocks) and test helpers
  (conftest jwt_keypair, domain-jwt tests). client_config_service keeps a
  self-contained copy BY DESIGN — it is imported bare by the acceptance
  harness where app.* resolves to a different package (documented inline).

- Fix 2 stale dns-server acceptance tests missed by the domain-JWT
  asymmetric migration (still decoding with the removed HS256 jwt_secret
  attr); expired-token test now uses a genuinely expired ES256 token.

- Fix pre-existing tests/test_installer.py::test_check_admin_windows:
  patching ctypes.windll fails on Linux (attribute doesn't exist);
  attach a MagicMock with create=True.

Verified: manager 142, dns-server 58, client-config acceptance 25,
root unit 67 — all passing. flake8/pyflakes clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Reviewer's Guide

Centralizes JWT verification in dns-server, factors out ephemeral ES256 keypair generation in manager, and deduplicates DB connection lifecycle in client_config_service, plus fixes a few test issues so behavior stays consistent but implementation is cleaner and less error‑prone.

Sequence diagram for dns-server JWT verification via verify_squawk_jwt

sequenceDiagram
    actor User
    participant DNSSelectiveRouter
    participant JWTVerifier

    User->>DNSSelectiveRouter: check_zone_permission(domain, token)
    DNSSelectiveRouter->>JWTVerifier: verify_squawk_jwt(token, JWT_PUBLIC_KEY)
    JWTVerifier-->>DNSSelectiveRouter: payload or None

    alt payload is not None
        DNSSelectiveRouter->>DNSSelectiveRouter: apply zone visibility/team rules
        DNSSelectiveRouter-->>User: allow or deny
    else payload is None
        DNSSelectiveRouter-->>User: deny
    end
Loading

File-Level Changes

Change Details Files
Introduce a shared Squawk JWT verification helper and refactor dns-server authorization paths to use it.
  • Add app.utils.jwt_verify.verify_squawk_jwt implementing ES256/RS256 verification with iss/aud checks, required exp/iat/tenant, and fail-closed behavior.
  • Update selective_router zone permission checks to call verify_squawk_jwt instead of inlined PyJWT verification and keep authorization logic local.
  • Update resilience zone permission checks to call verify_squawk_jwt, simplifying token validation and preserving team-based authorization.
dns-server/app/utils/jwt_verify.py
dns-server/app/services/selective_router.py
dns-server/app/utils/resilience.py
Deduplicate manager DB connection lifecycle in client_config_service via a decorator.
  • Define a _with_db decorator that acquires a DB connection via self._get_db(), passes it into the wrapped method, and guarantees db.close() in a finally block.
  • Apply @_with_db to ClientConfigManager methods that previously manually opened/closed DB connections and add db: DB as an explicit parameter to their signatures.
  • Remove in-method db = self._get_db() / db.close() boilerplate while keeping method bodies and behavior intact.
manager/backend/app/services/client_config_service.py
Factor ES256 keypair generation into a shared crypto helper and update config/test code to use it while keeping client_config_service self-contained for acceptance tests.
  • Add app.utils.crypto.generate_ephemeral_es256_keypair to generate an ES256 keypair as PEM strings for dev/test/standalone manager usage.
  • Refactor manager Config and TestConfig initializers to call generate_ephemeral_es256_keypair instead of inlined cryptography code when JWT keys are absent or for tests.
  • Update manager test fixtures (jwt_keypair) and domain-JWT tests to use the shared helper, while retaining a local _generate_ephemeral_es256_keypair in client_config_service with inline rationale due to acceptance harness import behavior.
manager/backend/app/utils/crypto.py
manager/backend/app/config.py
manager/backend/tests/conftest.py
manager/backend/tests/test_client_config_domain_jwt.py
manager/backend/app/services/client_config_service.py
Update dns-server client config API tests to match asymmetric ES256 domain JWT behavior and ensure proper expiry semantics.
  • Change TestClientConfigManager fixture to construct ClientConfigManager without a symmetric jwt_secret, relying on ephemeral ES256 keypair and public key.
  • Update domain JWT verification in tests to decode with ES256/RS256 using the manager’s public key, issuer, and audience instead of HS256 and jwt_secret.
  • Modify expired_jwt_rejection test to create an actually expired ES256 token signed with the manager’s private key, exercising expiry handling rather than algorithm rejection.
dns-server/tests_full_future/test_client_config_api.py
Fix a pre-existing installer admin-check test failure on non-Windows platforms by mocking ctypes.windll safely.
  • Import ctypes and build a MagicMock windll object with shell32.IsUserAnAdmin returning 1.
  • Patch ctypes.windll with the mock using create=True inside the Windows platform patch so the Windows code path is testable on Linux/macOS CI without AttributeError.
tests/test_installer.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="manager/backend/app/services/client_config_service.py" line_range="87-95" />
<code_context>
+    return private_pem, public_pem
+
+
+def _with_db(method):
+    """Open a DB connection for the wrapped method and guarantee it closes.
+
+    The wrapped method receives the connection as its first argument after
+    ``self``. Centralizes the get/close lifecycle previously duplicated in
+    every method (``db = self._get_db()`` ... ``finally: db.close()``).
+    """
+    @wraps(method)
+    def wrapper(self, *args, **kwargs):
+        db = self._get_db()
+        try:
</code_context>
<issue_to_address>
**suggestion:** Consider typing `_with_db` to clarify the wrapped signature and return type.

Since `_with_db` is untyped while the decorated methods explicitly take a `DB` parameter, please add type hints for `method` and the wrapper’s return value (e.g., `Callable[..., Any]` or a more specific signature). This will make it clearer to type checkers and IDEs that the decorator preserves the original call signature apart from the injected `db`, and help avoid misuse.

Suggested implementation:

```python
from typing import Any, Callable


def _with_db(method: Callable[..., Any]) -> Callable[..., Any]:
    """Open a DB connection for the wrapped method and guarantee it closes.

    The wrapped method receives the connection as its first argument after
    ``self``. Centralizes the get/close lifecycle previously duplicated in
    every method (``db = self._get_db()`` ... ``finally: db.close()``).
    """

    @wraps(method)
    def wrapper(self, *args: Any, **kwargs: Any) -> Any:
        db = self._get_db()
        try:
            return method(self, db, *args, **kwargs)
        finally:
            db.close()

    return wrapper

```

1. If this file already has `from typing import ...` imports, merge `Any` and `Callable` into that existing import line instead of adding a new one.
2. If you prefer more precise typing and you have access to the `DB` type and are using Python 3.10+ / `typing_extensions`, you can refine this using `ParamSpec` and `Concatenate` to model the injected `db` parameter explicitly.
</issue_to_address>

### Comment 2
<location path="dns-server/app/utils/jwt_verify.py" line_range="36-45" />
<code_context>
+logger = logging.getLogger(__name__)
+
+
+def verify_squawk_jwt(token: str, public_key: Optional[str]) -> Optional[dict]:
+    """Verify a Squawk user JWT; return its payload or None (fail closed).
+
+    Args:
+        token: The presented bearer token.
+        public_key: PEM public key to verify with (caller's configured key).
+
+    Returns:
+        The verified payload dict, or None on any failure — unconfigured key,
+        bad signature/alg, expired, wrong iss/aud, missing/empty tenant.
+    """
+    if not public_key:
+        logger.error("JWT_PUBLIC_KEY not configured; denying access")
+        return None
+
+    if not token:
+        return None
+
</code_context>
<issue_to_address>
**nitpick:** The `token` parameter type could be relaxed to `Optional[str]` to match the usage.

The function already treats a missing/empty token as valid input and returns early, so the signature should reflect that by using `token: Optional[str]`. This will better match the actual contract and prevent unnecessary type-checker issues when callers pass an optional token.
</issue_to_address>

### Comment 3
<location path="tests/test_installer.py" line_range="28-37" />
<code_context>

     def test_check_admin_windows(self):
         """Test admin check on Windows"""
+        import ctypes
+        from unittest.mock import MagicMock
+
+        # ctypes.windll only exists on Windows — attach a mock (create=True)
+        # so the Windows code path is exercisable on Linux/macOS CI.
+        windll = MagicMock()
+        windll.shell32.IsUserAnAdmin.return_value = 1
         with patch('platform.system', return_value='Windows'):
-            with patch('ctypes.windll.shell32.IsUserAnAdmin', return_value=1):
+            with patch.object(ctypes, 'windll', windll, create=True):
                 installer = SquawkInstaller()
                 assert installer.is_admin is True
</code_context>
<issue_to_address>
**suggestion (testing):** Add a complementary test for the non-admin Windows path

To fully cover the Windows admin logic, add a second test where `IsUserAnAdmin` returns `0` and assert `installer.is_admin is False`, so both branches of the conditional are exercised and protected against regressions.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +87 to +95
def _with_db(method):
"""Open a DB connection for the wrapped method and guarantee it closes.

The wrapped method receives the connection as its first argument after
``self``. Centralizes the get/close lifecycle previously duplicated in
every method (``db = self._get_db()`` ... ``finally: db.close()``).
"""
@wraps(method)
def wrapper(self, *args, **kwargs):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider typing _with_db to clarify the wrapped signature and return type.

Since _with_db is untyped while the decorated methods explicitly take a DB parameter, please add type hints for method and the wrapper’s return value (e.g., Callable[..., Any] or a more specific signature). This will make it clearer to type checkers and IDEs that the decorator preserves the original call signature apart from the injected db, and help avoid misuse.

Suggested implementation:

from typing import Any, Callable


def _with_db(method: Callable[..., Any]) -> Callable[..., Any]:
    """Open a DB connection for the wrapped method and guarantee it closes.

    The wrapped method receives the connection as its first argument after
    ``self``. Centralizes the get/close lifecycle previously duplicated in
    every method (``db = self._get_db()`` ... ``finally: db.close()``).
    """

    @wraps(method)
    def wrapper(self, *args: Any, **kwargs: Any) -> Any:
        db = self._get_db()
        try:
            return method(self, db, *args, **kwargs)
        finally:
            db.close()

    return wrapper
  1. If this file already has from typing import ... imports, merge Any and Callable into that existing import line instead of adding a new one.
  2. If you prefer more precise typing and you have access to the DB type and are using Python 3.10+ / typing_extensions, you can refine this using ParamSpec and Concatenate to model the injected db parameter explicitly.

Comment on lines +36 to +45
def verify_squawk_jwt(token: str, public_key: Optional[str]) -> Optional[dict]:
"""Verify a Squawk user JWT; return its payload or None (fail closed).

Args:
token: The presented bearer token.
public_key: PEM public key to verify with (caller's configured key).

Returns:
The verified payload dict, or None on any failure — unconfigured key,
bad signature/alg, expired, wrong iss/aud, missing/empty tenant.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: The token parameter type could be relaxed to Optional[str] to match the usage.

The function already treats a missing/empty token as valid input and returns early, so the signature should reflect that by using token: Optional[str]. This will better match the actual contract and prevent unnecessary type-checker issues when callers pass an optional token.

Comment thread tests/test_installer.py
Comment on lines 28 to 37
def test_check_admin_windows(self):
"""Test admin check on Windows"""
import ctypes
from unittest.mock import MagicMock

# ctypes.windll only exists on Windows — attach a mock (create=True)
# so the Windows code path is exercisable on Linux/macOS CI.
windll = MagicMock()
windll.shell32.IsUserAnAdmin.return_value = 1
with patch('platform.system', return_value='Windows'):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add a complementary test for the non-admin Windows path

To fully cover the Windows admin logic, add a second test where IsUserAnAdmin returns 0 and assert installer.is_admin is False, so both branches of the conditional are exercised and protected against regressions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant