Bind password setup grants to approved company welcome pages
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
This commit is contained in:
parent
c8ad7a85ea
commit
48a75b1a54
7 changed files with 267 additions and 8 deletions
|
|
@ -12,13 +12,14 @@ import threading
|
|||
import time
|
||||
from typing import Callable
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.parse import urlencode
|
||||
from urllib.parse import urlencode, urlsplit
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SetupGrant:
|
||||
subject: str
|
||||
expires_at: float
|
||||
return_to: str = ""
|
||||
|
||||
|
||||
class PasswordSetupGrants:
|
||||
|
|
@ -34,9 +35,18 @@ class PasswordSetupGrants:
|
|||
*,
|
||||
public_url: str,
|
||||
setter: Callable[[str, str], None],
|
||||
tenant_returns: dict[str, str] | None = None,
|
||||
ttl_seconds: int = 900,
|
||||
clock: Callable[[], float] = time.monotonic,
|
||||
) -> None:
|
||||
self.tenant_returns = dict(tenant_returns or {})
|
||||
for tenant, target in self.tenant_returns.items():
|
||||
parts = urlsplit(target)
|
||||
if (not tenant.startswith("tenant:") or parts.scheme != "https"
|
||||
or not parts.hostname or parts.username or parts.password
|
||||
or parts.query or parts.fragment or not parts.path.startswith("/")
|
||||
or parts.hostname in {"localhost", "127.0.0.1"}):
|
||||
raise ValueError("tenant welcome targets require exact HTTPS URLs")
|
||||
self.public_url = public_url.rstrip("/")
|
||||
self.setter = setter
|
||||
self.ttl_seconds = ttl_seconds
|
||||
|
|
@ -45,7 +55,7 @@ class PasswordSetupGrants:
|
|||
self._subjects: dict[str, str] = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def issue(self, subject: str) -> str:
|
||||
def issue(self, subject: str, tenant: str = "") -> str:
|
||||
if not subject or len(subject) > 255:
|
||||
raise ValueError("valid external subject is required")
|
||||
token = secrets.token_urlsafe(32)
|
||||
|
|
@ -56,6 +66,7 @@ class PasswordSetupGrants:
|
|||
self._grants.pop(previous, None)
|
||||
self._grants[digest] = SetupGrant(
|
||||
subject=subject,
|
||||
return_to=self.tenant_returns.get(tenant, ""),
|
||||
expires_at=self.clock() + self.ttl_seconds,
|
||||
)
|
||||
self._subjects[subject] = digest
|
||||
|
|
@ -67,7 +78,7 @@ class PasswordSetupGrants:
|
|||
grant = self._grants.get(digest)
|
||||
return grant is not None and grant.expires_at > self.clock()
|
||||
|
||||
def consume(self, token: str, password: str) -> None:
|
||||
def consume(self, token: str, password: str) -> str:
|
||||
if len(password) < 12:
|
||||
raise ValueError("password must contain at least 12 characters")
|
||||
digest = _digest(token)
|
||||
|
|
@ -77,6 +88,7 @@ class PasswordSetupGrants:
|
|||
raise ValueError("password setup link is invalid or expired")
|
||||
self._subjects.pop(grant.subject, None)
|
||||
self.setter(grant.subject, password)
|
||||
return grant.return_to
|
||||
|
||||
|
||||
class LLDAPPasswordSetter:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue