Skip to content

feat(spp_oauth): prepare spp_oauth for stable status with RS256 bridge module#114

Open
gonzalesedwin1123 wants to merge 4 commits into19.0from
fix/spp-oauth-stable-prep
Open

feat(spp_oauth): prepare spp_oauth for stable status with RS256 bridge module#114
gonzalesedwin1123 wants to merge 4 commits into19.0from
fix/spp-oauth-stable-prep

Conversation

@gonzalesedwin1123
Copy link
Member

Summary

  • Fix config defaults and improve test coverage for spp_oauth
  • Add new spp_api_v2_oauth bridge module enabling RS256 JWT authentication for API V2
  • Address staff engineer review findings across both modules (naming conventions, security, test quality)

Changes

spp_oauth

  • Category: "OpenSPP""OpenSPP/Integration" per module-visibility principles
  • Naming: Rename abbreviated field/param names (oauth_priv_keyoauth_private_key, oauth_pub_keyoauth_public_key) across models, config params, views, data, tests, and docs
  • Security: Restrict ACL from base.group_user to base.group_system with perm_create=1
  • Error handling: Remove unconditional ERROR logging from exception constructor — callers decide log level
  • Model class: Rename RegistryConfigOAuthConfig for clarity
  • Tests: Add wrong-key verification test, improve assertions (assertIsNotNone), remove numbered prefixes
  • Docs: Add DESCRIPTION.md and USAGE.md, fix stale references

spp_api_v2_oauth (new module)

Bridge module that auto-installs when both spp_api_v2 and spp_oauth are present, enabling RS256 JWT authentication alongside existing HS256 support.

  • Auth middleware: Override get_authenticated_client via FastAPI dependency override; route by JWT header alg field (RS256/HS256 only, reject unknown)
  • Token endpoint: POST /oauth/token/rs256 with same client auth flow, rate limiting, and payload structure as HS256
  • Security: Explicit algorithm allowlist, audience/issuer validation, no DB IDs in tokens
  • Tests: 31 tests covering RS256 auth, HS256 regression, token generation, endpoint logic
  • Manifest: auto_install: ["spp_api_v2", "spp_oauth"], application: False, category OpenSPP/Integration

Test plan

  • ./scripts/test_single_module.sh spp_oauth — 14/14 passing
  • ./scripts/test_single_module.sh spp_api_v2_oauth — 31/31 passing
  • pre-commit run — all 37 hooks passing (ruff, semgrep, bandit, pylint, OpenSPP checks)
  • Verify Settings UI: Settings > General Settings > SPP OAuth Settings renders correctly
  • Verify existing HS256 API clients continue working unchanged after bridge install
  • Verify /oauth/token/rs256 endpoint returns valid RS256 tokens when keys are configured

…rage

Empty default config parameter values so get_private_key()/get_public_key()
raise clear OpenSPPOAuthJWTException when keys are not configured, instead
of failing with a cryptic PyJWT error on the placeholder strings.

Also:
- Export get_private_key/get_public_key from tools __init__
- Remove unused MOCK_PRIVATE_KEY constant and deprecated default_backend()
- Move inline imports to top of test_rsa_encode_decode.py
- Add test_res_config_settings.py covering settings model
- Add test_exception_logs_error asserting _logger.error is called
- Add QA testing guide (USAGE.md) with 8 test scenarios
- Update DESCRIPTION.md with missing functions and cryptography dep
Bridge module that auto-installs when both spp_api_v2 and spp_oauth are
present, enabling RS256 JWT authentication for the API alongside the
existing HS256 flow.

- Override FastAPI auth dependency to route by JWT header alg field
- Add /oauth/token/rs256 endpoint for RS256 token generation
- Explicit algorithm allowlist (RS256/HS256 only) to prevent confusion attacks
- 22 tests covering RS256 auth, HS256 regression, and token generation
…indings

Rename abbreviated field/param names to full forms per naming conventions:
- oauth_priv_key → oauth_private_key
- oauth_pub_key → oauth_public_key
across models, config params, views, data, tests, and docs.

