Skip to content

Commit ae2fc6a

Browse files
authored
RedMesh: deployment branding readable without a session, for the sign-in page (#534)
* feat: serve the deployment branding to the sign-in page What changed: - Two endpoints with no actor, get_public_branding and download_public_branding_logo, answer the same theme/logo DTO and re-hashed logo bytes as the actor-bound reads. - The logo download logic is shared between the actor-bound and public endpoints. - Tests for the default, a set theme and logo, a tampered or lost file, and a missing namespace; the endpoint inventory grows by two. Why: - A browser with no session (the sign-in page, incognito) could not read the branding, so the sign-in page always showed the default theme and logo. Checks: - pytest red_mesh/tests: 6430 passed, 5 skipped. * chore: increment version
1 parent 21894b7 commit ae2fc6a

5 files changed

Lines changed: 72 additions & 3 deletions

File tree

‎extensions/business/cybersec/red_mesh/pentester_api_01.py‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4381,7 +4381,28 @@ def update_super_tenant_branding(self, actor: dict = None, changes: object = Non
43814381
@BasePlugin.endpoint(method="post")
43824382
def download_super_tenant_branding_logo(self, actor: dict = None):
43834383
"""The logo's bytes, re-hashed against the branding record, for every active account."""
4384-
logo = self._call_tenant_administration("super_tenant_branding_logo", actor)
4384+
return self._download_branding_logo(self._call_tenant_administration("super_tenant_branding_logo", actor))
4385+
4386+
@BasePlugin.endpoint(method="post")
4387+
def get_public_branding(self):
4388+
"""The deployment's theme and logo hash with no actor, for the sign-in page; the same DTO as
4389+
`get_super_tenant_branding`, never the logo's reference."""
4390+
try:
4391+
service = self._execution_service()
4392+
except TenantStoreError:
4393+
return administration_unavailable()
4394+
return service.public_super_tenant_branding()
4395+
4396+
@BasePlugin.endpoint(method="post")
4397+
def download_public_branding_logo(self):
4398+
"""The logo's bytes with no actor, for the sign-in page; re-hashed as the actor-bound download."""
4399+
try:
4400+
service = self._execution_service()
4401+
except TenantStoreError:
4402+
return administration_unavailable()
4403+
return self._download_branding_logo(service.public_super_tenant_branding_logo())
4404+
4405+
def _download_branding_logo(self, logo):
43854406
if not logo.get("success"):
43864407
return logo
43874408

‎extensions/business/cybersec/red_mesh/tenancy/administration.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1702,6 +1702,20 @@ def get_super_tenant_branding(self, actor):
17021702
self._actor(actor)
17031703
return super_tenant_branding.branding_dto(self._branding_row())
17041704

1705+
# The sign-in page shows the deployment's branding before anyone signs in, so these two take no
1706+
# actor. They answer only what the actor-bound reads answer: the DTO, and the logo's reference for
1707+
# the plugin's download.
1708+
@_endpoint
1709+
def public_super_tenant_branding(self):
1710+
return super_tenant_branding.branding_dto(self._branding_row())
1711+
1712+
@_endpoint
1713+
def public_super_tenant_branding_logo(self):
1714+
logo = self._branding_row()["logo"]
1715+
if logo is None:
1716+
raise AdministrationDenied(404, "not_found")
1717+
return logo
1718+
17051719
@_endpoint
17061720
def authorize_super_tenant_branding_update(self, actor):
17071721
"""Step 1 of a logo write: refused before the plugin stores any bytes."""

‎extensions/business/cybersec/red_mesh/tests/test_authz_surface.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from .conftest import mock_plugin_modules
77

88
TEST_CHANNEL_TOKEN = "test-model-token-material-at-least-32-bytes"
9-
EXPECTED_ENDPOINTS = 128 # +3 super-tenant branding (RM-134); +1 list_deployment_nodes (RM-126); +5 data maintenance (RM-108, temporary); +3 tenant contract (RM-095 phase 1); +6 engagements (RM-095 phase 2); +1 delete_tenant, -4 tenant assets (RM-107); +7 tenant drafts (RM-109 phase 2); +3 draft release/close and tenant document download (RM-109 phase 3); +8 engagement drafts (RM-109 phase 4); +3 super-tenant profile and generated pack (RM-110 phase 1); +6 tenant assets (RM-125)
9+
EXPECTED_ENDPOINTS = 130 # +2 public branding for the sign-in page; +3 super-tenant branding (RM-134); +1 list_deployment_nodes (RM-126); +5 data maintenance (RM-108, temporary); +3 tenant contract (RM-095 phase 1); +6 engagements (RM-095 phase 2); +1 delete_tenant, -4 tenant assets (RM-107); +7 tenant drafts (RM-109 phase 2); +3 draft release/close and tenant document download (RM-109 phase 3); +8 engagement drafts (RM-109 phase 4); +3 super-tenant profile and generated pack (RM-110 phase 1); +6 tenant assets (RM-125)
1010
TOKEN_ENDPOINTS = {"launch_model_test", "preflight_model_test_provider"}
1111
LAUNCH_ENDPOINTS = ("launch_network_scan", "launch_webapp_scan", "launch_test", "launch_model_test")
1212
# Pre-RM-075 public positional contract; account actor fields are appended to launches.
@@ -61,6 +61,9 @@
6161
'get_super_tenant_branding': 'actor',
6262
'update_super_tenant_branding': 'actor',
6363
'download_super_tenant_branding_logo': 'actor',
64+
# The sign-in page's branding, read before anyone signs in.
65+
'get_public_branding': None,
66+
'download_public_branding_logo': None,
6467
'store_generated_document': 'actor',
6568
'create_tenant_contract_draft': 'actor',
6669
'get_tenant_contract_draft': 'actor',

‎extensions/business/cybersec/red_mesh/tests/test_super_tenant_branding.py‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,37 @@ def test_no_namespace_means_unavailable_before_any_storage_access(self):
234234
self.assertEqual((self.store.writes, self.documents.puts), ([], []))
235235

236236

237+
class TestPublicBranding(_BrandingCase):
238+
"""The sign-in page's reads: no actor, the same answers as the actor-bound reads."""
239+
240+
def test_the_default_is_read_without_an_actor_or_a_write_and_no_logo_is_not_found(self):
241+
self.assertEqual(ok(self, self.plugin.get_public_branding()), EMPTY)
242+
refused(self, self.plugin.download_public_branding_logo(), 404, "not_found")
243+
self.assertEqual((self.store.writes, self.events), ([], []))
244+
245+
def test_a_set_theme_and_logo_read_as_the_actor_bound_reads_do(self):
246+
raw = png()
247+
data = ok(self, self.update({"logo": upload(raw), "theme": "light"}))
248+
self.assertEqual(ok(self, self.plugin.get_public_branding()), data)
249+
got = ok(self, self.plugin.download_public_branding_logo())
250+
self.assertEqual(got, ok(self, self.download()))
251+
self.assertEqual(base64.b64decode(got["content_b64"]), raw)
252+
253+
def test_a_tampered_file_answers_integrity_and_a_lost_one_not_found(self):
254+
ok(self, self.update({"logo": upload(png())}))
255+
self.documents.envelopes["doc-1"]["content_b64"] = base64.b64encode(png(size=4096)).decode("ascii")
256+
refused(self, self.plugin.download_public_branding_logo(), 409, "contract_integrity")
257+
del self.documents.envelopes["doc-1"]
258+
refused(self, self.plugin.download_public_branding_logo(), 404, "not_found")
259+
260+
def test_no_namespace_means_unavailable(self):
261+
self.plugin.cfg_tenancy_namespace = None
262+
for name in ("get_public_branding", "download_public_branding_logo"):
263+
with self.subTest(name):
264+
self.assertEqual(getattr(self.Plugin, name).__http_method__, "post")
265+
self.assertEqual(getattr(self.plugin, name)()["status_code"], 503)
266+
267+
237268
class TestBrandingStoredRow(unittest.TestCase):
238269
def test_the_validator_refuses_rows_the_operations_could_not_write(self):
239270
from extensions.business.cybersec.red_mesh.tenancy.super_tenant_branding import validate_super_tenant_branding

‎ver.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__VER__ = '2.10.490'
1+
__VER__ = '2.10.491'

0 commit comments

Comments
 (0)