Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
Vadym Samoilenko
0976ee9421 fix: Azure AD SSO authentication (AADSTS900144 client_id error)
Changes:
- Use tenant-specific authority instead of 'organizations' endpoint
- Pass code parameter explicitly in acquire_token_by_authorization_code
- Fix REDIRECT_URI to include /auth/callback path
- Add ALLOWED_TENANT_IDS support for multi-tenant auth
- Improve error logging for token acquisition

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
2026-02-25 11:48:07 +00:00
SamoilenkoVadym
1156209232 fix: prevent filename collisions that break Excel metadata lookup
- Remove collision-avoidance rename (_1, _2, etc) in FileService.save_upload;
  overwrite file on disk instead, preserving original filename
- Deduplicate in SessionStore.add_file_to_session: replace existing entry
  with same filename instead of appending duplicate
- Deduplicate upload results list for consistent frontend response

The rename broke Excel/import metadata lookup which matches by
Path(filename).stem.lower(). Files are already isolated per user_id
directory, so overwriting is safe.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 10:22:25 +00:00
5 changed files with 77 additions and 19 deletions

View file

@ -12,9 +12,17 @@ ROOT_PATH=/solventum-image-metadata
# === Azure AD / SSO ===
AZURE_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
AZURE_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef
# AZURE_CLIENT_SECRET is not needed (client-side MSAL.js flow)
# Must match Azure AD App Registration > Authentication > SPA Redirect URIs exactly
REDIRECT_URI=https://ai-sandbox.oliver.solutions/solventum-image-metadata/
# AZURE_CLIENT_SECRET is REQUIRED for server-side MSAL flow (get from Azure Portal > App > Certificates & secrets)
AZURE_CLIENT_SECRET=
# Must match Azure AD App Registration > Authentication > Redirect URIs EXACTLY (including /auth/callback path)
# For production: https://ai-sandbox.oliver.solutions/solventum-image-metadata/auth/callback
# For local dev: http://localhost:5001/auth/callback
REDIRECT_URI=https://ai-sandbox.oliver.solutions/solventum-image-metadata/auth/callback
# Optional: Multi-tenant support - comma-separated list of allowed tenant IDs
# Leave empty to allow any organizational tenant (after Azure Portal configuration)
# Example: tenant-id-1,tenant-id-2,tenant-id-3
ALLOWED_TENANT_IDS=
# === OpenAI (optional — for AI metadata generation) ===
OPENAI_API_KEY=

View file

