Skip to content
Draft
18 changes: 16 additions & 2 deletions spp_dci_client_compliance/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,10 @@ After installing:

1. Set system parameter ``dci.client_compliance.mock_registry_url`` to
point to your mock registry (default: ``http://mock_registry:3335``)
2. Set system parameter ``dci.client_compliance.bearer_token`` for
authentication (default: ``compliance-test-api-key-12345``)
2. Set system parameter ``dci.client_compliance.bearer_token`` to a
**private** token for authentication. There is no default, and the
well-known value ``compliance-test-api-key-12345`` is rejected; the
trigger endpoints refuse to run until a private token is configured.
3. Verify test data source exists under **Settings > Technical > DCI >
Configuration > Data Sources** (auto-created if missing)

Expand Down Expand Up @@ -114,6 +116,18 @@ Dependencies
.. contents::
:local:

Changelog
=========

19.0.1.0.2
~~~~~~~~~~

- fix(security): remove compliance data sources that still hold the old
shared bearer token on upgrade, refuse to serve any such record from
the trigger controller, and reject the well-known default token when
configured, so upgraded or freshly configured databases cannot use the
shared credential over the unauthenticated trigger routes.

Bug Tracker
===========

Expand Down
2 changes: 1 addition & 1 deletion spp_dci_client_compliance/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
"name": "OpenSPP DCI Client Compliance Tests",
"category": "OpenSPP",
"version": "19.0.1.0.1",
"version": "19.0.1.0.2",
"sequence": 1,
"author": "OpenSPP.org",
"website": "https://github.com/OpenSPP/OpenSPP2",
Expand Down
26 changes: 24 additions & 2 deletions spp_dci_client_compliance/controllers/trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from odoo.exceptions import UserError
from odoo.http import request

from ..models.data_source import DEFAULT_COMPLIANCE_BEARER_TOKEN

_logger = logging.getLogger(__name__)

COMPLIANCE_ENABLED_PARAM = "dci.client_compliance.enabled"
Expand Down Expand Up @@ -70,6 +72,18 @@ def _get_compliance_bearer_token(env):
f"Set the system parameter {BEARER_TOKEN_PARAM!r} before "
f"using the trigger endpoints."
)
# Refuse the well-known default token: it is public, so accepting it
# would recreate the exact exposure the 19.0.1.0.2 migration purges -
# a data source authenticating outbound requests with a shared secret.
# Plain equality against a PUBLIC sentinel - not a secret comparison, so
# timing analysis leaks nothing.
# nosemgrep: odoo-timing-attack-password
if token == DEFAULT_COMPLIANCE_BEARER_TOKEN:
raise UserError(
f"The DCI client compliance bearer token is set to the well-known "
f"default value, which is not allowed. Set {BEARER_TOKEN_PARAM!r} to a "
f"private token before using the trigger endpoints."
)
return token

def _disabled_response(self):
Expand All @@ -95,16 +109,24 @@ def _get_test_data_source(self):
# nosemgrep: odoo-sudo-without-context
DataSource = request.env["spp.dci.data.source"].sudo()

# Exclude any record still holding the well-known default token directly
# in the domain. Upgraded databases may retain such a record (see the
# 19.0.1.0.2 migration); serving it would re-expose the shared secret
# over the ``auth='none'`` routes. Filtering in the domain rather than
# post-search means a legitimately re-keyed record is still found even
# when a stale record sorts ahead of it under limit=1.
not_default = ("bearer_token", "!=", DEFAULT_COMPLIANCE_BEARER_TOKEN)

# First try to find one marked for compliance testing
test_ds = DataSource.search(
[("is_compliance_test", "=", True)],
[("is_compliance_test", "=", True), not_default],
limit=1,
)

if not test_ds:
# Fall back to one named "DCI Compliance Test"
test_ds = DataSource.search(
[("name", "=", "DCI Compliance Test")],
[("name", "=", "DCI Compliance Test"), not_default],
limit=1,
)

