Skip to content

feat: adds user authentication #218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Jun 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ exclude =
.git,
__pycache__,
build,
venv,
.venv
env
max-complexity = 10
1 change: 1 addition & 0 deletions docs/source/Contributing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ To run uvicorn locally:
export ENV_NAME='local'
export AWS_DEFAULT_REGION='us-west-2'
export AIND_AIRFLOW_PARAM_PREFIX='/aind/dev/airflow/variables/job_types'
export AIND_SSO_SECRET_NAME='/aind/dev/data_transfer_service/sso/secrets'
uvicorn aind_data_transfer_service.server:app --host 0.0.0.0 --port 5000 --reload

You can now access aind-data-transfer-service at
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ server = [
'wtforms',
'requests==2.25.0',
'openpyxl',
'python-logging-loki'
'python-logging-loki',
'authlib'
]

[tool.setuptools.packages.find]
Expand Down
84 changes: 84 additions & 0 deletions src/aind_data_transfer_service/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
__version__ as aind_data_transfer_models_version,
)
from aind_data_transfer_models.core import SubmitJobRequest, validation_context
from authlib.integrations.starlette_client import OAuth
from botocore.exceptions import ClientError
from fastapi import Request
from fastapi.responses import JSONResponse, StreamingResponse
Expand All @@ -23,6 +24,9 @@
from openpyxl import load_workbook
from pydantic import SecretStr, ValidationError
from starlette.applications import Starlette
from starlette.config import Config
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import RedirectResponse
from starlette.routing import Route

from aind_data_transfer_service import OPEN_DATA_BUCKET_NAME
Expand Down Expand Up @@ -95,6 +99,27 @@ def get_project_names() -> List[str]:
return project_names


def set_oauth() -> OAuth:
"""Set up OAuth for the service"""
secrets_client = boto3.client("secretsmanager")
secret_response = secrets_client.get_secret_value(
SecretId=os.getenv("AIND_SSO_SECRET_NAME")
)
secret_value = json.loads(secret_response["SecretString"])
for secrets in secret_value:
os.environ[secrets] = secret_value[secrets]
config = Config()
oauth = OAuth(config)
oauth.register(
name="azure",
client_id=config("CLIENT_ID"),
client_secret=config("CLIENT_SECRET"),
server_metadata_url=config("AUTHORITY"),
client_kwargs={"scope": "openid email profile"},
)
return oauth


def get_job_types(version: Optional[str] = None) -> List[str]:
"""Get a list of job_types"""
params = get_parameter_infos(version)
Expand Down Expand Up @@ -1090,6 +1115,60 @@ def get_parameter(request: Request):
)


async def admin(request: Request):
"""Get admin page if authenticated, else redirect to login."""
user = request.session.get("user")
if os.getenv("ENV_NAME") == "local":
user = {"name": "local user"}
if user:
return templates.TemplateResponse(
name="admin.html",
context=(
{
"request": request,
"project_names_url": project_names_url,
"user_name": user.get("name", "unknown"),
"user_email": user.get("email", "unknown"),
}
),
)
return RedirectResponse(url="/login")


async def login(request: Request):
"""Redirect to Azure login page"""
oauth = set_oauth()
redirect_uri = request.url_for("auth")
response = await oauth.azure.authorize_redirect(request, redirect_uri)
return response


async def logout(request: Request):
"""Logout user and clear session"""
request.session.pop("user", None)
return RedirectResponse(url="/")


async def auth(request: Request):
"""Authenticate user and store user info in session"""
oauth = set_oauth()
try:
token = await oauth.azure.authorize_access_token(request)
user = token.get("userinfo")
if not user:
raise ValueError("User info not found in access token.")
request.session["user"] = dict(user)
except Exception as error:
return JSONResponse(
content={
"message": "Error Logging In",
"data": {"error": f"{error.__class__.__name__}{error.args}"},
},
status_code=500,
)
return RedirectResponse(url="/admin")


routes = [
Route("/", endpoint=index, methods=["GET", "POST"]),
Route("/api/validate_csv", endpoint=validate_csv_legacy, methods=["POST"]),
Expand Down Expand Up @@ -1132,6 +1211,11 @@ def get_parameter(request: Request):
endpoint=download_job_template,
methods=["GET"],
),
Route("/login", login, methods=["GET"]),
Route("/logout", logout, methods=["GET"]),
Route("/auth", auth, methods=["GET"]),
Route("/admin", admin, methods=["GET"]),
]

