Skip to content

Commit ea554b5

Browse files
feat(spp_dci_server): settings page for API tokens and security flags
Operators had to edit raw ir.config_parameter records to manage the DCI bearer-token allow-list and security flags. Add a 'DCI Server' section to Settings (and a Server Settings item under the DCI config menu) exposing: - API Authentication: dci.api_tokens, dci.api_tokens_required, dci.sender_id - Development/Insecure: allow_unsigned_requests, bypass_bearer_auth, allow_http_callbacks, allow_internal_callback_ips (clearly warned) The boolean flags are managed via get_values/set_values writing literal 'true'/'false' strings, because Odoo's config_parameter boolean machinery deletes the parameter when False (which would make the default-true dci.api_tokens_required impossible to disable) and misreads 'false' as truthy. The menu is gated to base.group_system.
1 parent eca3a91 commit ea554b5

6 files changed

Lines changed: 234 additions & 0 deletions

File tree

spp_dci_server/__manifest__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"views/sender_registry_views.xml",
2525
"views/transaction_views.xml",
2626
"views/subscription_views.xml",
27+
"views/res_config_settings_views.xml",
2728
],
2829
"installable": True,
2930
"application": False,

spp_dci_server/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from . import (
22
fastapi_endpoint_dci,
3+
res_config_settings,
34
sender_registry,
45
server_key,
56
subscription,
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
3+
from odoo import api, fields, models
4+
5+
6+
class ResConfigSettings(models.TransientModel):
7+
_inherit = "res.config.settings"
8+
9+
# The DCI auth middleware stores every boolean flag as the literal string
10+
# "true"/"false" (see middleware/signature.py:_read_security_flag) and falls
11+
# back to an in-code default when the parameter is missing. Odoo's
12+
# ``config_parameter`` boolean machinery is incompatible with that: it
13+
# *deletes* the parameter when the field is False (so a default-true flag can
14+
# never be turned off) and reads back ``bool("false")`` as True. We therefore
15+
# manage these flags explicitly in get_values/set_values instead.
16+
_DCI_FLAG_PARAMS = {
17+
"dci_api_tokens_required": ("dci.api_tokens_required", True),
18+
"dci_allow_unsigned_requests": ("dci.allow_unsigned_requests", False),
19+
"dci_bypass_bearer_auth": ("dci.bypass_bearer_auth", False),
20+
"dci_allow_http_callbacks": ("dci.allow_http_callbacks", False),
21+
"dci_allow_internal_callback_ips": ("dci.allow_internal_callback_ips", False),
22+
}
23+
24+
# --- API authentication ---
25+
dci_api_tokens = fields.Char(
26+
string="DCI API Bearer Tokens",
27+
config_parameter="dci.api_tokens",
28+
help="Accepted bearer tokens for incoming DCI requests. "
29+
"Comma-separated for multiple clients. Each token must match the "
30+
"Bearer Token configured on the calling client's data source.",
31+
)
32+
dci_sender_id = fields.Char(
33+
string="DCI Server Sender ID",
34+
config_parameter="dci.sender_id",
35+
default="openspp",
36+
help="This server's own DCI sender id, stamped on outgoing envelopes.",
37+
)
38+
dci_api_tokens_required = fields.Boolean(
39+
string="Require DCI API Tokens",
40+
default=True,
41+
help="When enabled and no tokens are configured, every request is "
42+
"rejected (fail-closed). Disable only for development.",
43+
)
44+
45+
# --- Development / insecure options (never enable in production) ---
46+
dci_allow_unsigned_requests = fields.Boolean(
47+
string="Allow Unsigned Requests",
48+
default=False,
49+
help="Development only. Accept DCI envelopes that carry no signature, "
50+
"skipping signature verification. Never enable in production.",
51+
)
52+
dci_bypass_bearer_auth = fields.Boolean(
53+
string="Bypass Bearer Authentication",
54+
default=False,
55+
help="Development only. Skip the bearer-token check entirely. Never enable in production.",
56+
)
57+
dci_allow_http_callbacks = fields.Boolean(
58+
string="Allow HTTP Callbacks",
59+
default=False,
60+
help="Development only. Permit plain-http (non-TLS) callback URLs. Never enable in production.",
61+
)
62+
dci_allow_internal_callback_ips = fields.Boolean(
63+
string="Allow Internal Callback IPs",
64+
default=False,
65+
help="Development only. Permit callbacks to internal/private IP addresses. Never enable in production.",
66+
)
67+
68+
@api.model
69+
def get_values(self):
70+
res = super().get_values()
71+
# System parameters require sudo; this view is gated to base.group_system.
72+
# nosemgrep: odoo-sudo-without-context
73+
icp = self.env["ir.config_parameter"].sudo()
74+
for field_name, (param, default) in self._DCI_FLAG_PARAMS.items():
75+
default_str = "true" if default else "false"
76+
res[field_name] = icp.get_param(param, default_str).lower() == "true"
77+
return res
78+
79+
def set_values(self):
80+
super().set_values()
81+
# System parameters require sudo; this view is gated to base.group_system.
82+
# nosemgrep: odoo-sudo-without-context
83+
icp = self.env["ir.config_parameter"].sudo()
84+
for field_name, (param, _default) in self._DCI_FLAG_PARAMS.items():
85+
icp.set_param(param, "true" if self[field_name] else "false")

spp_dci_server/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from . import test_rate_limit_middleware
1313
from . import test_receipt
1414
from . import test_receipt_router
15+
from . import test_res_config_settings
1516
from . import test_search_router
1617
from . import test_sender_registry
1718
from . import test_server_key
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
3+
from odoo.tests import TransactionCase, tagged
4+
5+
from odoo.addons.spp_dci_server.middleware.signature import _read_security_flag
6+
7+
8+
@tagged("post_install", "-at_install")
9+
class TestDCIServerConfigSettings(TransactionCase):
10+
"""The DCI Server settings page must round-trip to ir.config_parameter
11+
and feed the values the auth middleware reads."""
12+
13+
def setUp(self):
14+
super().setUp()
15+
self.Settings = self.env["res.config.settings"]
16+
self.Param = self.env["ir.config_parameter"].sudo()
17+
18+
def _save(self, values):
19+
settings = self.Settings.create(values)
20+
settings.execute()
21+
return settings
22+
23+
def test_api_tokens_round_trip(self):
24+
"""Bearer tokens entered in settings persist to dci.api_tokens."""
25+
self._save({"dci_api_tokens": "alpha,beta"})
26+
self.assertEqual(self.Param.get_param("dci.api_tokens"), "alpha,beta")
27+
28+
# And reading settings back reflects the stored value.
29+
reloaded = self.Settings.create({})
30+
self.assertEqual(reloaded.dci_api_tokens, "alpha,beta")
31+
32+
def test_sender_id_round_trip(self):
33+
self._save({"dci_sender_id": "openspp.test.server"})
34+
self.assertEqual(self.Param.get_param("dci.sender_id"), "openspp.test.server")
35+
36+
def test_security_flags_feed_middleware(self):
37+
"""Toggling the dev flags is visible to the middleware's flag reader."""
38+
self._save(
39+
{
40+
"dci_allow_unsigned_requests": True,
41+
"dci_api_tokens_required": False,
42+
}
43+
)
44+
self.assertEqual(_read_security_flag(self.env, "dci.allow_unsigned_requests"), "true")
45+
self.assertEqual(_read_security_flag(self.env, "dci.api_tokens_required"), "false")
46+
47+
# Flipping them back is honoured too.
48+
self._save(
49+
{
50+
"dci_allow_unsigned_requests": False,
51+
"dci_api_tokens_required": True,
52+
}
53+
)
54+
self.assertEqual(_read_security_flag(self.env, "dci.allow_unsigned_requests"), "false")
55+
self.assertEqual(_read_security_flag(self.env, "dci.api_tokens_required"), "true")
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<odoo>
3+
<record id="res_config_settings_dci_server_view_form" model="ir.ui.view">
4+
<field name="name">
5+
res.config.settings.view.form.inherit.dci.server
6+
</field>
7+
<field name="model">res.config.settings</field>
8+
<field name="priority" eval="10" />
9+
<field name="inherit_id" ref="base.res_config_settings_view_form" />
10+
<field name="arch" type="xml">
11+
<xpath expr="//form" position="inside">
12+
<app
13+
string="DCI Server"
14+
name="dci_server_settings"
15+
logo="/spp_dci_server/static/description/icon.png"
16+
>
17+
<block title="API Authentication" name="dci_server_auth">
18+
<setting
19+
string="DCI API Bearer Tokens"
20+
help="Comma-separated list of accepted bearer tokens. Each must match the Bearer Token on the calling client's data source."
21+
>
22+
<field name="dci_api_tokens" />
23+
</setting>
24+
<setting
25+
string="Require DCI API Tokens"
26+
help="Fail closed: reject every request when no tokens are configured. Disable only for development."
27+
>
28+
<field name="dci_api_tokens_required" />
29+
</setting>
30+
<setting
31+
string="DCI Server Sender ID"
32+
help="This server's own DCI sender id, stamped on outgoing envelopes."
33+
>
34+
<field name="dci_sender_id" />
35+
</setting>
36+
</block>
37+
<block title="Development / Insecure Options" name="dci_server_dev">
38+
<div class="text-warning mb-3" colspan="2">
39+
<i class="fa fa-warning" /> These options weaken or
40+
disable DCI security checks. Enable them only in
41+
development — never in production.
42+
</div>
43+
<setting
44+
string="Allow Unsigned Requests"
45+
help="Accept DCI envelopes that carry no signature, skipping signature verification."
46+
>
47+
<field name="dci_allow_unsigned_requests" />
48+
</setting>
49+
<setting
50+
string="Bypass Bearer Authentication"
51+
help="Skip the bearer-token check entirely."
52+
>
53+
<field name="dci_bypass_bearer_auth" />
54+
</setting>
55+
<setting
56+
string="Allow HTTP Callbacks"
57+
help="Permit plain-http (non-TLS) callback URLs."
58+
>
59+
<field name="dci_allow_http_callbacks" />
60+
</setting>
61+
<setting
62+
string="Allow Internal Callback IPs"
63+
help="Permit callbacks to internal/private IP addresses."
64+
>
65+
<field name="dci_allow_internal_callback_ips" />
66+
</setting>
67+
</block>
68+
</app>
69+
</xpath>
70+
</field>
71+
</record>
72+
73+
<record id="res_config_settings_dci_server_action" model="ir.actions.act_window">
74+
<field name="name">Settings</field>
75+
<field name="type">ir.actions.act_window</field>
76+
<field name="res_model">res.config.settings</field>
77+
<field name="view_id" ref="res_config_settings_dci_server_view_form" />
78+
<field name="view_mode">form</field>
79+
<field name="target">current</field>
80+
<field name="context">{'module': 'spp_dci_server'}</field>
81+
</record>
82+
83+
<menuitem
84+
id="menu_spp_dci_server_settings"
85+
name="Server Settings"
86+
parent="spp_dci_client.menu_spp_dci_config"
87+
sequence="5"
88+
action="res_config_settings_dci_server_action"
89+
groups="base.group_system"
90+
/>
91+
</odoo>

0 commit comments

Comments
 (0)