Compare commits

...
2 Commits
Author SHA1 Message Date
Jeroen SchweitzerandClaude Opus 4.5 3516376d92 chore: cleanup auth code after debugging session
Build and Push / build (release) Successful in 50s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 21:41:10 +01:00
Jeroen SchweitzerandClaude Opus 4.5 ffa984e271 fix: separate httpx and SQLAlchemy async contexts in bulk sync
Build and Push / build (release) Successful in 50s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 21:34:34 +01:00
3 changed files with 78 additions and 63 deletions
+12
View File
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.4.6] - 2026-01-01
### Changed
- Code cleanup: move inline `re` import to top of auth/service.py
## [1.4.5] - 2026-01-01
### Fixed
- Separate httpx and SQLAlchemy async contexts in bulk sync (fixes greenlet error)
## [1.4.4] - 2026-01-01 ## [1.4.4] - 2026-01-01
### Fixed ### Fixed
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "core-api" name = "core-api"
version = "1.4.4" version = "1.4.6"
description = "Core Code API - Infrastructure management and tools API" description = "Core Code API - Infrastructure management and tools API"
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
+65 -62
View File
@@ -3,6 +3,7 @@ Authentication Service
Business logic for user synchronization from Authentik. Business logic for user synchronization from Authentik.
""" """
import re
import uuid import uuid
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional from typing import Optional
@@ -251,7 +252,6 @@ class AuthService:
def _extract_cookie(self, headers: httpx.Headers, cookie_name: str) -> str: def _extract_cookie(self, headers: httpx.Headers, cookie_name: str) -> str:
"""Extract a specific cookie value from Set-Cookie headers""" """Extract a specific cookie value from Set-Cookie headers"""
import re
for header in headers.get_list('set-cookie'): for header in headers.get_list('set-cookie'):
if header.startswith(f'{cookie_name}='): if header.startswith(f'{cookie_name}='):
match = re.match(rf'{cookie_name}=([^;]+)', header) match = re.match(rf'{cookie_name}=([^;]+)', header)
@@ -369,6 +369,8 @@ class AuthService:
errors = [] errors = []
total_in_authentik = 0 total_in_authentik = 0
# Step 1: Fetch all user data from Authentik API
authentik_users = []
try: try:
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client: async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
# Authenticate with Authentik to get session cookie # Authenticate with Authentik to get session cookie
@@ -393,72 +395,73 @@ class AuthService:
authentik_users = data.get("results", []) authentik_users = data.get("results", [])
total_in_authentik = data.get("pagination", {}).get("count", len(authentik_users)) total_in_authentik = data.get("pagination", {}).get("count", len(authentik_users))
for auth_user in authentik_users:
try:
# Skip service accounts and inactive users
if auth_user.get("type") in ("service_account", "internal_service_account"):
continue
if not auth_user.get("is_active", True):
continue
# Extract user data from Authentik
authentik_id = uuid.UUID(auth_user["uuid"])
email = auth_user.get("email") or f"{auth_user['username']}@local"
name = auth_user.get("name") or auth_user.get("username", "Unknown")
avatar_url = auth_user.get("avatar")
# Get user's groups for role mapping
groups = []
groups_summary = auth_user.get("groups_obj", [])
for group in groups_summary:
groups.append(group.get("name", ""))
# Check if user exists
stmt = select(User).where(User.authentik_id == authentik_id)
result = await self.session.execute(stmt)
user = result.scalar_one_or_none()
if user is None:
# Create new user
user = User(
authentik_id=authentik_id,
email=email,
name=name,
avatar_url=avatar_url,
)
self.session.add(user)
await self.session.flush()
# Create default preferences
preferences = UserPreferences(user_id=user.id)
self.session.add(preferences)
created += 1
logger.info(f"Created user from Authentik: {email}")
else:
# Update existing user
user.email = email
user.name = name
user.avatar_url = avatar_url
updated += 1
logger.info(f"Updated user from Authentik: {email}")
# Sync roles from groups
await self.sync_roles(user, groups)
except Exception as e:
failed += 1
error_msg = f"Failed to sync user {auth_user.get('username', 'unknown')}: {str(e)}"
errors.append(error_msg)
logger.warning(error_msg)
# Commit all changes
await self.session.commit()
except httpx.HTTPStatusError as e: except httpx.HTTPStatusError as e:
raise ValueError(f"Authentik API error: {e.response.status_code}") raise ValueError(f"Authentik API error: {e.response.status_code}")
except httpx.RequestError as e: except httpx.RequestError as e:
raise ValueError(f"Failed to connect to Authentik: {str(e)}") raise ValueError(f"Failed to connect to Authentik: {str(e)}")
# Step 2: Sync users to database (outside of httpx context to avoid greenlet issues)
for auth_user in authentik_users:
try:
# Skip service accounts and inactive users
if auth_user.get("type") in ("service_account", "internal_service_account"):
continue
if not auth_user.get("is_active", True):
continue
# Extract user data from Authentik
authentik_id = uuid.UUID(auth_user["uuid"])
email = auth_user.get("email") or f"{auth_user['username']}@local"
name = auth_user.get("name") or auth_user.get("username", "Unknown")
avatar_url = auth_user.get("avatar")
# Get user's groups for role mapping
groups = []
groups_summary = auth_user.get("groups_obj", [])
for group in groups_summary:
groups.append(group.get("name", ""))
# Check if user exists
stmt = select(User).where(User.authentik_id == authentik_id)
result = await self.session.execute(stmt)
user = result.scalar_one_or_none()
if user is None:
# Create new user
user = User(
authentik_id=authentik_id,
email=email,
name=name,
avatar_url=avatar_url,
)
self.session.add(user)
await self.session.flush()
# Create default preferences
preferences = UserPreferences(user_id=user.id)
self.session.add(preferences)
created += 1
logger.info(f"Created user from Authentik: {email}")
else:
# Update existing user
user.email = email
user.name = name
user.avatar_url = avatar_url
updated += 1
logger.info(f"Updated user from Authentik: {email}")
# Sync roles from groups
await self.sync_roles(user, groups)
except Exception as e:
failed += 1
error_msg = f"Failed to sync user {auth_user.get('username', 'unknown')}: {str(e)}"
errors.append(error_msg)
logger.warning(error_msg)
# Commit all changes
await self.session.commit()
return BulkSyncResultSchema( return BulkSyncResultSchema(
created=created, created=created,
updated=updated, updated=updated,