app = Starlette(routes=routes)
app.add_middleware(SessionMiddleware, secret_key=None)
36 changes: 36 additions & 0 deletions src/aind_data_transfer_service/templates/admin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<title>{% block title %} {% endblock %} AIND Data Transfer Service Admin</title>
<style>
body {
margin: 20px;
font-family: arial, sans-serif;
}
nav {
height: 40px;
}
</style>
</head>
<body>
<nav>
<a href="/">Submit Jobs</a> |
<a href="/jobs">Job Status</a> |
<a href="/job_params">Job Parameters</a> |
<a title="Download job template as .xslx" href="/api/job_upload_template" download>Job Submit Template</a> |
<a title="List of project names" href="{{ project_names_url }}" target="_blank">Project Names</a> |
<a title="For more information click here" href="https://aind-data-transfer-service.readthedocs.io"
target="_blank">Help</a> |
<a href="/admin">Admin</a> |
<a href="/logout">Log out</a>
</nav>
<div>
<h3>Admin</h3>
<div>Hello {{user_name}}, welcome to the admin page</div>
<div>Email: {{user_email}}</div>
</div>
</body>
</html>
3 changes: 2 additions & 1 deletion src/aind_data_transfer_service/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
<a href="/job_params">Job Parameters</a> |
<a title="Download job template as .xslx" href= "/api/job_upload_template" download>Job Submit Template</a> |
<a title="List of project names" href= "{{ project_names_url }}" target="_blank" >Project Names</a> |
<a title="For more information click here" href="https://aind-data-transfer-service.readthedocs.io" target="_blank" >Help</a>
<a title="For more information click here" href="https://aind-data-transfer-service.readthedocs.io" target="_blank" >Help</a> |
<a href="/admin">Admin</a>
</nav>
<br>
<div>
Expand Down
3 changes: 2 additions & 1 deletion src/aind_data_transfer_service/templates/job_params.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
<a href="/job_params">Job Parameters</a> |
<a title="Download job template as .xslx" href= "/api/job_upload_template" download>Job Submit Template</a> |
<a title="List of project names" href= "{{ project_names_url }}" target="_blank" >Project Names</a> |
<a title="For more information click here" href="https://aind-data-transfer-service.readthedocs.io" target="_blank" >Help</a>
<a title="For more information click here" href="https://aind-data-transfer-service.readthedocs.io" target="_blank" >Help</a> |
<a href="/admin">Admin</a>
</nav>
<div class="content">
<h4 class="mb-2">
Expand Down
3 changes: 2 additions & 1 deletion src/aind_data_transfer_service/templates/job_status.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
<a href="/job_params">Job Parameters</a> |
<a title="Download job template as .xslx" href= "/api/job_upload_template" download>Job Submit Template</a> |
<a title="List of project names" href= "{{ project_names_url }}" target="_blank" >Project Names</a> |
<a title="For more information click here" href="https://aind-data-transfer-service.readthedocs.io" target="_blank" >Help</a>
<a title="For more information click here" href="https://aind-data-transfer-service.readthedocs.io" target="_blank" >Help</a> |
<a href="/admin">Admin</a>
</nav>
<div class="content">
<!-- display total entries -->
Expand Down
19 changes: 19 additions & 0 deletions tests/resources/get_secrets_response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"ARN": "arn_value",
"Name": "secret_name",
"VersionId": "version_id",
"SecretString": "{\"CLIENT_ID\":\"client_id\",\"CLIENT_SECRET\":\"client_secret\",\"AUTHORITY\":\"https://authority\"}",
"VersionStages": ["AWSCURRENT"],
"CreatedDate": "2025-04-15T16:44:07.279000Z",
"ResponseMetadata": {
"RequestId": "request_id",
"HTTPStatusCode": 200,
"HTTPHeaders": {
"x-amzn-requestid": "2b090d64-c92d-48c5-a43a-abf5696c815e",
"content-type": "application/x-amz-json-1.1",
"content-length": "748",
"date": "Wed, 23 Apr 2025 21:19:04 GMT"
},
"RetryAttempts": 0
}
}
Loading