spp_oauth changes:
- Category: "OpenSPP" → "OpenSPP/Integration"
- ACL: restrict to base.group_system with perm_create=1
- Remove ERROR logging from exception constructor (let callers decide)
- Rename model class RegistryConfig → OAuthConfig
- Add wrong-key verification test
- Improve test assertions (assertIsNotNone) and remove numbered prefixes
- Fix stale doc references to removed logging behavior

spp_api_v2_oauth changes:
- Remove empty ir.model.access.csv (no new models)
- Add RS256 endpoint to API auth allowlist
- Fix nosemgrep annotation for multiline sudo()
- Remove Python <3.11 compat guard (Odoo 19 requires 3.11+)
- Import constants from ..constants instead of redeclaring
- Replace silent if-guards with skipTest() for endpoint tests
- Safe int() cast for token_lifetime_hours config
- asyncio.get_event_loop() → asyncio.run()
- Manual key restore → addCleanup for test safety
- SimpleNamespace for mock credentials
@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the OpenSPP API's authentication capabilities by introducing support for RS256 JSON Web Tokens (JWTs) through a new bridge module. It also refines the existing OAuth module, improving its maintainability, security, and user experience. The changes aim to provide more robust and flexible authentication options, particularly beneficial for distributed and zero-trust architectures, while ensuring backward compatibility for existing HS256 clients.

Highlights

  • New RS256 JWT Authentication Bridge: Introduced a new module, spp_api_v2_oauth, which enables RS256 (asymmetric RSA) JWT authentication for the OpenSPP API V2. This module automatically installs when both spp_api_v2 and spp_oauth are present, providing a dedicated /oauth/token/rs256 endpoint and routing incoming tokens based on their algorithm.
  • Enhanced spp_oauth Module Stability: The spp_oauth module has been prepared for stable status with several improvements, including fixing config defaults, enhancing test coverage, and addressing staff engineer review findings related to naming conventions, security, and test quality.
  • Improved Naming and Security in spp_oauth: Renamed abbreviated field/parameter names (e.g., oauth_priv_key to oauth_private_key) across models, config, views, data, tests, and documentation. Access Control Lists (ACL) for OAuth settings were restricted from base.group_user to base.group_system with create permissions, enhancing security.
  • Comprehensive Testing and Documentation: Extensive test coverage was added for both modules, including wrong-key verification, improved assertions, and tests for RS256 authentication, HS256 regression, token generation, and endpoint logic. New DESCRIPTION.md and USAGE.md files were added to spp_oauth and spp_api_v2_oauth for clearer documentation.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces significant enhancements by adding RS256 JWT authentication support through a new spp_api_v2_oauth bridge module, while also improving the existing spp_oauth module. The changes are well-structured, with comprehensive test coverage for both new functionality and regression testing for existing HS256 authentication. Key improvements include stricter access controls on OAuth key configuration, clearer naming conventions, and better error handling. The documentation has also been thoroughly updated. My review focuses on a couple of opportunities to reduce code duplication to further enhance long-term maintainability. Overall, this is an excellent contribution that strengthens the platform's authentication capabilities.

I am having trouble creating individual review comments. Click here to see my feedback.

spp_api_v2_oauth/constants.py (7-8)

medium

The constants JWT_AUDIENCE and JWT_ISSUER are also used in the spp_api_v2 module, where they are currently hardcoded. While defining them as constants here is an improvement, it introduces duplication between modules. To maintain a single source of truth, consider defining these constants in a shared location, perhaps within the spp_api_v2 module itself (since spp_api_v2_oauth depends on it), and importing them here. This would make future updates to these values safer and more maintainable.

spp_api_v2_oauth/routers/oauth_rs256.py (73-81)

medium

The logic to retrieve and parse the token_lifetime_hours configuration parameter, including the try-except block and default value handling, is duplicated from the original HS256 token endpoint in spp_api_v2. To improve maintainability and avoid potential inconsistencies, this logic could be extracted into a shared utility function that both token endpoints can call.

spp_oauth/tools/oauth_exception.py (1-9)

medium

Removing the automatic error logging from the exception's constructor is a good design choice. It correctly places the responsibility of logging on the caller, allowing for more context-specific log levels and messages. However, the exception message itself could be more informative for the developer who needs to debug it. Consider including the original error message when wrapping other exceptions.

class OpenSPPOAuthJWTException(Exception):
    """Raised when an OAuth JWT operation fails (missing keys, invalid tokens, etc.)."""
    pass