@ -115,7 +115,15 @@ async def upload_files(
"filename": filename,
"file_type": file_type,
})
results.append(file_result)
# Deduplicate results: replace existing entry with same filename
existing_idx = next(
(i for i, r in enumerate(results) if r.get("filename") == filename),
None,
)
if existing_idx is not None:
results[existing_idx] = file_result
else:
results.append(file_result)
else:
file_result = await metadata_service.process_uploaded_file(
filepath=filepath,
@ -125,7 +133,15 @@ async def upload_files(
import_map=import_map,
)
store.add_file_to_session(session_id, file_result)
results.append(file_result)
# Deduplicate results: replace existing entry with same filename
existing_idx = next(
(i for i, r in enumerate(results) if r.get("filename") == filename),
None,
)
if existing_idx is not None:
results[existing_idx] = file_result
else:
results.append(file_result)
except ValueError as e:
results.append({"filename": upload_file.filename, "error": str(e)})

View file

@ -38,14 +38,9 @@ class FileService:
user_dir.mkdir(parents=True, exist_ok=True)
filepath = user_dir / filename
# Handle name collisions
if filepath.exists():
stem = filepath.stem
suffix = filepath.suffix
counter = 1
while filepath.exists():
filepath = user_dir / f"{stem}_{counter}{suffix}"
counter += 1
# Overwrite if file already exists (user re-uploads same file).
# Preserving original filename is critical for Excel metadata lookup.
# Stream to disk (handles large files without loading into memory)
with open(filepath, "wb") as f:

View file

@ -105,7 +105,11 @@ class SessionStore:
conn.close()
def add_file_to_session(self, session_id: str, file_entry: Dict[str, Any]):
"""Add a processed file entry to a session."""
"""Add a processed file entry to a session.
If a file with the same filename already exists in the session,
it is replaced (deduplication for re-uploaded files).
"""
conn = self._get_conn()
try:
row = conn.execute(
@ -114,7 +118,16 @@ class SessionStore:
).fetchone()
if row:
files = json.loads(row["files_json"])
files.append(file_entry)
# Deduplicate: replace existing entry with same filename
filename = file_entry.get("filename", "")
existing_idx = next(
(i for i, f in enumerate(files) if f.get("filename") == filename),
None,
)
if existing_idx is not None:
files[existing_idx] = file_entry
else:
files.append(file_entry)
conn.execute(
"UPDATE file_sessions SET files_json = ? WHERE session_id = ?",
(json.dumps(files, ensure_ascii=False), session_id),

View file

@ -165,6 +165,11 @@ class MicrosoftSSO:
self.tenant_id = os.getenv('AZURE_TENANT_ID')
self.redirect_uri = os.getenv('REDIRECT_URI', 'http://localhost:5001/auth/callback')
# Optional: Comma-separated list of allowed tenant IDs for multi-tenant auth
# Example: "tenant-id-1,tenant-id-2,tenant-id-3"
allowed_tenants = os.getenv('ALLOWED_TENANT_IDS', '')
self.allowed_tenant_ids = [t.strip() for t in allowed_tenants.split(',') if t.strip()]
# Check if SSO is configured
if not all([self.client_id, self.client_secret, self.tenant_id]):
self.enabled = False
@ -173,14 +178,16 @@ class MicrosoftSSO:
try:
import msal
# Use specific tenant_id for single-tenant, or 'organizations' for multi-tenant
# Single-tenant is more reliable and avoids client_id issues
self.authority = f"https://login.microsoftonline.com/{self.tenant_id}"
self.app = msal.ConfidentialClientApplication(
self.client_id,
client_id=self.client_id,
authority=self.authority,
client_credential=self.client_secret
)
self.enabled = True
logger.info("Microsoft SSO initialized successfully")
logger.info(f"Microsoft SSO initialized successfully (authority: {self.authority})")
except ImportError:
self.enabled = False
logger.warning("Microsoft SSO not available (msal library not installed)")
@ -202,11 +209,13 @@ class MicrosoftSSO:
return None
try:
return self.app.get_authorization_request_url(
auth_url = self.app.get_authorization_request_url(
scopes=["User.Read"],
state=state,
redirect_uri=self.redirect_uri
)
logger.info(f"Generated auth URL with redirect_uri: {self.redirect_uri}")
return auth_url
except Exception as e:
logger.error(f"Error generating auth URL: {e}")
return None
@ -225,11 +234,28 @@ class MicrosoftSSO:
return None
try:
# Explicitly pass all parameters as named arguments
result = self.app.acquire_token_by_authorization_code(
auth_code,
code=auth_code,
scopes=["User.Read"],
redirect_uri=self.redirect_uri
)
# Check for errors in the result
if result and 'error' in result:
logger.error(f"Token acquisition error: {result.get('error')} - {result.get('error_description')}")
return result
# Validate tenant if allowed_tenant_ids is configured
if result and self.allowed_tenant_ids:
user_tenant = result.get('id_token_claims', {}).get('tid')
if user_tenant and user_tenant not in self.allowed_tenant_ids:
logger.warning(f"User from unauthorized tenant: {user_tenant}")
return {
'error': 'unauthorized_tenant',
'error_description': 'Your organization is not authorized to access this application'
}
return result
except Exception as e:
logger.error(f"Error acquiring token: {e}")