Skip to content

feat(spp_dci_client): DCI client improvements and audit trail logging#39

Merged
emjay0921 merged 5 commits intofeat/api-outgoing-logfrom
feat/dci-client-improvements
Feb 20, 2026
Merged

feat(spp_dci_client): DCI client improvements and audit trail logging#39
emjay0921 merged 5 commits intofeat/api-outgoing-logfrom
feat/dci-client-improvements

Conversation

@jeremi
Copy link
Member

@jeremi jeremi commented Feb 17, 2026

Summary

  • Allow non-admin users to use DCI verification by fixing data source access rules
  • Add action_approve_system() to spp_approval mixin for automated/programmatic approvals
  • Log all outgoing DCI SPDCI requests to the outgoing API audit trail (from PR feat(spp_api_v2): add outgoing API log model, service, and auditor security #38)
  • Add integration tests for outgoing log creation during DCI calls
  • Housekeeping: add dci_data_source.xml to .gitignore

Dependencies

Test plan

  • Run test_single_module.sh spp_dci_client
  • Run test_single_module.sh spp_approval
  • Verify non-admin users can trigger DCI birth verification
  • Verify outgoing DCI requests appear in the API outgoing log

Note

Medium Risk
Introduces sudo() paths and a new approval bypass method, plus adds persistent logging in the DCI request finally-block; mistakes here could impact access control expectations or create noisy/large audit logs.

Overview
Adds a new ApprovalMixin._action_approve_system() helper to allow system-initiated approvals that bypass user permission checks (via sudo()) and only runs when the record is pending.

Updates DCI OAuth2 token fetching to read/write admin-restricted credentials and token cache using sudo(), enabling non-admin flows that need tokens.

Extends DCIClient._make_request() to always emit an outgoing API audit log (soft-depending on spp_api_v2) with duration, status/status_code, response payload (including 401 pre-retry), and error detail, using a separate cursor so logs persist across rollbacks; adds a comprehensive integration test suite covering success, retries, and error cases.

Written by Cursor Bugbot for commit 3ea418e. This will update automatically on new commits. Configure here.

@gemini-code-assist
Copy link

Summary of Changes

Hello @jeremi, 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 DCI client by broadening access for non-admin users to DCI verification, introducing a system-level approval mechanism for automated processes, and establishing a robust audit trail for all outgoing DCI API requests. The changes improve both functionality and accountability, ensuring that all external communications are logged for review and troubleshooting, while also making the system more flexible for automated workflows.

Highlights

  • Non-Admin DCI Verification: Enabled non-admin users to perform DCI verification by adjusting data source access rules, specifically by using sudo() for OAuth2 credential access and caching.
  • Automated Approvals: Introduced a new method action_approve_system() in the approval mixin, allowing for automated or programmatic approvals that bypass standard user permission checks.
  • Outgoing API Audit Trail Logging: Implemented comprehensive logging for all outgoing DCI requests to an API audit trail, capturing request details, responses, status codes, and error information, with a separate database cursor to ensure log persistence even on transaction rollback.
  • Integration Tests for Logging: Added a new test file with integration tests to thoroughly validate the creation and content of outgoing API logs during various DCI client call scenarios, including success, HTTP errors, connection errors, and retries.
  • Git Ignore Update: Updated the .gitignore file to exclude spp_dci_demo/data/dci_data_source.xml, preventing local demo credentials from being committed.
Changelog
  • .gitignore
    • Added spp_dci_demo/data/dci_data_source.xml to the ignore list to prevent committing local demo credentials.
  • spp_approval/models/approval_mixin.py
    • Implemented action_approve_system method to allow system-initiated approvals, bypassing user permission checks and logging the action.
  • spp_dci_client/models/data_source.py
    • Modified get_oauth2_token to use sudo() when accessing and caching OAuth2 client credentials, enabling non-admin users to perform DCI verification.
  • spp_dci_client/services/client.py
    • Imported the time module for measuring request durations.
    • Introduced variables to track logging details (status, status code, response data, error detail) throughout the _make_request method.
    • Enhanced the 401 Unauthorized retry logic to capture the initial 401 response data and status for logging.
    • Captured successful response status codes and data for logging.
    • Updated exception handling for httpx.HTTPStatusError, connection errors, and generic exceptions to populate logging variables with relevant details.
    • Added a finally block to _make_request to ensure _log_outgoing_call is always invoked, regardless of request outcome.
    • Implemented _log_outgoing_call to record outgoing API calls to spp.api.outgoing.log using a separate database cursor for transaction safety.
    • Implemented _copy_envelope_for_log to create a shallow copy of the request envelope suitable for audit log storage, preserving cryptographic signatures.
  • spp_dci_client/tests/init.py
    • Imported the new test_outgoing_log_integration module to include its tests in the test suite.
  • spp_dci_client/tests/test_outgoing_log_integration.py
    • Added a new test file containing TestOutgoingLogClientMethods to test DCI client logging helper methods without spp_api_v2 dependency.
    • Added TestOutgoingLogIntegration to verify the DCI client's integration with the outgoing API log under various scenarios (success, HTTP errors, connection errors, timeouts, SSL errors, DNS errors, generic exceptions, and 401 retries).
Activity
  • The pull request was created by jeremi and is awaiting review.
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.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

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 improvements, including the addition of action_approve_system for programmatic approvals, a fix allowing non-admin users to trigger DCI verification via sudo() for credential access, and robust audit trail logging for all outgoing DCI requests, complemented by comprehensive integration tests. However, two critical security issues were identified: a potential bypass of the approval workflow in ApprovalMixin and the exposure of cached OAuth2 access tokens to non-admin users in the DCIDataSource model. Addressing these vulnerabilities is crucial to ensure system security. Additionally, consider minor suggestions in spp_dci_client/services/client.py for more specific exception handling.

Comment on lines 335 to 362
def action_approve_system(self, comment=None):
"""System-initiated approval bypassing user permission checks.

Use this for automated approvals triggered by system events (e.g., DCI
verification match, scheduled jobs) where there is no human approver.

This method uses sudo() to bypass access controls and skips the
_check_can_approve() permission validation.

Args:
comment: Optional approval comment for audit trail
"""
for record in self:
if record.approval_state != "pending":
_logger.warning(
"Skipping system approval for %s %s: state is %s, not pending",
record._name,
record.id,
record.approval_state,
)
continue
record.sudo()._do_approve(comment=comment)
_logger.info(
"System auto-approved %s %s: %s",
record._name,
record.id,
comment or "(no comment)",
)

Choose a reason for hiding this comment

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

security-high high

The action_approve_system method allows bypassing all approval authorization checks. Since it starts with the action_ prefix and lacks any caller verification, it is exposed to Odoo's RPC interface. Any user with 'write' access to a record using this mixin can call this method to approve it, even if they are not an authorized approver. This method explicitly uses sudo() and skips the _check_can_approve() validation, which is intended for system-initiated approvals but creates a significant security risk if exposed. Consider renaming it to start with an underscore (e.g., _action_approve_system) to prevent RPC exposure, or adding a check to ensure it is only called in a superuser context (e.g., if not self.env.su: raise AccessError(...)).

log_error_detail = "401 Unauthorized - retrying with fresh token"
try:
log_response_data = response.json()
except Exception:

Choose a reason for hiding this comment

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

medium

Catching a broad Exception can hide unexpected errors. It's better to catch the specific exception that response.json() might raise, which is json.JSONDecodeError. This makes the code more robust and the intent clearer. Note that you'll need to add import json at the top of the file for this change.

Suggested change
except Exception:
except json.JSONDecodeError:

technical_detail += f": {error_data['header']['status_reason_message']}"
else:
technical_detail += f": {response_text}"
except Exception:

Choose a reason for hiding this comment

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

medium

Similar to my other comment, it's better to catch specific exceptions here. The try block performs JSON decoding and dictionary lookups. Catching (json.JSONDecodeError, KeyError, TypeError) would be more precise than a broad Exception and would make debugging easier if an unexpected error occurs. Note that you'll need to add import json at the top of the file for this change.

Suggested change
except Exception:
except (json.JSONDecodeError, KeyError, TypeError):

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

# Cache token with expiry (use sudo to write to restricted model)
expires_at = now + timedelta(seconds=expires_in)
self.write(
sudo_self.write(
Copy link

Choose a reason for hiding this comment

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

Missing sudo in token cache clearing breaks non-admin 401 retry

Medium Severity

The get_oauth2_token method now correctly uses sudo_self.write() to cache the token for non-admin users, but clear_oauth2_token_cache() still calls self.write() without sudo(). Per ir.model.access.csv, non-admin users have read-only access to spp.dci.data.source (perm_write=0). When a non-admin user hits a 401 response in _make_request, the call to self.data_source.clear_oauth2_token_cache() raises an AccessError, preventing the token-refresh retry from ever executing. Before this PR, non-admin users couldn't reach this code path at all (they'd fail at the credential read), so the sudo changes make this a newly reachable failure.

Fix in Cursor Fix in Web

- Use sudo() when accessing OAuth2 credentials in data source
- Use sudo() when caching OAuth2 token (write to restricted model)
- Add _after_submit() hook to auto-approve CR on submit if DCI verified

This enables users like demo_officer to use DCI birth verification
without requiring administrator privileges.
Add a dedicated method for system-initiated approvals that bypasses
user permission checks. This enables automated approval workflows
triggered by system events (e.g., DCI verification match) where
there is no human approver.

Also adds security control to invalidate verification when verified
fields (name, DOB, gender, BRN) are edited after verification.
Instrument _make_request to log all outgoing calls via
spp.api.outgoing.log (soft dependency). Logs persist in a separate
cursor so they survive transaction rollback on UserError. Captures
timing, status codes, request/response payloads, and error details
for success, HTTP errors, connection errors, timeouts, and 401
retries.
…oval and narrow exception handling

Rename action_approve_system to _action_approve_system to prevent Odoo
from exposing it via the RPC interface, since it uses sudo() and skips
_check_can_approve() authorization checks.

Replace broad Exception catches in spp_dci_client with specific exception
types (json.JSONDecodeError, KeyError, TypeError) and add json import.
…d fix test log ordering

Pass auto=True to _do_approve in _action_approve_system so automated approvals
show "(auto)" in activity feedback and skip submitter notification, matching the
intent of system-initiated approvals.

Fix reversed log assertions in test_401_retry_creates_two_log_entries: due to
try-finally semantics with recursive calls, the inner retry's finally creates
the success log first (lower ID), so with order="id desc" logs[0] is the 401
entry and logs[1] is the success entry.
@jeremi jeremi force-pushed the feat/dci-client-improvements branch from db3b9a4 to 3ea418e Compare February 18, 2026 14:50
Copy link
Contributor

@emjay0921 emjay0921 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 complete. Clean implementation across all three changes. No issues found.

@emjay0921
Copy link
Contributor

Review Notes

Code looks good overall — the separate-cursor logging pattern, soft dependency on spp_api_v2, and error resilience are well-implemented. A few observations:

  1. Blocked by PR feat(spp_api_v2): add outgoing API log model, service, and auditor security #38 — that PR has 3 deterministic test failures (test_display_name_falls_back_to_url, test_log_call_sanitizes_url, test_sanitize_url_masks_sensitive_params) that must be fixed before this can reach 19.0.

  2. _action_approve_system() has no consumers — added to spp_approval but not called anywhere in this PR or spp_dci_client. Consider deferring to the PR that actually wires it up, to keep changes focused.

  3. No CI ran — since this targets feat/api-outgoing-log instead of 19.0, the test workflow didn't trigger. Please run spp_dci_client and spp_approval tests locally to validate.

  4. Integration tests leave persistent data — the separate cursor pattern means log entries survive TransactionCase rollback. Not a blocker for ephemeral CI databases, but worth being aware of.

@emjay0921 emjay0921 merged commit 322c56e into feat/api-outgoing-log Feb 20, 2026
1 check passed
@emjay0921 emjay0921 deleted the feat/dci-client-improvements branch February 20, 2026 09:17
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.

3 participants

Comments