Expand Down
34 changes: 34 additions & 0 deletions spp_dci_client_compliance/migrations/19.0.1.0.2/post-migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
"""Remove compliance data sources that still hold the old shared bearer token.

The 19.0.1.0.1 migration cleared only the ``ir.config_parameter`` copy of the
well-known token ``compliance-test-api-key-12345``. Earlier versions also
created an ``spp.dci.data.source`` record carrying that token in its own
``bearer_token`` column, which the trigger controller would keep using once the
compliance gate was re-enabled - re-exposing the shared secret over the
``auth='none'`` routes on upgraded databases.

Delete any such retained record so the controller falls back to its fail-closed
create path (which requires an operator-configured token). Records an operator
has re-keyed with a real token are left untouched.
"""

import logging

from odoo import SUPERUSER_ID, api

_logger = logging.getLogger(__name__)


def migrate(cr, version):
if not version:
return

env = api.Environment(cr, SUPERUSER_ID, {})
removed = env["spp.dci.data.source"]._purge_default_compliance_bearer_token()
if removed:
_logger.warning(
"Removed %d DCI compliance data source(s) that still held the default bearer token. "
"Set 'dci.client_compliance.bearer_token' before re-enabling the trigger endpoints.",
removed,
)
44 changes: 43 additions & 1 deletion spp_dci_client_compliance/models/data_source.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
"""Extension to spp.dci.data.source for compliance testing."""

from odoo import fields, models
import logging

from odoo import api, fields, models

_logger = logging.getLogger(__name__)

# Well-known bearer token that earlier module versions (19.0.1.0.0) shipped as
# the default for the compliance test data source. It is public, so any data
# source still holding it must never be used to authenticate outbound requests.
DEFAULT_COMPLIANCE_BEARER_TOKEN = "compliance-test-api-key-12345"


class DCIDataSourceCompliance(models.Model):
Expand All @@ -15,3 +24,36 @@ class DCIDataSourceCompliance(models.Model):
help="Mark this data source as used for DCI compliance testing. "
"Only one data source should have this flag enabled.",
)

@api.model
def _purge_default_compliance_bearer_token(self):
"""Delete data sources that still hold the well-known default token.

Earlier module versions created a compliance data source carrying
``DEFAULT_COMPLIANCE_BEARER_TOKEN``. The 19.0.1.0.2 post-migration
calls this to remove any such retained record so the trigger
controller falls back to its fail-closed create path (which requires
an operator-configured token). Records an operator has re-keyed with a
real token are matched on the token value alone, so they are left
untouched.

Returns:
int: number of data source records removed.
"""
# sudo(): bearer_token is field-level restricted to base.group_system;
# this maintenance sweep must see and remove records regardless of the
# calling user. Scope is limited to the exact known public secret.
# active_test=False: archived records still hold the token in their
# bearer_token column and remain usable by non-controller consumers
# (and readable at rest), so they must be purged too.
# nosemgrep: odoo-sudo-without-context
records = self.sudo().with_context(active_test=False)
stale = records.search([("bearer_token", "=", DEFAULT_COMPLIANCE_BEARER_TOKEN)])
for record in stale:
_logger.warning(
"Removing DCI compliance data source %r that still held the default bearer token.",
record.code,
)
count = len(stale)
stale.unlink()
return count
2 changes: 1 addition & 1 deletion spp_dci_client_compliance/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Testing infrastructure for SPDCI protocol compliance validation. Exposes HTTP en
After installing:

1. Set system parameter `dci.client_compliance.mock_registry_url` to point to your mock registry (default: `http://mock_registry:3335`)
2. Set system parameter `dci.client_compliance.bearer_token` for authentication (default: `compliance-test-api-key-12345`)
2. Set system parameter `dci.client_compliance.bearer_token` to a **private** token for authentication. There is no default, and the well-known value `compliance-test-api-key-12345` is rejected; the trigger endpoints refuse to run until a private token is configured.
3. Verify test data source exists under **Settings > Technical > DCI > Configuration > Data Sources** (auto-created if missing)

