-
Notifications
You must be signed in to change notification settings - Fork 0
feat(spp_api_v2): add outgoing API log model, service, and auditor security #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
f0b8178
feat(spp_api_v2): add auditor security group for API log payloads
jeremi d0916c4
feat(spp_api_v2): add outgoing API log model, service, and menu
jeremi e36e14f
fix(spp_api_v2): add payload masking and URL sanitization to outgoing…
jeremi 6438991
fix(spp_api_v2): remove url fallback from display_name to prevent sec…
jeremi 028e4e7
fix(dci): allow non-admin users to use DCI verification
jeremi 7bcc8e4
feat(spp_approval): add action_approve_system() for automated approvals
jeremi 6507a5a
feat(spp_dci_client): log outgoing DCI requests to audit trail
jeremi b14a931
fix(spp_approval,spp_dci_client): prevent RPC exposure of system appr…
jeremi 3ea418e
fix(spp_approval,spp_dci_client): add auto=True to system approval an…
jeremi c8937ac
feat(dci-demo): add DCI birth verification demo module
jeremi e09ac76
fix(dci-demo): handle reg_records in OpenCRVS SPDCI response format
jeremi 65e5c98
fix(dci-demo): add auto-approve to wizard flow
jeremi 64b4d6e
fix(dci-demo): hide data source selector when only one CRVS registry …
jeremi 5fb4be8
refactor(spp_dci_demo): remove wizard, consolidate auto-approval in _…
jeremi 43850e3
feat(spp_dci_demo,spp_mis_demo_v2): add Conditional Child Grant and d…
jeremi 7ee52e1
fix(spp_dci_demo): invalidate membership cache before enrollment and …
jeremi b7ecdf8
fix(spp_dci_demo): remove deprecated manifest keys, fix XML IDs and s…
jeremi 76e158e
fix(spp_dci_demo): fix XML ID naming and remaining semgrep finding
jeremi a9a731d
chore(spp_dci_demo): add auto-generated pyproject.toml from whool hook
jeremi 322c56e
Merge pull request #39 from OpenSPP/feat/dci-client-improvements
emjay0921 eeeb530
Merge pull request #40 from OpenSPP/feat/dci-demo-module
emjay0921 f5fbed7
fix(spp_api_v2): fix 3 failing outgoing log tests
emjay0921 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,222 @@ | ||
| # Part of OpenSPP. See LICENSE file for full copyright and licensing details. | ||
| """Audit log for outgoing API calls to external services.""" | ||
|
|
||
| import logging | ||
|
|
||
| from odoo import api, fields, models | ||
|
|
||
| _logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class ApiOutgoingLog(models.Model): | ||
| """ | ||
| Audit log for outgoing HTTP calls to external services. | ||
|
|
||
| Captures all outgoing API requests (DCI, webhooks, etc.) with | ||
| request/response details for troubleshooting and compliance. | ||
| """ | ||
|
|
||
| _name = "spp.api.outgoing.log" | ||
| _description = "Outgoing API Log" | ||
| _order = "timestamp desc" | ||
| _rec_name = "display_name" | ||
|
|
||
| # ========================================== | ||
| # Request - WHAT was sent | ||
| # ========================================== | ||
| url = fields.Char( | ||
| required=True, | ||
| index=True, | ||
| groups="spp_api_v2.group_api_v2_auditor", | ||
| help="Full URL called (may contain sensitive query parameters)", | ||
| ) | ||
|
|
||
| endpoint = fields.Char( | ||
| index=True, | ||
| help="Path portion of the URL, e.g. /registry/sync/search", | ||
| ) | ||
|
|
||
| http_method = fields.Selection( | ||
| [ | ||
| ("POST", "POST"), | ||
| ("GET", "GET"), | ||
| ("PUT", "PUT"), | ||
| ("PATCH", "PATCH"), | ||
| ("DELETE", "DELETE"), | ||
| ], | ||
| default="POST", | ||
| required=True, | ||
| ) | ||
|
|
||
| request_summary = fields.Json( | ||
| groups="spp_api_v2.group_api_v2_auditor", | ||
| help="Request payload (secrets redacted)", | ||
| ) | ||
|
|
||
| # ========================================== | ||
| # Response - WHAT came back | ||
| # ========================================== | ||
| response_summary = fields.Json( | ||
| groups="spp_api_v2.group_api_v2_auditor", | ||
| help="Response payload", | ||
| ) | ||
|
|
||
| response_status_code = fields.Integer( | ||
| index=True, | ||
| ) | ||
|
|
||
| # ========================================== | ||
| # Context - WHO triggered it | ||
| # ========================================== | ||
| user_id = fields.Many2one( | ||
| "res.users", | ||
| default=lambda self: self.env.uid, | ||
| index=True, | ||
| ) | ||
|
|
||
| origin_model = fields.Char( | ||
| index=True, | ||
| help="Model that triggered the call, e.g. spp.dci.data.source", | ||
| ) | ||
|
|
||
| origin_record_id = fields.Integer( | ||
| help="Record ID that triggered the call", | ||
| ) | ||
|
|
||
| # ========================================== | ||
| # Timestamps & Performance | ||
| # ========================================== | ||
| timestamp = fields.Datetime( | ||
| required=True, | ||
| default=fields.Datetime.now, | ||
| index=True, | ||
| ) | ||
|
|
||
| duration_ms = fields.Integer( | ||
| help="Request duration in milliseconds", | ||
| ) | ||
|
|
||
| # ========================================== | ||
| # Service Context | ||
| # ========================================== | ||
| service_name = fields.Char( | ||
| index=True, | ||
| help="Human-readable service name, e.g. DCI Client", | ||
| ) | ||
|
|
||
| service_code = fields.Char( | ||
| index=True, | ||
| help="Machine-readable service code, e.g. crvs_main", | ||
| ) | ||
|
|
||
| # ========================================== | ||
| # Result | ||
| # ========================================== | ||
| status = fields.Selection( | ||
| [ | ||
| ("success", "Success"), | ||
| ("http_error", "HTTP Error"), | ||
| ("connection_error", "Connection Error"), | ||
| ("timeout", "Timeout"), | ||
| ("error", "Error"), | ||
| ], | ||
| default="success", | ||
| required=True, | ||
| index=True, | ||
| ) | ||
|
|
||
| error_detail = fields.Text( | ||
| groups="spp_api_v2.group_api_v2_auditor", | ||
| help="Error message or traceback", | ||
| ) | ||
|
|
||
| # ========================================== | ||
| # Computed fields | ||
| # ========================================== | ||
| display_name = fields.Char( | ||
| compute="_compute_display_name", | ||
| store=True, | ||
| ) | ||
|
|
||
| @api.depends("http_method", "endpoint", "url", "timestamp") | ||
| def _compute_display_name(self): | ||
| for record in self: | ||
| timestamp_str = record.timestamp.strftime("%Y-%m-%d %H:%M") if record.timestamp else "" | ||
| path = record.endpoint or record.url or "API Call" | ||
| record.display_name = f"{record.http_method} {path} @ {timestamp_str}" | ||
|
|
||
| # ========================================== | ||
| # API Methods | ||
| # ========================================== | ||
| @api.model | ||
| def log_call( | ||
| self, | ||
| url: str, | ||
| http_method: str = "POST", | ||
| endpoint: str = None, | ||
| request_summary: dict = None, | ||
| response_summary: dict = None, | ||
| response_status_code: int = None, | ||
| user_id: int = None, | ||
| origin_model: str = None, | ||
| origin_record_id: int = None, | ||
| duration_ms: int = None, | ||
| service_name: str = None, | ||
| service_code: str = None, | ||
| status: str = "success", | ||
| error_detail: str = None, | ||
| ): | ||
| """ | ||
| Log an outgoing API call. | ||
|
|
||
| Args: | ||
| url: Full URL called | ||
| http_method: HTTP method (POST, GET, PUT, PATCH, DELETE) | ||
| endpoint: Path portion of the URL | ||
| request_summary: Request payload (secrets redacted) | ||
| response_summary: Response payload | ||
| response_status_code: HTTP response status code | ||
| user_id: User who triggered the call | ||
| origin_model: Odoo model that triggered the call | ||
| origin_record_id: Record ID that triggered the call | ||
| duration_ms: Request duration in milliseconds | ||
| service_name: Human-readable service name | ||
| service_code: Machine-readable service code | ||
| status: Result status | ||
| error_detail: Error message or traceback | ||
|
|
||
| Returns: | ||
| Created spp.api.outgoing.log record | ||
| """ | ||
| vals = { | ||
| "url": url, | ||
| "http_method": http_method, | ||
| "status": status, | ||
| "timestamp": fields.Datetime.now(), | ||
| } | ||
|
|
||
| # Optional fields | ||
| if endpoint: | ||
| vals["endpoint"] = endpoint | ||
| if request_summary is not None: | ||
| vals["request_summary"] = request_summary | ||
| if response_summary is not None: | ||
| vals["response_summary"] = response_summary | ||
| if response_status_code is not None: | ||
| vals["response_status_code"] = response_status_code | ||
| if user_id is not None: | ||
| vals["user_id"] = user_id | ||
| if origin_model: | ||
| vals["origin_model"] = origin_model | ||
| if origin_record_id is not None: | ||
| vals["origin_record_id"] = origin_record_id | ||
| if duration_ms is not None: | ||
| vals["duration_ms"] = duration_ms | ||
| if service_name: | ||
| vals["service_name"] = service_name | ||
| if service_code: | ||
| vals["service_code"] = service_code | ||
| if error_detail: | ||
| vals["error_detail"] = error_detail | ||
|
|
||
| return self.create(vals) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
urlfield stores the full URL of outgoing API calls, which may contain sensitive information such as API keys or tokens in query parameters. Unlike the payload fields, theurlfield is not restricted to thegroup_api_v2_auditorgroup, making it visible to any user with 'viewer' access to the logs. Consider addinggroups="spp_api_v2.group_api_v2_auditor"to this field or implementing logic to strip sensitive query parameters before logging.