Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion spp_oauth/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "OpenSPP API: Oauth",
"summary": "The module establishes an OAuth 2.0 authentication framework, securing OpenSPP API communication for integrated systems and applications.",
"category": "OpenSPP",
"version": "19.0.2.0.0",
"version": "19.0.2.0.1",
"author": "OpenSPP.org",
"development_status": "Production/Stable",
"maintainers": ["jeremi", "gonzalesedwin1123", "reichie020212"],
Expand Down
16 changes: 16 additions & 0 deletions spp_oauth/models/res_config_settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from odoo import fields, models

# The OAuth signing keys are only meaningful to system administrators. Access to
# res.config.settings is restricted to base.group_system via Odoo core's ACL (this
# module no longer widens it), but default_get() reads config_parameter fields with
# sudo() and performs no access check, so it is guarded explicitly below.
OAUTH_KEY_FIELDS = ("oauth_priv_key", "oauth_pub_key")


class RegistryConfig(models.TransientModel):
_inherit = "res.config.settings"
Expand All @@ -12,3 +18,13 @@ class RegistryConfig(models.TransientModel):
string="OAuth Public Key",
config_parameter="spp_oauth.oauth_pub_key",
)

def default_get(self, fields_list):
res = super().default_get(fields_list)
# default_get sources config_parameter values via sudo(), bypassing model
# and field access checks. Never expose the OAuth signing keys to a user
# who is not a system administrator.
if not self.env.user.has_group("base.group_system"):
Comment thread
gonzalesedwin1123 marked this conversation as resolved.
Outdated
for field_name in OAUTH_KEY_FIELDS:
res.pop(field_name, None)
return res
1 change: 0 additions & 1 deletion spp_oauth/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_res_config_settings_spp_oauth_user,res.config.settings spp_oauth user,base.model_res_config_settings,base.group_user,1,1,0,0
1 change: 1 addition & 0 deletions spp_oauth/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from . import test_rsa_encode_decode
from . import test_oauth_errors
from . import test_config_settings_acl
76 changes: 76 additions & 0 deletions spp_oauth/tests/test_config_settings_acl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
"""Security: OAuth signing keys must be restricted to system administrators.

Regression test for "OAuth signing keys exposed to all internal users": the
module granted ``base.group_user`` read/write on ``res.config.settings``, which
exposes ``oauth_priv_key`` (the RS256 JWT signing key) and ``oauth_pub_key``.
Any internal user could therefore:
- read the model directly (``read``/``search``) via RPC, and
- retrieve the keys through ``res.config.settings.default_get()``, which reads
the backing ``ir.config_parameter`` values with ``sudo()`` and performs no
model-ACL or field-group check.

Access must require a system administrator (``base.group_system``), and the
signing keys must not leak through ``default_get`` to non-admins.
"""

from odoo import Command
from odoo.exceptions import AccessError
from odoo.tests import TransactionCase, tagged

PRIV_KEY_SENTINEL = "TEST-OAUTH-PRIVATE-KEY-DO-NOT-LEAK"
PUB_KEY_SENTINEL = "TEST-OAUTH-PUBLIC-KEY"


@tagged("post_install", "-at_install")
class TestOAuthConfigSettingsAcl(TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
icp = cls.env["ir.config_parameter"].sudo()
icp.set_param("spp_oauth.oauth_priv_key", PRIV_KEY_SENTINEL)
icp.set_param("spp_oauth.oauth_pub_key", PUB_KEY_SENTINEL)

# A plain internal user: base.group_user only, no system access.
cls.plain_user = cls.env["res.users"].create(
{
"name": "Plain Internal User",
"login": "plain_internal_oauth_test",
"group_ids": [Command.link(cls.env.ref("base.group_user").id)],
}
)
# A system administrator.
cls.admin_user = cls.env["res.users"].create(
{
"name": "System Admin User",
"login": "system_admin_oauth_test",
"group_ids": [Command.link(cls.env.ref("base.group_system").id)],
}
)

def test_plain_user_cannot_access_settings_model(self):
"""base.group_user (any internal user) must NOT have read access to
res.config.settings — that is the model exposing the signing keys."""
with self.assertRaises(AccessError):
self.env["res.config.settings"].with_user(self.plain_user).check_access("read")

def test_plain_user_cannot_read_keys_via_default_get(self):
"""The signing keys must not leak to a non-admin through default_get(),
which reads the backing config parameters with sudo()."""
settings = self.env["res.config.settings"].with_user(self.plain_user)
values = settings.default_get(["oauth_priv_key", "oauth_pub_key"])
self.assertNotIn("oauth_priv_key", values)
self.assertNotIn("oauth_pub_key", values)

def test_admin_can_access_settings_model(self):
"""A system administrator retains read access to res.config.settings."""
# Raises AccessError only if admin access was wrongly removed.
self.env["res.config.settings"].with_user(self.admin_user).check_access("read")

def test_admin_can_read_keys_via_default_get(self):
"""A system administrator can still read the signing keys, so the
Settings UI keeps working for admins."""
settings = self.env["res.config.settings"].with_user(self.admin_user)
values = settings.default_get(["oauth_priv_key", "oauth_pub_key"])
self.assertEqual(values.get("oauth_priv_key"), PRIV_KEY_SENTINEL)
self.assertEqual(values.get("oauth_pub_key"), PUB_KEY_SENTINEL)
Loading