Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
25 changes: 24 additions & 1 deletion spp_dci_client/models/data_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class DCIDataSource(models.Model):
_description = "DCI Data Source"
_order = "name"

_SECRET_MASK = "********"

name = fields.Char(
required=True,
string="Name",
Expand Down Expand Up @@ -70,7 +72,15 @@ class DCIDataSource(models.Model):
oauth2_client_secret = fields.Char(
string="OAuth2 Client Secret",
groups="base.group_system",
help="OAuth2 client secret (visible only to system administrators)",
copy=False,
help="OAuth2 client secret (internal storage, not displayed in UI)",
)
oauth2_client_secret_display = fields.Char(
string="OAuth2 Client Secret",
compute="_compute_oauth2_client_secret_display",
inverse="_inverse_oauth2_client_secret_display",
groups="base.group_system",
help="OAuth2 client secret (write-only for security - value is masked after saving)",
)
oauth2_scope = fields.Char(
string="OAuth2 Scope",
Expand Down Expand Up @@ -177,6 +187,19 @@ class DCIDataSource(models.Model):
help="Cached token expiration timestamp (internal use only)",
)

@api.depends("oauth2_client_secret")
def _compute_oauth2_client_secret_display(self):
for record in self:
record.oauth2_client_secret_display = self._SECRET_MASK if record.oauth2_client_secret else False

def _inverse_oauth2_client_secret_display(self):
for record in self:
value = record.oauth2_client_secret_display
if value and value != self._SECRET_MASK:
record.oauth2_client_secret = value
elif not value:
record.oauth2_client_secret = False

@api.constrains("code")
def _check_code_unique(self):
"""Ensure code is unique across all data sources."""
Expand Down
86 changes: 86 additions & 0 deletions spp_dci_client/tests/test_data_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class TestDataSource(TransactionCase):
def setUpClass(cls):
super().setUpClass()
cls.DataSource = cls.env["spp.dci.data.source"]
cls.SECRET_MASK = cls.DataSource._SECRET_MASK

def test_create_data_source(self):
"""Test creating a data source with required fields"""
Expand Down Expand Up @@ -629,3 +630,88 @@ def test_default_values(self):
self.assertEqual(ds.search_endpoint, "/registry/sync/search")
self.assertEqual(ds.subscribe_endpoint, "/registry/subscribe")
self.assertEqual(ds.auth_endpoint, "/oauth2/client/token")

def test_secret_display_field_masks_value(self):
"""Test that oauth2_client_secret_display returns masked value"""
ds = self.DataSource.create(
{
"name": "Test CRVS",
"code": "test_crvs_mask",
"base_url": "https://crvs.example.org/api",
"auth_type": "oauth2",
"oauth2_token_url": "https://auth.example.org/token",
"oauth2_client_id": "client123",
"oauth2_client_secret": "secret456",
"our_sender_id": "openspp.example.org",
}
)
# Display field should show mask, stored field should have real value
self.assertEqual(ds.oauth2_client_secret_display, self.SECRET_MASK)
self.assertEqual(ds.oauth2_client_secret, "secret456")

def test_secret_display_field_empty_when_no_secret(self):
"""Test that oauth2_client_secret_display is empty when no secret is set"""
ds = self.DataSource.create(
{
"name": "Test CRVS",
"code": "test_crvs_empty",
"base_url": "https://crvs.example.org/api",
"auth_type": "none",
}
)
self.assertFalse(ds.oauth2_client_secret_display)
self.assertFalse(ds.oauth2_client_secret)

def test_secret_display_write_updates_stored_field(self):
"""Test that writing a new value through display field updates the stored secret"""
ds = self.DataSource.create(
{
"name": "Test CRVS",
"code": "test_crvs_write",
"base_url": "https://crvs.example.org/api",
"auth_type": "oauth2",
"oauth2_token_url": "https://auth.example.org/token",
"oauth2_client_id": "client123",
"oauth2_client_secret": "old_secret",
"our_sender_id": "openspp.example.org",
}
)
# Write a new secret through the display field
ds.write({"oauth2_client_secret_display": "brand_new_secret"})
self.assertEqual(ds.oauth2_client_secret, "brand_new_secret")
# Invalidate cache to force recomputation of the display field
ds.invalidate_recordset(["oauth2_client_secret_display"])
self.assertEqual(ds.oauth2_client_secret_display, self.SECRET_MASK)

def test_secret_display_mask_value_does_not_overwrite(self):
"""Test that writing the mask value does not overwrite the real secret"""
ds = self.DataSource.create(
{
"name": "Test CRVS",
"code": "test_crvs_nooverwrite",
"base_url": "https://crvs.example.org/api",
"auth_type": "oauth2",
"oauth2_token_url": "https://auth.example.org/token",
"oauth2_client_id": "client123",
"oauth2_client_secret": "real_secret_value",
"our_sender_id": "openspp.example.org",
}
)
# Writing the mask value should not change the stored secret
ds.write({"oauth2_client_secret_display": self.SECRET_MASK})
self.assertEqual(ds.oauth2_client_secret, "real_secret_value")

def test_secret_display_clear_removes_secret(self):
"""Test that clearing the display field removes the stored secret"""
ds = self.DataSource.create(
{
"name": "Test CRVS",
"code": "test_crvs_clear",
"base_url": "https://crvs.example.org/api",
"auth_type": "none",
"oauth2_client_secret": "secret_to_clear",
}
)
# Clear the secret through the display field
ds.write({"oauth2_client_secret_display": False})
self.assertFalse(ds.oauth2_client_secret)
Loading
Loading