Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3516376d92 | ||
|
|
ffa984e271 |
@@ -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
@@ -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
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user