From dedbeae10f8354e18a2d6af297aaec8206a0d370 Mon Sep 17 00:00:00 2001 From: JoaquinBN Date: Thu, 4 Jun 2026 10:22:58 +0200 Subject: [PATCH 1/2] Hide non-visible operators from public validator endpoints The public Wall of Shame and validator wallet endpoints surfaced the linked operator's profile identity (name, address, id, avatar) even for users who set their profile to non-visible, which contradicted the rest of the API where hidden users are not enumerable. Operator identity is now withheld whenever the linked user is not visible; the validator still appears, identified only by its on-chain operator address, so a misbehaving node cannot disappear by toggling visibility. Two production safeguards are also tightened: session and CSRF cookies are marked Secure outside DEBUG, and the wallet login endpoint no longer echoes raw exception details to clients in production. ## Claude Implementation Notes - backend/validators/serializers.py: Gate operator_user on user.visible in ValidatorWalletSerializer and WallOfShameSerializer. - backend/validators/views.py: Gate the grouped Wall of Shame _operator_user_payload on user.visible. - backend/tally/settings.py: Set SESSION_COOKIE_SECURE and CSRF_COOKIE_SECURE to not DEBUG so cookies are HTTPS-only in production. - backend/ethereum_auth/views.py: Log the exception and return a generic auth-failure message when DEBUG is off; keep full detail in DEBUG. - backend/validators/tests/test_grafana_service.py: Add regression test that a non-visible operator's identity is withheld while the validator still appears. --- backend/ethereum_auth/views.py | 5 ++- backend/tally/settings.py | 5 +++ backend/validators/serializers.py | 4 +- .../validators/tests/test_grafana_service.py | 40 +++++++++++++++++++ backend/validators/views.py | 2 +- 5 files changed, 52 insertions(+), 4 deletions(-) diff --git a/backend/ethereum_auth/views.py b/backend/ethereum_auth/views.py index a5b2a6f8..32f91621 100644 --- a/backend/ethereum_auth/views.py +++ b/backend/ethereum_auth/views.py @@ -2,6 +2,7 @@ import string from datetime import timedelta +from django.conf import settings from django.contrib.auth import get_user_model from django.utils import timezone from rest_framework import status @@ -170,8 +171,10 @@ def login(request): }) except Exception as e: + logger.exception("Authentication failed") + error_detail = f'Authentication failed: {str(e)}' if settings.DEBUG else 'Authentication failed.' return Response( - {'error': f'Authentication failed: {str(e)}'}, + {'error': error_detail}, status=status.HTTP_400_BAD_REQUEST ) diff --git a/backend/tally/settings.py b/backend/tally/settings.py index a3559e62..1df75c07 100644 --- a/backend/tally/settings.py +++ b/backend/tally/settings.py @@ -287,6 +287,11 @@ def get_required_env(key): SESSION_COOKIE_DOMAIN = None # Allow cookies on localhost SESSION_SAVE_EVERY_REQUEST = True # Save session on every request to extend expiry +# Only send the session/CSRF cookies over HTTPS in production. In DEBUG the +# dev server runs over plain HTTP, so keep these off locally. +SESSION_COOKIE_SECURE = not DEBUG +CSRF_COOKIE_SECURE = not DEBUG + # Dynamic session cookie name based on port to prevent conflicts in multi-port development # This allows running multiple instances on different ports without session conflicts import sys diff --git a/backend/validators/serializers.py b/backend/validators/serializers.py index 1157d20f..b8d5a39e 100644 --- a/backend/validators/serializers.py +++ b/backend/validators/serializers.py @@ -31,7 +31,7 @@ class Meta: read_only_fields = ['id', 'created_at', 'updated_at'] def get_operator_user(self, obj): - if obj.operator and obj.operator.user: + if obj.operator and obj.operator.user and obj.operator.user.visible: user = obj.operator.user return { 'id': user.id, @@ -89,7 +89,7 @@ class Meta: read_only_fields = fields def get_operator_user(self, obj): - if obj.operator and obj.operator.user: + if obj.operator and obj.operator.user and obj.operator.user.visible: user = obj.operator.user return { 'id': user.id, diff --git a/backend/validators/tests/test_grafana_service.py b/backend/validators/tests/test_grafana_service.py index 41220077..522c01ce 100644 --- a/backend/validators/tests/test_grafana_service.py +++ b/backend/validators/tests/test_grafana_service.py @@ -403,6 +403,46 @@ def test_grouped_validator_reasons_across_networks(self): {('asimov', 'metrics'), ('bradbury', 'logs')}, ) + def test_hidden_operator_identity_is_not_exposed(self): + """A hidden (visible=False) operator must not have their profile + identity surfaced on the public Wall of Shame, but the validator wallet + itself should still appear (falling back to the on-chain address).""" + user = User.objects.create_user( + email='hidden-operator@example.com', + password='password', + name='Hidden Operator', + address='0xhiddenoperator00000000000000000000000000'[:42], + visible=False, + ) + validator = Validator.objects.create(user=user) + wallet = ValidatorWallet.objects.create( + address='0xhiddenwallet0000000000000000000000000000'[:42], + network='asimov', + operator=validator, + operator_address=user.address, + status='active', + moniker='hidden-asimov', + metrics_status='shame', + logs_status='on', + ) + + response = self.client.get('/api/v1/validators/wallets/wall-of-shame/') + + # operator_user must be withheld in both payloads for the hidden user. + for entry in response.data['wallets']: + if entry['address'] == wallet.address: + self.assertIsNone(entry['operator_user']) + + group = next( + item for item in response.data['validators'] + if item['operator_address'] == user.address + ) + self.assertIsNone(group['operator_user']) + # The validator still appears (so a misbehaving node isn't hidden), + # identified only by its on-chain operator address. + self.assertEqual(group['status'], 'shame') + self.assertEqual(group['operator_address'], user.address) + def test_outdated_version_is_warning_during_grace_period(self): TargetNodeVersion.objects.create( version='2.0.0', diff --git a/backend/validators/views.py b/backend/validators/views.py index d5417ec1..763c8612 100644 --- a/backend/validators/views.py +++ b/backend/validators/views.py @@ -624,7 +624,7 @@ def _days_since(started_at, now): @staticmethod def _operator_user_payload(wallet): - if wallet.operator and wallet.operator.user: + if wallet.operator and wallet.operator.user and wallet.operator.user.visible: user = wallet.operator.user return { 'id': user.id, From 5ceda15393c06f37ec6b5271828c94c9182c4f5d Mon Sep 17 00:00:00 2001 From: JoaquinBN Date: Thu, 4 Jun 2026 10:23:06 +0200 Subject: [PATCH 2/2] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7a6247c..1a46fb96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable user-facing changes to this project will be documented in this file. ## Unreleased +- The public Wall of Shame and validator wallet endpoints no longer reveal the profile identity (name, avatar, profile link) of operators who set their account to non-visible; the validator still appears, identified only by its on-chain operator address (dedbeae) - New Wall of Shame page under Validators publicly flags every active validator that isn't reporting metrics or logs in the last five minutes. The page lists all active validators (SHAME rows first) with their moniker, addresses, operator profile link, network badge, and binary ON/SHAME badges for both metrics and logs. Backed by a five-minute cron that cross-references on-chain active validators against our observability stack (98d083e) - Stewards reviewing submissions can now copy a compact AI-ready bundle (user, contribution, mission, state, submitter notes, evidence URLs, staff reply, proposal, and internal CRM notes) from a copy icon next to each submission's title; the copy aborts with a warning if internal notes can't be loaded so the clipboard payload is never silently incomplete. Evidence URL types gain an admin-editable "Allow duplicate" flag that exempts URLs of that type (for example shared GitHub repositories) from duplicate detection across both manual and automated review (ac68ec7) - Contributions explorer now shows highlighted contributions again, the page footer is flush to the bottom and CTAs users to submit a contribution, and the highlights section collapses to a single compact line when empty so contributions get more room. The Submit Contribution form no longer shows misleading "Please add a description" errors for evidence URLs, detects URL types correctly when users omit `https://`, and disables the submit button when a required GitHub or X URL has no linked social account (b249787)