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
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:
+74
-33
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user