Skip to content

Commit 5a2ee2f

Browse files
Merge pull request #27 from OpenSPP/spp_dci_client_mask_oauth_client_secret
feat(spp_dci_client): mask OAuth2 client secret in UI
2 parents 535bbd1 + 53f28a7 commit 5a2ee2f

File tree

3 files changed

+277
-38
lines changed

3 files changed

+277
-38
lines changed

spp_dci_client/models/data_source.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class DCIDataSource(models.Model):
2222
_description = "DCI Data Source"
2323
_order = "name"
2424

25+
_SECRET_MASK = "********"
26+
2527
name = fields.Char(
2628
required=True,
2729
string="Name",
@@ -70,7 +72,15 @@ class DCIDataSource(models.Model):
7072
oauth2_client_secret = fields.Char(
7173
string="OAuth2 Client Secret",
7274
groups="base.group_system",
73-
help="OAuth2 client secret (visible only to system administrators)",
75+
copy=False,
76+
help="OAuth2 client secret (internal storage, not displayed in UI)",
77+
)
78+
oauth2_client_secret_display = fields.Char(
79+
string="OAuth2 Client Secret",
80+
compute="_compute_oauth2_client_secret_display",
81+
inverse="_inverse_oauth2_client_secret_display",
82+
groups="base.group_system",
83+
help="OAuth2 client secret (write-only for security - value is masked after saving)",
7484
)
7585
oauth2_scope = fields.Char(
7686
string="OAuth2 Scope",
@@ -177,6 +187,19 @@ class DCIDataSource(models.Model):
177187
help="Cached token expiration timestamp (internal use only)",
178188
)
179189

190+
@api.depends("oauth2_client_secret")
191+
def _compute_oauth2_client_secret_display(self):
192+
for record in self:
193+
record.oauth2_client_secret_display = self._SECRET_MASK if record.oauth2_client_secret else False
194+
195+
def _inverse_oauth2_client_secret_display(self):
196+
for record in self:
197+
value = record.oauth2_client_secret_display
198+
if value and value != self._SECRET_MASK:
199+
record.oauth2_client_secret = value
200+
elif not value:
201+
record.oauth2_client_secret = False
202+
180203
@api.constrains("code")
181204
def _check_code_unique(self):
182205
"""Ensure code is unique across all data sources."""

spp_dci_client/tests/test_data_source.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class TestDataSource(TransactionCase):
1414
def setUpClass(cls):
1515
super().setUpClass()
1616
cls.DataSource = cls.env["spp.dci.data.source"]
17+
cls.SECRET_MASK = cls.DataSource._SECRET_MASK
1718

1819
def test_create_data_source(self):
1920
"""Test creating a data source with required fields"""
@@ -629,3 +630,88 @@ def test_default_values(self):
629630
self.assertEqual(ds.search_endpoint, "/registry/sync/search")
630631
self.assertEqual(ds.subscribe_endpoint, "/registry/subscribe")
631632
self.assertEqual(ds.auth_endpoint, "/oauth2/client/token")
633+
634+
def test_secret_display_field_masks_value(self):
635+
"""Test that oauth2_client_secret_display returns masked value"""
636+
ds = self.DataSource.create(
637+
{
638+
"name": "Test CRVS",
639+
"code": "test_crvs_mask",
640+
"base_url": "https://crvs.example.org/api",
641+
"auth_type": "oauth2",
642+
"oauth2_token_url": "https://auth.example.org/token",
643+
"oauth2_client_id": "client123",
644+
"oauth2_client_secret": "secret456",
645+
"our_sender_id": "openspp.example.org",
646+
}
647+
)
648+
# Display field should show mask, stored field should have real value
649+
self.assertEqual(ds.oauth2_client_secret_display, self.SECRET_MASK)
650+
self.assertEqual(ds.oauth2_client_secret, "secret456")
651+
652+
def test_secret_display_field_empty_when_no_secret(self):
653+
"""Test that oauth2_client_secret_display is empty when no secret is set"""
654+
ds = self.DataSource.create(
655+
{
656+
"name": "Test CRVS",
657+
"code": "test_crvs_empty",
658+
"base_url": "https://crvs.example.org/api",
659+
"auth_type": "none",
660+
}
661+
)
662+
self.assertFalse(ds.oauth2_client_secret_display)
663+
self.assertFalse(ds.oauth2_client_secret)
664+
665+
def test_secret_display_write_updates_stored_field(self):
666+
"""Test that writing a new value through display field updates the stored secret"""
667+
ds = self.DataSource.create(
668+
{
669+
"name": "Test CRVS",
670+
"code": "test_crvs_write",
671+
"base_url": "https://crvs.example.org/api",
672+
"auth_type": "oauth2",
673+
"oauth2_token_url": "https://auth.example.org/token",
674+
"oauth2_client_id": "client123",
675+
"oauth2_client_secret": "old_secret",
676+
"our_sender_id": "openspp.example.org",
677+
}
678+
)
679+
# Write a new secret through the display field
680+
ds.write({"oauth2_client_secret_display": "brand_new_secret"})
681+
self.assertEqual(ds.oauth2_client_secret, "brand_new_secret")
682+
# Invalidate cache to force recomputation of the display field
683+
ds.invalidate_recordset(["oauth2_client_secret_display"])
684+
self.assertEqual(ds.oauth2_client_secret_display, self.SECRET_MASK)
685+
686+
def test_secret_display_mask_value_does_not_overwrite(self):
687+
"""Test that writing the mask value does not overwrite the real secret"""
688+
ds = self.DataSource.create(
689+
{
690+
"name": "Test CRVS",
691+
"code": "test_crvs_nooverwrite",
692+
"base_url": "https://crvs.example.org/api",
693+
"auth_type": "oauth2",
694+
"oauth2_token_url": "https://auth.example.org/token",
695+
"oauth2_client_id": "client123",
696+
"oauth2_client_secret": "real_secret_value",
697+
"our_sender_id": "openspp.example.org",
698+
}
699+
)
700+
# Writing the mask value should not change the stored secret
701+
ds.write({"oauth2_client_secret_display": self.SECRET_MASK})
702+
self.assertEqual(ds.oauth2_client_secret, "real_secret_value")
703+
704+
def test_secret_display_clear_removes_secret(self):
705+
"""Test that clearing the display field removes the stored secret"""
706+
ds = self.DataSource.create(
707+
{
708+
"name": "Test CRVS",
709+
"code": "test_crvs_clear",
710+
"base_url": "https://crvs.example.org/api",
711+
"auth_type": "none",
712+
"oauth2_client_secret": "secret_to_clear",
713+
}
714+
)
715+
# Clear the secret through the display field
716+
ds.write({"oauth2_client_secret_display": False})
717+
self.assertFalse(ds.oauth2_client_secret)

0 commit comments

Comments
 (0)