You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Added UI code for
1-Add new Knowledge Graph
Create and initiate graph
2-Data Ingest for a KG
Upload files to server
Download files from cloud
Ingest the uploaded or ingest files
S3 bedrock configuration(placeholder in ui rightnow will integrate later)
Auto matic ingest after file upload is in progress
3-Refresh Knowledge Graph
Show warning the you can cancel or proceed
tg-pr-agentBot
changed the title
GML-2011:Add UI page for Knowledge Graph init and Data Ingestion, and Knowledge Graph Rebuild
GML-2011:Add UI page for Knowledge Graph init and Data Ingestion, and Knowledge Graph Rebuild
Nov 9, 2025
Here are some key observations to aid the review process:
⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 No relevant tests
🔒 Security concerns
Path traversal: The upload and delete endpoints in graphrag/app/routers/ui.py construct paths with unsanitized file.filename and query filename, enabling directory traversal and arbitrary file overwrite/deletion. Sanitize with os.path.basename, validate characters, and reject paths with separators.
Injection: The create_graph endpoint in graphrag/app/routers/supportai.py directly f-strings user-controlled graphname into GSQL. Validate/escape graph names to prevent GSQL injection and enforce identifier rules.
Sensitive information exposure: The cloud download endpoint accepts raw credentials (AWS keys, GCS service account JSON, Azure account key) over HTTP API. Ensure transport is HTTPS-only, avoid logging secrets (including exception traces), and document least-privilege guidance. Consider using temporary credentials or server-side stored secrets rather than accepting long-lived keys.
Blocking I/O in async routes: Cloud downloads use blocking SDKs (boto3, google-cloud-storage, azure-storage-blob) inside async def, which can block the event loop. Run these in a threadpool (anyio.to_thread.run_sync / starlette.concurrency.run_in_threadpool) or make the routes synchronous to avoid DoS via long-running downloads.
The new endpoints use os (filesystem ops) and json (GCS credentials parsing) without importing them, which will raise runtime errors. Ensure required modules are imported.
upload_dir=os.path.join("uploads", graphname)
ifnotos.path.exists(upload_dir):
return {"graphname": graphname, "files": [], "total_files": 0, "total_size": 0}
files_info= []
total_size=0forfilenameinos.listdir(upload_dir):
file_path=os.path.join(upload_dir, filename)
ifos.path.isfile(file_path):
file_stat=os.stat(file_path)
files_info.append({
"filename": filename,
"size": file_stat.st_size,
"modified": file_stat.st_mtime,
})
total_size+=file_stat.st_sizereturn {
"graphname": graphname,
"files": files_info,
"total_files": len(files_info),
"total_size": total_size,
}
exceptExceptionase:
logger.error(f"Error listing files for graph {graphname}: {e}")
raiseHTTPException(status_code=500, detail=f"Error listing files: {str(e)}")
@router.post(route_prefix+"/{graphname}/uploads")
asyncdefupload_files(
graphname: str,
creds: Annotated[tuple[list[str], HTTPBasicCredentials], Depends(ui_basic_auth)],
files: list[UploadFile] =File(...),
overwrite: bool=False,
):
""" Upload one or multiple files for a specific graphname. Files are stored in uploads/{graphname}/ directory. Parameters: - graphname: The graph name to associate files with - files: List of files to upload - overwrite: If False (default), will reject if files already exist """try:
upload_dir=os.path.join("uploads", graphname)
os.makedirs(upload_dir, exist_ok=True)
# Check for existing files if overwrite is Falseifnotoverwrite:
existing_files= []
forfileinfiles:
file_path=os.path.join(upload_dir, file.filename)
ifos.path.exists(file_path):
existing_files.append(file.filename)
ifexisting_files:
return {
"status": "conflict",
"message": "Some files already exist. Set overwrite=true to replace them.",
"existing_files": existing_files,
}
# Save uploaded filesuploaded_files= []
total_size=0forfileinfiles:
file_path=os.path.join(upload_dir, file.filename)
# Write file to diskwithopen(file_path, "wb") asf:
content=awaitfile.read()
f.write(content)
file_size=len(content)
total_size+=file_sizeuploaded_files.append({
"filename": file.filename,
"size": file_size,
"path": file_path,
})
logger.info(f"Uploaded file {file.filename} ({file_size} bytes) for graph {graphname}")
return {
"status": "success",
"message": f"Successfully uploaded {len(uploaded_files)} file(s)",
"graphname": graphname,
"uploaded_files": uploaded_files,
"total_size": total_size,
}
exceptExceptionase:
exc=traceback.format_exc()
logger.error(f"Error uploading files for graph {graphname}: {e}")
logger.debug_pii(f"Upload error trace:\n{exc}")
raiseHTTPException(status_code=500, detail=f"Error uploading files: {str(e)}")
@router.delete(route_prefix+"/{graphname}/uploads")asyncdefclear_uploaded_files(
graphname: str,
creds: Annotated[tuple[list[str], HTTPBasicCredentials], Depends(ui_basic_auth)],
filename: str|None=None,
):
""" Clear uploaded files for a specific graphname. Parameters: - graphname: The graph name whose files to clear - filename: If provided, only delete this specific file. Otherwise, delete all files. """try:
upload_dir=os.path.join("uploads", graphname)
ifnotos.path.exists(upload_dir):
return {
"status": "success",
"message": f"No files found for graph {graphname}",
"deleted_files": [],
}
deleted_files= []
iffilename:
# Delete specific filefile_path=os.path.join(upload_dir, filename)
ifos.path.exists(file_path) andos.path.isfile(file_path):
os.remove(file_path)
deleted_files.append(filename)
logger.info(f"Deleted file {filename} for graph {graphname}")
else:
raiseHTTPException(status_code=404, detail=f"File {filename} not found")
else:
# Delete all files in the directoryforfilenameinos.listdir(upload_dir):
file_path=os.path.join(upload_dir, filename)
ifos.path.isfile(file_path):
os.remove(file_path)
deleted_files.append(filename)
# Remove the directory if it's emptyifnotos.listdir(upload_dir):
os.rmdir(upload_dir)
logger.info(f"Deleted {len(deleted_files)} file(s) for graph {graphname}")
return {
"status": "success",
"message": f"Successfully deleted {len(deleted_files)} file(s)",
"graphname": graphname,
"deleted_files": deleted_files,
}
exceptHTTPException:
raiseexceptExceptionase:
exc=traceback.format_exc()
logger.error(f"Error deleting files for graph {graphname}: {e}")
logger.debug_pii(f"Delete error trace:\n{exc}")
raiseHTTPException(status_code=500, detail=f"Error deleting files: {str(e)}")
# Cloud Storage Download Endpoints@router.post(route_prefix+"/{graphname}/cloud/download")asyncdefdownload_from_cloud(
graphname: str,
credentials: Annotated[HTTPBase, Depends(security)],
provider: str=None,
# S3 parametersaccess_key: str=None,
secret_key: str=None,
bucket: str=None,
region: str=None,
prefix: str="",
# GCS parametersproject_id: str=None,
gcs_credentials_json: str=None,
# Azure parametersaccount_name: str=None,
account_key: str=None,
container: str=None,
):
""" Download files from cloud storage (S3, GCS, or Azure) to local directory. Parameters: - graphname: The graph name to associate downloaded files with - provider: Cloud provider (s3, gcs, azure) - For S3: access_key, secret_key, bucket, region, prefix - For GCS: project_id, gcs_credentials_json, bucket, prefix - For Azure: account_name, account_key, container, prefix """try:
download_dir=os.path.join("downloaded_files_cloud", graphname)
os.makedirs(download_dir, exist_ok=True)
downloaded_files= []
ifprovider=="s3":
# Import boto3 for S3try:
importboto3frombotocore.exceptionsimportClientErrorexceptImportError:
raiseHTTPException(
status_code=500,
detail="boto3 is not installed. Please install it to use S3 downloads."
)
ifnotall([access_key, secret_key, bucket, region]):
raiseHTTPException(
status_code=400,
detail="Missing S3 credentials: access_key, secret_key, bucket, and region are required"
)
# Create S3 clients3_client=boto3.client(
's3',
aws_access_key_id=access_key,
aws_secret_access_key=secret_key,
region_name=region
)
# List and download objectstry:
paginator=s3_client.get_paginator('list_objects_v2')
pages=paginator.paginate(Bucket=bucket, Prefix=prefixor"")
forpageinpages:
if'Contents'notinpage:
continueforobjinpage['Contents']:
key=obj['Key']
# Skip directoriesifkey.endswith('/'):
continue# Get filenamefilename=os.path.basename(key)
local_path=os.path.join(download_dir, filename)
# Download files3_client.download_file(bucket, key, local_path)
downloaded_files.append(filename)
logger.info(f"Downloaded {key} to {local_path}")
exceptClientErrorase:
logger.error(f"S3 download error: {e}")
raiseHTTPException(status_code=500, detail=f"S3 error: {str(e)}")
elifprovider=="gcs":
# Import GCS clienttry:
fromgoogle.cloudimportstoragefromgoogle.oauth2importservice_accountexceptImportError:
raiseHTTPException(
status_code=500,
detail="google-cloud-storage is not installed. Please install it to use GCS downloads."
)
ifnotall([project_id, gcs_credentials_json, bucket]):
raiseHTTPException(
status_code=400,
detail="Missing GCS credentials: project_id, gcs_credentials_json, and bucket are required"
)
try:
# Parse credentials JSONcreds_dict=json.loads(gcs_credentials_json)
credentials=service_account.Credentials.from_service_account_info(creds_dict)
# Create GCS clientgcs_client=storage.Client(project=project_id, credentials=credentials)
bucket_obj=gcs_client.bucket(bucket)
# List and download blobsblobs=bucket_obj.list_blobs(prefix=prefixor"")
forblobinblobs:
# Skip directoriesifblob.name.endswith('/'):
continue# Get filenamefilename=os.path.basename(blob.name)
local_path=os.path.join(download_dir, filename)
# Download blobblob.download_to_filename(local_path)
downloaded_files.append(filename)
logger.info(f"Downloaded {blob.name} to {local_path}")
exceptjson.JSONDecodeError:
raiseHTTPException(status_code=400, detail="Invalid GCS credentials JSON")
exceptExceptionase:
logger.error(f"GCS download error: {e}")
raiseHTTPException(status_code=500, detail=f"GCS error: {str(e)}")
File upload/delete paths use file.filename and user-supplied filename directly with os.path.join without sanitization. This allows path traversal and overwriting arbitrary files. Use os.path.basename, validate allowed characters, and reject paths containing separators. Also consider streaming writes instead of await file.read() to avoid large memory usage.
forfileinfiles:
file_path=os.path.join(upload_dir, file.filename)
# Write file to diskwithopen(file_path, "wb") asf:
content=awaitfile.read()
f.write(content)
file_size=len(content)
total_size+=file_sizeuploaded_files.append({
"filename": file.filename,
"size": file_size,
"path": file_path,
})
logger.info(f"Uploaded file {file.filename} ({file_size} bytes) for graph {graphname}")
return {
"status": "success",
"message": f"Successfully uploaded {len(uploaded_files)} file(s)",
"graphname": graphname,
"uploaded_files": uploaded_files,
"total_size": total_size,
}
exceptExceptionase:
exc=traceback.format_exc()
logger.error(f"Error uploading files for graph {graphname}: {e}")
logger.debug_pii(f"Upload error trace:\n{exc}")
raiseHTTPException(status_code=500, detail=f"Error uploading files: {str(e)}")
@router.delete(route_prefix+"/{graphname}/uploads")
asyncdefclear_uploaded_files(
graphname: str,
creds: Annotated[tuple[list[str], HTTPBasicCredentials], Depends(ui_basic_auth)],
filename: str|None=None,
):
""" Clear uploaded files for a specific graphname. Parameters: - graphname: The graph name whose files to clear - filename: If provided, only delete this specific file. Otherwise, delete all files. """try:
upload_dir=os.path.join("uploads", graphname)
ifnotos.path.exists(upload_dir):
return {
"status": "success",
"message": f"No files found for graph {graphname}",
"deleted_files": [],
}
deleted_files= []
iffilename:
# Delete specific filefile_path=os.path.join(upload_dir, filename)
ifos.path.exists(file_path) andos.path.isfile(file_path):
os.remove(file_path)
deleted_files.append(filename)
logger.info(f"Deleted file {filename} for graph {graphname}")
else:
raiseHTTPException(status_code=404, detail=f"File {filename} not found")
chengbiao-jin
changed the title
GML-2011:Add UI page for Knowledge Graph init and Data Ingestion, and Knowledge Graph Rebuild
GML-2011 Add UI page for Knowledge Graph init and Data Ingestion, and Knowledge Graph Rebuild
Nov 20, 2025
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
User description
Added UI code for
1-Add new Knowledge Graph
Create and initiate graph
2-Data Ingest for a KG
Upload files to server
Download files from cloud
Ingest the uploaded or ingest files
S3 bedrock configuration(placeholder in ui rightnow will integrate later)
Auto matic ingest after file upload is in progress
3-Refresh Knowledge Graph
Show warning the you can cancel or proceed
PR Type
Enhancement
Description
Setup UI for graph create, ingest, refresh
Server-side uploads with list/delete
Cloud imports from S3/GCS/Azure
New create_graph API, proxy, dependency
Diagram Walkthrough
flowchart LR uiSetup["Setup UI"] apiCreate["POST /{graph}/graphrag/create_graph"] apiUpload["POST /ui/{graph}/uploads"] fsUploads["uploads/{graph}/"] apiCloud["POST /ui/{graph}/cloud/download"] fsCloud["downloaded_files_cloud/{graph}/"] apiIngest["POST /{graph}/graphrag/create_ingest + ingest"] apiRefresh["GET /{graph}/graphrag/forceupdate"] uiSetup -- "Create Graph" --> apiCreate uiSetup -- "Upload files" --> apiUpload apiUpload -- "stores files" --> fsUploads uiSetup -- "Download from cloud" --> apiCloud apiCloud -- "writes files" --> fsCloud uiSetup -- "Ingest data" --> apiIngest uiSetup -- "Refresh graph" --> apiRefreshFile Walkthrough
6 files
Add endpoint to create TigerGraph graphAdd server uploads and cloud download APIsReload graph list on focus/navigationAdd Setup navigation button with iconRegister Setup page routeNew KG Setup page with ingest workflow5 files
Proxy dynamic graphrag/supportai routes to backendRemove shared common path symlinkRemove shared configs path symlinkRemove redundant common symlink referenceRemove redundant configs symlink reference1 files
Add python-multipart for file uploads