173 lines
5.6 KiB
Python
173 lines
5.6 KiB
Python
"""
|
|
Matrix User Provisioning for DAARION Auth Service
|
|
|
|
This module handles automatic Matrix user creation when users register in DAARION.
|
|
"""
|
|
|
|
import hashlib
|
|
import hmac
|
|
import httpx
|
|
import logging
|
|
import os
|
|
from typing import Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SYNAPSE_ADMIN_URL = os.getenv("SYNAPSE_ADMIN_URL", "http://daarion-synapse:8008")
|
|
REGISTRATION_SECRET = os.getenv("SYNAPSE_REGISTRATION_SECRET", "daarion_reg_secret_2024")
|
|
|
|
|
|
async def generate_matrix_mac(
|
|
nonce: str,
|
|
username: str,
|
|
password: str,
|
|
admin: bool = False,
|
|
user_type: Optional[str] = None
|
|
) -> str:
|
|
"""Generate HMAC for Synapse registration."""
|
|
mac = hmac.new(
|
|
key=REGISTRATION_SECRET.encode('utf-8'),
|
|
digestmod=hashlib.sha1
|
|
)
|
|
|
|
mac.update(nonce.encode('utf-8'))
|
|
mac.update(b"\x00")
|
|
mac.update(username.encode('utf-8'))
|
|
mac.update(b"\x00")
|
|
mac.update(password.encode('utf-8'))
|
|
mac.update(b"\x00")
|
|
mac.update(b"admin" if admin else b"notadmin")
|
|
|
|
if user_type:
|
|
mac.update(b"\x00")
|
|
mac.update(user_type.encode('utf-8'))
|
|
|
|
return mac.hexdigest()
|
|
|
|
|
|
async def provision_matrix_user(
|
|
user_id: str,
|
|
email: str,
|
|
display_name: Optional[str] = None,
|
|
is_admin: bool = False
|
|
) -> dict:
|
|
"""
|
|
Create a Matrix user for a DAARION user.
|
|
|
|
Args:
|
|
user_id: DAARION user ID (UUID)
|
|
email: User email
|
|
display_name: Optional display name
|
|
is_admin: Whether user should be Matrix admin
|
|
|
|
Returns:
|
|
dict with matrix_user_id and access_token
|
|
"""
|
|
# Generate Matrix username from DAARION user_id
|
|
# Use first 8 chars of UUID for readability
|
|
matrix_username = f"daarion_{user_id[:8].replace('-', '')}"
|
|
|
|
# Generate a secure password (user won't need it - they'll use SSO)
|
|
matrix_password = hashlib.sha256(
|
|
f"{user_id}:{REGISTRATION_SECRET}".encode()
|
|
).hexdigest()[:32]
|
|
|
|
async with httpx.AsyncClient() as client:
|
|
try:
|
|
# Step 1: Get nonce
|
|
nonce_response = await client.get(
|
|
f"{SYNAPSE_ADMIN_URL}/_synapse/admin/v1/register"
|
|
)
|
|
nonce_response.raise_for_status()
|
|
nonce = nonce_response.json()["nonce"]
|
|
|
|
# Step 2: Generate MAC
|
|
mac = await generate_matrix_mac(
|
|
nonce=nonce,
|
|
username=matrix_username,
|
|
password=matrix_password,
|
|
admin=is_admin
|
|
)
|
|
|
|
# Step 3: Register user
|
|
register_response = await client.post(
|
|
f"{SYNAPSE_ADMIN_URL}/_synapse/admin/v1/register",
|
|
json={
|
|
"nonce": nonce,
|
|
"username": matrix_username,
|
|
"password": matrix_password,
|
|
"admin": is_admin,
|
|
"mac": mac,
|
|
"displayname": display_name or email.split("@")[0],
|
|
}
|
|
)
|
|
register_response.raise_for_status()
|
|
|
|
result = register_response.json()
|
|
logger.info(f"Matrix user created: {result['user_id']}")
|
|
|
|
return {
|
|
"matrix_user_id": result["user_id"],
|
|
"access_token": result.get("access_token"),
|
|
"device_id": result.get("device_id"),
|
|
"home_server": "app.daarion.space"
|
|
}
|
|
|
|
except httpx.HTTPStatusError as e:
|
|
if e.response.status_code == 400:
|
|
error_detail = e.response.json().get("error", "")
|
|
if "User ID already taken" in error_detail:
|
|
logger.info(f"Matrix user already exists: {matrix_username}")
|
|
return {
|
|
"matrix_user_id": f"@{matrix_username}:daarion.space",
|
|
"access_token": None,
|
|
"device_id": None,
|
|
"home_server": "app.daarion.space",
|
|
"already_exists": True
|
|
}
|
|
logger.error(f"Failed to create Matrix user: {e.response.text}")
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Matrix provisioning error: {e}")
|
|
raise
|
|
|
|
|
|
async def get_matrix_login_token(matrix_user_id: str) -> Optional[str]:
|
|
"""
|
|
Get a login token for existing Matrix user.
|
|
This allows SSO-style login without password.
|
|
"""
|
|
# TODO: Implement when needed
|
|
# This requires Synapse's login token API
|
|
pass
|
|
|
|
|
|
async def update_matrix_profile(
|
|
matrix_user_id: str,
|
|
display_name: Optional[str] = None,
|
|
avatar_url: Optional[str] = None,
|
|
access_token: str = None
|
|
) -> bool:
|
|
"""Update Matrix user profile."""
|
|
async with httpx.AsyncClient() as client:
|
|
try:
|
|
if display_name:
|
|
await client.put(
|
|
f"{SYNAPSE_ADMIN_URL}/_matrix/client/v3/profile/{matrix_user_id}/displayname",
|
|
json={"displayname": display_name},
|
|
headers={"Authorization": f"Bearer {access_token}"} if access_token else {}
|
|
)
|
|
|
|
if avatar_url:
|
|
await client.put(
|
|
f"{SYNAPSE_ADMIN_URL}/_matrix/client/v3/profile/{matrix_user_id}/avatar_url",
|
|
json={"avatar_url": avatar_url},
|
|
headers={"Authorization": f"Bearer {access_token}"} if access_token else {}
|
|
)
|
|
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"Failed to update Matrix profile: {e}")
|
|
return False
|
|
|