### Controller Endpoints
Expand Down
7 changes: 7 additions & 0 deletions spp_dci_client_compliance/readme/HISTORY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
### 19.0.1.0.2

- fix(security): remove compliance data sources that still hold the old shared
bearer token on upgrade, refuse to serve any such record from the trigger
controller, and reject the well-known default token when configured, so
upgraded or freshly configured databases cannot use the shared credential
over the unauthenticated trigger routes.
34 changes: 22 additions & 12 deletions spp_dci_client_compliance/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -416,8 +416,10 @@ <h1>Configuration</h1>
<ol class="arabic simple">
<li>Set system parameter <tt class="docutils literal">dci.client_compliance.mock_registry_url</tt> to
point to your mock registry (default: <tt class="docutils literal"><span class="pre">http://mock_registry:3335</span></tt>)</li>
<li>Set system parameter <tt class="docutils literal">dci.client_compliance.bearer_token</tt> for
authentication (default: <tt class="docutils literal"><span class="pre">compliance-test-api-key-12345</span></tt>)</li>
<li>Set system parameter <tt class="docutils literal">dci.client_compliance.bearer_token</tt> to a
<strong>private</strong> token for authentication. There is no default, and the
well-known value <tt class="docutils literal"><span class="pre">compliance-test-api-key-12345</span></tt> is rejected; the
trigger endpoints refuse to run until a private token is configured.</li>
<li>Verify test data source exists under <strong>Settings &gt; Technical &gt; DCI &gt;
Configuration &gt; Data Sources</strong> (auto-created if missing)</li>
</ol>
Expand Down Expand Up @@ -489,32 +491,40 @@ <h1>Dependencies</h1>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-1">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-2">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-3">Authors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-4">Maintainers</a></li>
</ul>
</li>
<li><a class="reference internal" href="#changelog" id="toc-entry-1">Changelog</a></li>
</ul>
</div>
<div class="section" id="changelog">
<h2><a class="toc-backref" href="#toc-entry-1">Changelog</a></h2>
</div>
</div>
<div class="section" id="section-1">
<h1>19.0.1.0.2</h1>
<ul class="simple">
<li>fix(security): remove compliance data sources that still hold the old
shared bearer token on upgrade, refuse to serve any such record from
the trigger controller, and reject the well-known default token when
configured, so upgraded or freshly configured databases cannot use the
shared credential over the unauthenticated trigger routes.</li>
</ul>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-1">Bug Tracker</a></h2>
<h2>Bug Tracker</h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OpenSPP/OpenSPP2/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OpenSPP/OpenSPP2/issues/new?body=module:%20spp_dci_client_compliance%0Aversion:%2019.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h2><a class="toc-backref" href="#toc-entry-2">Credits</a></h2>
<h2>Credits</h2>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-3">Authors</a></h3>
<h3>Authors</h3>
<ul class="simple">
<li>OpenSPP.org</li>
</ul>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-4">Maintainers</a></h3>
<h3>Maintainers</h3>
<p>Current maintainers:</p>
<p><a class="reference external image-reference" href="https://github.com/jeremi"><img alt="jeremi" src="https://github.com/jeremi.png?size=40px" /></a> <a class="reference external image-reference" href="https://github.com/gonzalesedwin1123"><img alt="gonzalesedwin1123" src="https://github.com/gonzalesedwin1123.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OpenSPP/OpenSPP2/tree/19.0/spp_dci_client_compliance">OpenSPP/OpenSPP2</a> project on GitHub.</p>
Expand Down
1 change: 1 addition & 0 deletions spp_dci_client_compliance/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

from . import test_trigger_controller
from . import test_trigger_edge_cases
from . import test_stale_bearer_token
Loading
Loading