spp_oauth/tools/rsa_encode_decode.py (48-49)

medium

The variable name privkey is an abbreviation. For consistency with the other changes in this PR (e.g., oauth_private_key), it would be clearer to use the full name private_key.

    private_key = get_private_key(env)
    return jwt.encode(headers=header, payload=payload, key=private_key, algorithm=JWT_ALGORITHM)

spp_oauth/tools/rsa_encode_decode.py (61-63)

medium

The variable name pubkey is an abbreviation. For consistency with other variable names and to improve clarity, it's better to use the full name public_key.

    public_key = get_public_key(env)
    try:
        return jwt.decode(access_token, key=public_key, algorithms=[JWT_ALGORITHM])

@codecov
Copy link

codecov bot commented Mar 18, 2026

Codecov Report

❌ Patch coverage is 92.90780% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.83%. Comparing base (93fa4cb) to head (dd735c8).
⚠️ Report is 83 commits behind head on 19.0.

Files with missing lines Patch % Lines
spp_api_v2_oauth/routers/oauth_rs256.py 87.23% 6 Missing ⚠️
spp_api_v2_oauth/middleware/auth_rs256.py 94.44% 3 Missing ⚠️
spp_api_v2_oauth/__manifest__.py 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             19.0     #114      +/-   ##
==========================================
+ Coverage   70.76%   70.83%   +0.06%     
==========================================
  Files         682      689       +7     
  Lines       37347    37469     +122     
==========================================
+ Hits        26429    26541     +112     
- Misses      10918    10928      +10     
Flag Coverage Δ
spp_api_v2_oauth 92.00% <92.00%> (?)
spp_base_common 90.26% <ø> (ø)
spp_oauth 97.22% <100.00%> (-0.22%) ⬇️
spp_programs 45.51% <ø> (ø)
spp_security 66.66% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
spp_api_v2_oauth/__init__.py 100.00% <100.00%> (ø)
spp_api_v2_oauth/constants.py 100.00% <100.00%> (ø)
spp_api_v2_oauth/models/__init__.py 100.00% <100.00%> (ø)
spp_api_v2_oauth/models/fastapi_endpoint.py 100.00% <100.00%> (ø)
spp_oauth/__manifest__.py 0.00% <ø> (ø)
spp_oauth/models/res_config_settings.py 100.00% <100.00%> (ø)
spp_oauth/tools/__init__.py 100.00% <100.00%> (ø)
spp_oauth/tools/oauth_exception.py 100.00% <ø> (ø)
spp_oauth/tools/rsa_encode_decode.py 100.00% <100.00%> (ø)
spp_api_v2_oauth/__manifest__.py 0.00% <0.00%> (ø)
... and 2 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…l-disclosure warnings

Semgrep's python-logger-credential-disclosure rule flags log strings
containing "token" as potential secret leaks. Reword messages to avoid
the keyword while preserving log clarity.
@gonzalesedwin1123
Copy link
Member Author

Response to Gemini Code Review

Thanks for the review. Here's the status of each finding:

✅ Already Fixed

Finding #4 (privkey abbreviation) and #5 (pubkey abbreviation) — Both were renamed to private_key and public_key in commit de347d3. The review was based on an earlier diff.

ℹ️ No Action Needed

Finding #3 (exception class pass) — A class with only a docstring is valid Python; the docstring serves as the body. Adding pass is redundant and would be flagged by linters.

📋 Valid But Out of Scope

Finding #1 (duplicated JWT_AUDIENCE/JWT_ISSUER constants) and Finding #2 (duplicated token_lifetime_hours parsing) — Both are valid observations. The values "openspp" and "openspp-api-v2" are hardcoded as string literals in 4 places across spp_api_v2 (auth.py:208-209, oauth.py:219/221). They are not exported as named constants.

Fixing this properly requires modifying spp_api_v2 to extract shared constants/utilities — which is intentionally out of scope for this PR. The bridge module's design principle is zero changes to existing modules. Our constants.py centralizes them within the bridge, and a comment documents that they must match spp_api_v2.

These would be good follow-up items for a spp_api_v2 refactor PR.

@gonzalesedwin1123 gonzalesedwin1123 marked this pull request as ready for review March 18, 2026 06:00
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