The Dex LDAP connector publishes uid, cn and mail as claims, so the profile page now offers just Name (cn), the verified email change and the password. The photo endpoint, SSH keys, addresses, phones and other extended attributes are no longer read or written; existing LDAP values are left untouched. Apache's profile upload limit drops back to the 256 KiB used by other member routes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
114 lines
4.3 KiB
Python
Executable File
114 lines
4.3 KiB
Python
Executable File
"""Validation-only forms for the shared LDAP profile; no directory writes."""
|
|
|
|
import unicodedata
|
|
|
|
from django import forms
|
|
from django.core.exceptions import ValidationError
|
|
|
|
from .forms import PasswordForm
|
|
|
|
|
|
MAX_INPUT_LENGTH = 32768
|
|
MAX_VALUES = 16
|
|
|
|
|
|
def _safe_text(value: str) -> str:
|
|
# Normalize browser line endings before removing unsafe control characters.
|
|
value = value.replace("\r\n", "\n").replace("\r", "\n").replace("\t", " ")
|
|
return "".join(
|
|
char for char in value
|
|
if char == "\n" or unicodedata.category(char) not in {"Cc", "Cf", "Cs"}
|
|
)
|
|
|
|
|
|
class ProfileValuesField(forms.CharField):
|
|
"""Text input becomes an ordered, deduplicated list of LDAP strings."""
|
|
|
|
def __init__(self, *, value_length: int = 2048, max_values: int = MAX_VALUES, **kwargs):
|
|
self.value_length = value_length
|
|
self.max_values = max_values
|
|
kwargs.setdefault("required", False)
|
|
kwargs.setdefault("help_text", "One value per line. Up to 16 values; leave blank to clear.")
|
|
super().__init__(
|
|
max_length=MAX_INPUT_LENGTH,
|
|
widget=forms.Textarea(attrs={"rows": 3, "maxlength": MAX_INPUT_LENGTH}),
|
|
**kwargs,
|
|
)
|
|
|
|
def clean(self, value) -> list[str]:
|
|
if value is not None and len(str(value)) > MAX_INPUT_LENGTH:
|
|
raise ValidationError("Enter no more than 32,768 characters in this field.")
|
|
text = super().clean(_safe_text(str(value)) if value is not None else "")
|
|
entries = [line.strip() for line in text.split("\n") if line.strip()]
|
|
if len(entries) > self.max_values:
|
|
raise ValidationError(f"Enter no more than {self.max_values} value(s).")
|
|
if any(len(entry) > self.value_length for entry in entries):
|
|
raise ValidationError(f"Each value must be at most {self.value_length:,} characters.")
|
|
if self.required and not entries:
|
|
raise ValidationError("Enter at least one value.")
|
|
return list(dict.fromkeys(entries))
|
|
|
|
|
|
def _current_password_field() -> forms.CharField:
|
|
return forms.CharField(
|
|
label="Current password", max_length=1024, strip=False,
|
|
widget=forms.PasswordInput(
|
|
render_value=False, attrs={"autocomplete": "current-password"}
|
|
),
|
|
help_text="Enter your current eom.dev password to authorize this change.",
|
|
)
|
|
|
|
|
|
class CurrentPasswordForm(forms.Form):
|
|
current_password = _current_password_field()
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
kwargs.setdefault("auto_id", "id_confirmation_%s")
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
class ProfileForm(CurrentPasswordForm):
|
|
"""Only Dex-visible values: cn is published to services as the "name" claim."""
|
|
|
|
cn = ProfileValuesField(
|
|
label="Name", required=True, max_values=1, value_length=256,
|
|
help_text="How you appear across eom.dev services.",
|
|
)
|
|
field_order = ["cn", "current_password"]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
kwargs.setdefault("auto_id", "id_profile_%s")
|
|
super().__init__(*args, **kwargs)
|
|
self.fields["cn"].widget = forms.TextInput(attrs={"maxlength": 256, "autocomplete": "name"})
|
|
|
|
def identity_fields(self):
|
|
return [self["cn"]]
|
|
|
|
def authorization_fields(self):
|
|
return [self["current_password"]]
|
|
|
|
|
|
class ProfileEmailForm(CurrentPasswordForm):
|
|
email = forms.EmailField(label="New email address", max_length=254, widget=forms.EmailInput(attrs={"autocomplete": "email"}))
|
|
field_order = ["email", "current_password"]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
kwargs.setdefault("auto_id", "id_email_%s")
|
|
super().__init__(*args, **kwargs)
|
|
|
|
def clean_email(self) -> str:
|
|
return self.cleaned_data["email"].casefold()
|
|
|
|
|
|
class ProfilePasswordForm(PasswordForm, CurrentPasswordForm):
|
|
field_order = ["current_password", "password", "confirm_password"]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
kwargs.setdefault("auto_id", "id_password_%s")
|
|
super().__init__(*args, **kwargs)
|
|
for name in ("password", "confirm_password"):
|
|
self.fields[name].strip = False
|
|
self.fields[name].widget = forms.PasswordInput(
|
|
render_value=False, attrs={"autocomplete": "new-password"}
|
|
)
|