Files
www/membership/members/service_groups.py
T
2026-10-29 14:49:44 -04:00

64 lines
3.1 KiB
Python
Executable File

"""Explicit, administrator-controlled service grants for each effective level."""
import re
from django.core.exceptions import ImproperlyConfigured
from ldap3.core.exceptions import LDAPInvalidDnError
from ldap3.utils.dn import parse_dn
def validate_service_groups(value, paid_levels):
if not isinstance(value, dict):
raise ImproperlyConfigured("Membership service groups must be an object mapping levels to lists of LDAP group DNs.")
valid_levels = {0, *paid_levels}
for level, groups in value.items():
if not isinstance(level, str) or not re.fullmatch(r"0|[1-9][0-9]*", level) or int(level) not in valid_levels:
raise ImproperlyConfigured("Service group levels must be 0 or a configured paid membership level.")
if not isinstance(groups, list):
raise ImproperlyConfigured("Each service group level must contain a list of LDAP group DNs.")
seen = set()
for dn in groups:
if not isinstance(dn, str) or not dn or dn != dn.strip() or len(dn) > 1024 or any(ord(char) < 32 for char in dn):
raise ImproperlyConfigured("Service group DNs must be nonempty valid LDAP DNs of at most 1024 characters.")
try:
parts = parse_dn(dn)
except LDAPInvalidDnError:
raise ImproperlyConfigured("Invalid LDAP service group DN.") from None
if not parts or dn.casefold() in seen:
raise ImproperlyConfigured("Service group DNs must be valid and unique within each level.")
seen.add(dn.casefold())
return value
def configured_groups(tiers, service_groups):
"""All managed targets, not only the groups this account currently earns."""
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