feat(users): passwordless user creation via emailed set-password link, display full name instead of username
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled

Admins now create users with a name + email (no password) and role;
a Send Email action (per-user, also reusable for resets) issues a
one-time token and POSTs it to an admin-configurable webhook URL —
intended for a Power Automate flow that delivers the actual email.
The old admin-sets-the-password flow is kept server-side
(LegacyUserCreateWithPassword) but no longer wired into the UI.

Every user-facing surface (top bar, sidebar, user management, audit
log, assignee pickers, dispute notifications) now shows full_name
instead of username, falling back to username when unset.

Also fixes long attack_procedure/expected_detection/suggested_text
text overflowing its container in the template review panels and
Red/Blue team fields (missing break-words on whitespace-pre-wrap
blocks).
This commit is contained in:
kitos
2026-07-17 16:58:25 +02:00
parent 60ab2e558f
commit 4dea19cae9
36 changed files with 974 additions and 159 deletions
+74 -33
View File
@@ -112,49 +112,57 @@ def _validate_password_strength(password: str) -> str:
# ── Create ──────────────────────────────────────────────────────────
class UserCreate(BaseModel):
"""Payload for creating a new user."""
"""Payload for creating a new user.
# username: str
username: str
# Assign email = None
email: str | None = None
# password: str
password: str
No password is set here — the admin creates the user with just a
name/email/role, then uses "Send Email" to let the user set their own
password via a one-time link. ``username`` is no longer admin-supplied;
the service layer derives it from ``email`` since it's still needed
internally as the login identifier, but it is never shown in the UI.
"""
full_name: str
email: str
# Assign role = "viewer"
role: str = "viewer"
# Apply the @field_validator decorator
@field_validator("username")
# Apply the @classmethod decorator
@field_validator("full_name")
@classmethod
def full_name_not_blank(cls, v: str) -> str:
v = v.strip()
if not v:
raise ValueError("Full name is required")
return v
@field_validator("email")
@classmethod
def email_format(cls, v: str) -> str:
v = v.strip()
if "@" not in v or v.startswith("@") or v.endswith("@"):
raise ValueError("A valid email address is required")
return v
# ── Legacy direct-password creation (hidden from the UI, kept for a
# possible future revert — see UserCreate above for the active path) ──
class LegacyUserCreateWithPassword(BaseModel):
"""The old admin-sets-the-password creation payload. Not wired to any
active endpoint; retained only so this path can be restored quickly."""
username: str
email: str | None = None
password: str
role: str = "viewer"
@field_validator("username")
@classmethod
# Define function username_format
def username_format(cls, v: str) -> str:
"""Validate the username field against the platform policy.
Args:
v (str): Raw username value from the request body.
Returns:
str: The validated username.
"""
# Return _validate_username(v)
return _validate_username(v)
# Apply the @field_validator decorator
@field_validator("password")
# Apply the @classmethod decorator
@classmethod
# Define function password_strength
def password_strength(cls, v: str) -> str:
"""Validate the password field against the complexity policy.
Args:
v (str): Raw password value from the request body.
Returns:
str: The validated password.
"""
# Return _validate_password_strength(v)
return _validate_password_strength(v)
@@ -163,11 +171,15 @@ class UserCreate(BaseModel):
class UserUpdate(BaseModel):
"""Payload for partially updating an existing user.
Every field is optional so callers send only what changed.
Every field is optional so callers send only what changed. ``password``
is intentionally still supported server-side (see
LegacyUserCreateWithPassword) but the current UI never sends it —
password changes go through the email-a-set-password-link flow instead.
"""
# Assign email = None
email: str | None = None
full_name: str | None = None
# Assign role = None
role: str | None = None
# Assign is_active = None
@@ -248,6 +260,7 @@ class UserOut(BaseModel):
id: uuid.UUID
# username: str
username: str
full_name: str | None = None
# Assign email = None
email: str | None = None
# role: str
@@ -291,6 +304,34 @@ class UserOperatorOut(BaseModel):
id: uuid.UUID
username: str
full_name: str | None = None
role: str
model_config = ConfigDict(from_attributes=True)
# ── Password setup / reset (via emailed one-time token) ─────────────
class SendPasswordEmailOut(BaseModel):
"""Response after an admin triggers a set-password email for a user."""
detail: str
class SetPasswordTokenValidateOut(BaseModel):
"""Response for checking a set-password token before rendering the form."""
valid: bool
full_name: str | None = None
class SetPasswordRequest(BaseModel):
"""Payload for completing a password setup/reset via a one-time token."""
token: str
new_password: str
@field_validator("new_password")
@classmethod
def new_password_strength(cls, v: str) -> str:
return _validate_password_strength(v)