UI
This commit is contained in:
@@ -35,3 +35,29 @@ def configured_groups(tiers, service_groups):
|
||||
return {tier["group_dn"] for tier in tiers.values()} | {
|
||||
dn for groups in service_groups.values() for dn in groups
|
||||
}
|
||||
|
||||
|
||||
def service_names(level, service_groups):
|
||||
"""Public labels from the same exact-level mapping used by reconciliation."""
|
||||
labels = {
|
||||
"dex users": "Dex · single sign-on",
|
||||
"discourse users": "Discourse · community forums",
|
||||
"gitea users": "Gitea · Git hosting",
|
||||
"matrix users": "Matrix · community chat",
|
||||
"mastodon users": "Mastodon · social networking",
|
||||
"peertube users": "PeerTube · video sharing",
|
||||
}
|
||||
names = []
|
||||
for dn in service_groups.get(str(level), []):
|
||||
# Show only a group's common name, never its directory hierarchy.
|
||||
common_name = next((value for attribute, value, _separator in parse_dn(dn) if attribute.casefold() == "cn"), "")
|
||||
if not common_name:
|
||||
name = "Additional community service"
|
||||
else:
|
||||
# LDAP DN values escape reserved characters and UTF-8 bytes.
|
||||
raw = re.sub(r"\\([,=+<>#;\"\\ ])", r"\1", common_name)
|
||||
raw = re.sub(r"(?:\\[0-9a-fA-F]{2})+", lambda match: bytes.fromhex(match.group().replace("\\", "")).decode("utf-8", errors="replace"), raw)
|
||||
name = labels.get(raw.casefold(), re.sub(r"\s+users$", "", raw, flags=re.IGNORECASE))
|
||||
if name not in names:
|
||||
names.append(name)
|
||||
return names
|
||||
|
||||
@@ -15,7 +15,7 @@ from django.utils import timezone
|
||||
|
||||
from members import billing, views
|
||||
from members.models import Account, CryptoInvoice, ManagedGroup, Subscription
|
||||
from members.service_groups import configured_groups, validate_service_groups
|
||||
from members.service_groups import configured_groups, service_names, validate_service_groups
|
||||
from ldap3 import Connection, MOCK_SYNC, Server
|
||||
|
||||
BASE = "ou=groups,dc=example,dc=org"
|
||||
@@ -79,6 +79,42 @@ class ServiceGroupLDAPIntegrationTests(TestCase):
|
||||
self.assertTrue(ManagedGroup.objects.filter(account=member, dn=FREE_GROUP).exists())
|
||||
|
||||
|
||||
@override_settings(MEMBERSHIP_TIERS=TIERS)
|
||||
class ServiceDisplayTests(TestCase):
|
||||
@override_settings(MEMBERSHIP_SERVICE_GROUPS={
|
||||
"0": ["cn=Dex Users," + BASE, "cn=Matrix Users," + BASE],
|
||||
"1": ["cn=Gitea Users," + BASE],
|
||||
"2": ["cn=Mastodon Users," + BASE, "cn=PeerTube Users," + BASE],
|
||||
})
|
||||
def test_home_displays_exact_level_services_without_ldap_or_settings_mutation(self):
|
||||
with patch("members.directory.connection", side_effect=AssertionError("Public page must not contact LDAP")):
|
||||
response = self.client.get("/members/")
|
||||
self.assertContains(response, "Dex · single sign-on")
|
||||
self.assertEqual(response.context["free_services"], ["Dex · single sign-on", "Matrix · community chat"])
|
||||
self.assertEqual(response.context["tiers"]["supporter"]["services"], ["Gitea · Git hosting"])
|
||||
self.assertEqual(response.context["tiers"]["patron"]["services"], ["Mastodon · social networking", "PeerTube · video sharing"])
|
||||
self.assertEqual(response.context["tiers"]["premier"]["services"], [])
|
||||
self.assertNotContains(response, BASE)
|
||||
self.assertNotIn("services", TIERS["supporter"])
|
||||
|
||||
@override_settings(MEMBERSHIP_SERVICE_GROUPS={})
|
||||
def test_unconfigured_services_do_not_advertise_access(self):
|
||||
response = self.client.get("/members/")
|
||||
self.assertContains(response, "No additional service access is currently configured.")
|
||||
self.assertNotContains(response, "Mastodon · social networking")
|
||||
|
||||
@override_settings(MEMBERSHIP_SERVICE_GROUPS={"0": [r"cn=\<script\>alert(1)\</script\>," + BASE]})
|
||||
def test_group_labels_are_html_escaped(self):
|
||||
response = self.client.get("/members/")
|
||||
self.assertContains(response, "<script>alert(1)</script>")
|
||||
self.assertNotContains(response, "<script>alert(1)</script>")
|
||||
|
||||
def test_custom_labels_decode_dn_escapes_and_hide_hierarchy(self):
|
||||
self.assertEqual(service_names(0, {"0": [r"cn=Games\, Friends Users," + BASE]}), ["Games, Friends"])
|
||||
self.assertEqual(service_names(0, {"0": [r"cn=Caf\c3\a9 Users," + BASE]}), ["Café"])
|
||||
self.assertEqual(service_names(0, {"0": ["ou=services," + BASE]}), ["Additional community service"])
|
||||
|
||||
|
||||
class ServiceGroupValidationTests(SimpleTestCase):
|
||||
def test_valid_mapping_is_returned_unchanged(self):
|
||||
self.assertIs(validate_service_groups(SERVICE_GROUPS, {1, 2, 10}), SERVICE_GROUPS)
|
||||
|
||||
@@ -26,7 +26,7 @@ from django.views.decorators.http import require_GET, require_POST, require_http
|
||||
|
||||
from . import billing, directory, profiles, shkeeper
|
||||
from .forms import PasswordForm, RegistrationForm
|
||||
from .service_groups import configured_groups
|
||||
from .service_groups import configured_groups, service_names
|
||||
from .profile_forms import CurrentPasswordForm, ProfileEmailForm, ProfileForm, ProfilePasswordForm
|
||||
from .models import Account, CryptoInvoice, ManagedGroup, ProfileEmailChange, RateWindow, Registration, Subscription, WebhookEvent
|
||||
|
||||
@@ -76,7 +76,9 @@ def home(request):
|
||||
account = Account.objects.filter(pk=request.session.get("account_id")).first() if request.session.get("account_id") else None
|
||||
subscriptions = Subscription.objects.filter(account=account) if account else []
|
||||
invoices = CryptoInvoice.objects.filter(account=account).order_by("-created_at")[:20] if account else []
|
||||
return page(request, account=account, tiers=settings.MEMBERSHIP_TIERS, subscriptions=subscriptions, crypto_invoices=invoices, shkeeper_enabled=settings.SHKEEPER_ENABLED, now=timezone.now())
|
||||
# Presentation copies only: never mutate authorization/payment settings.
|
||||
tiers = {key: {**tier, "services": service_names(tier["level"], settings.MEMBERSHIP_SERVICE_GROUPS)} for key, tier in settings.MEMBERSHIP_TIERS.items()}
|
||||
return page(request, account=account, tiers=tiers, free_services=service_names(0, settings.MEMBERSHIP_SERVICE_GROUPS), subscriptions=subscriptions, crypto_invoices=invoices, shkeeper_enabled=settings.SHKEEPER_ENABLED, now=timezone.now())
|
||||
|
||||
|
||||
@require_http_methods(["GET", "POST"])
|
||||
|
||||
Reference in New Issue
Block a user