Skip to content

Commit

Permalink
Merge branch 'main' into update-dependencies
Browse files Browse the repository at this point in the history
* main:
  Displays sync history in dashboard (#86)
  🔐 OAuth (#89)
  • Loading branch information
mrharpo committed Jul 31, 2023
2 parents fed009e + 98f6642 commit 77d249f
Show file tree
Hide file tree
Showing 10 changed files with 610 additions and 128 deletions.
19 changes: 17 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,24 @@
"request": "launch",
"cwd": "${workspaceFolder}",
"module": "pytest",
"args": ["-v", "-n", "auto"],
"args": [
"-v",
"-n",
"auto"
],
"jinja": false,
"justMyCode": false
},
{
"name": "Metaflow: Run",
"type": "python",
"request": "launch",
"program": "${file}",
"args": [
"run"
],
"subProcess": true,
"console": "integratedTerminal"
}
]
}
}
13 changes: 13 additions & 0 deletions chowda/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
from ._version import __version__
from os import environ
from dotenv import find_dotenv, load_dotenv

__all__ = ['__version__']

if environ.get('METAFLOW_NAMESPACE'):
from metaflow import namespace

namespace(environ.get('METAFLOW_NAMESPACE'))


# Load dotenv file if present
CHOWDA_ENV = environ.get('CHOWDA_ENV', 'development')
dotenv_path = find_dotenv(filename=f'.env.{CHOWDA_ENV}')
load_dotenv(dotenv_path)
7 changes: 6 additions & 1 deletion chowda/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from starlette.middleware import Middleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import HTMLResponse
from starlette.routing import Route

from chowda._version import __version__
from chowda.admin import Admin
from chowda.api import api
from chowda.config import STATIC_DIR, TEMPLATES_DIR
from chowda.auth import OAuthProvider
from chowda.config import SECRET, STATIC_DIR, TEMPLATES_DIR
from chowda.db import engine
from chowda.models import (
Batch,
Expand Down Expand Up @@ -54,6 +57,8 @@
title='Chowda',
templates_dir=TEMPLATES_DIR,
statics_dir=STATIC_DIR,
auth_provider=OAuthProvider(),
middlewares=[Middleware(SessionMiddleware, secret_key=SECRET)],
)

# Add views
Expand Down
3 changes: 3 additions & 0 deletions chowda/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .oauth import OAuthProvider

__all__ = ['OAuthProvider']
79 changes: 79 additions & 0 deletions chowda/auth/oauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
from typing import Optional

from authlib.integrations.starlette_client import OAuth
from starlette.datastructures import URL
from starlette.middleware import Middleware
from starlette.requests import Request
from starlette.responses import RedirectResponse, Response
from starlette.routing import Route
from starlette_admin import BaseAdmin
from starlette_admin.auth import AdminUser, AuthMiddleware, AuthProvider

from chowda.config import AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_DOMAIN

oauth = OAuth()
oauth.register(
'auth0',
client_id=AUTH0_CLIENT_ID,
client_secret=AUTH0_CLIENT_SECRET,
client_kwargs={
'scope': 'openid profile email',
},
server_metadata_url=f'https://{AUTH0_DOMAIN}/.well-known/openid-configuration',
)


class OAuthProvider(AuthProvider):
async def is_authenticated(self, request: Request) -> bool:
if request.session.get('user', None) is not None:
request.state.user = request.session.get('user')
return True
return False

def get_admin_user(self, request: Request) -> Optional[AdminUser]:
user = request.state.user
return AdminUser(
username=user['name'],
photo_url=user['picture'],
)

async def render_login(self, request: Request, admin: BaseAdmin):
"""Override the default login behavior to implement custom logic."""
auth0 = oauth.create_client('auth0')
redirect_uri = request.url_for(
admin.route_name + ':authorize_auth0'
).include_query_params(next=request.query_params.get('next'))
return await auth0.authorize_redirect(request, str(redirect_uri))

async def render_logout(self, request: Request, admin: BaseAdmin) -> Response:
"""Override the default logout to implement custom logic"""
request.session.clear()
return RedirectResponse(
url=URL(f'https://{AUTH0_DOMAIN}/v2/logout').include_query_params(
returnTo=request.url_for(admin.route_name + ':index'),
client_id=AUTH0_CLIENT_ID,
)
)

async def handle_auth_callback(self, request: Request):
auth0 = oauth.create_client('auth0')
token = await auth0.authorize_access_token(request)
request.session.update({'user': token['userinfo']})
return RedirectResponse(request.query_params.get('next'))

def setup_admin(self, admin: 'BaseAdmin'):
super().setup_admin(admin)
"""add custom authentication callback route"""
admin.routes.append(
Route(
'/auth0/authorize',
self.handle_auth_callback,
methods=['GET'],
name='authorize_auth0',
)
)

def get_middleware(self, admin: 'BaseAdmin') -> Middleware:
return Middleware(
AuthMiddleware, provider=self, allow_paths=['/auth0/authorize']
)
18 changes: 8 additions & 10 deletions chowda/config.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
from os import environ

from dotenv import find_dotenv, load_dotenv

CHOWDA_ENV = environ.get('CHOWDA_ENV', 'development')


dotenv_path = find_dotenv(filename=f'.env.{CHOWDA_ENV}')


load_dotenv(dotenv_path)

DB_USER = environ.get('DB_USER', 'postgres')
DB_PASSWORD = environ.get('DB_PASSWORD', 'postgres')
DB_HOST = environ.get('DB_HOST', 'localhost')
Expand All @@ -22,3 +12,11 @@

TEMPLATES_DIR = environ.get('TEMPLATES_DIR', 'templates')
STATIC_DIR = environ.get('STATIC_DIR', 'static')

AUTH0_CLIENT_ID = environ.get('AUTH0_CLIENT_ID')
AUTH0_CLIENT_SECRET = environ.get('AUTH0_CLIENT_SECRET')
AUTH0_DOMAIN = environ.get('AUTH0_DOMAIN')

SECRET = environ.get('CHOWDA_SECRET')

API_AUDIENCE = environ.get('API_AUDIENCE', 'https://chowda.wgbh-mla.org/api')
32 changes: 25 additions & 7 deletions chowda/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from dataclasses import dataclass
from datetime import datetime
from json import loads
from typing import Any, ClassVar, Dict

Expand All @@ -12,9 +11,13 @@
from starlette_admin.contrib.sqlmodel import ModelView
from starlette_admin.exceptions import FormValidationError

from chowda.config import API_AUDIENCE
from chowda.db import engine
from chowda.models import MediaFile

from metaflow import Flow
from metaflow.exception import MetaflowNotFound


@dataclass
class MediaFilesGuidLinkField(BaseField):
Expand Down Expand Up @@ -43,6 +46,13 @@ async def serialize_value(
return len(value)


class AdminModelView(ModelView):
def is_accessible(self, request: Request) -> bool:
return set(request.state.user.get(f'{API_AUDIENCE}/roles', set())).intersection(
{'admin', 'clammer'}
)


class CollectionView(ModelView):
fields: ClassVar[list[Any]] = [
'name',
Expand Down Expand Up @@ -105,7 +115,7 @@ class MediaFileView(ModelView):
fields: ClassVar[list[Any]] = ['guid', 'collections', 'batches']


class UserView(ModelView):
class UserView(AdminModelView):
fields: ClassVar[list[Any]] = ['first_name', 'last_name', 'email']


Expand All @@ -128,18 +138,26 @@ class ClamsEventView(ModelView):


class DashboardView(CustomView):
def sony_ci_last_sync(self):
# TODO: replace with actual "last sync" time
return datetime.now()
def sync_history(self) -> Dict[str, Any]:
try:
return [
{'created_at': sync_run.created_at, 'successful': sync_run.successful}
for sync_run in list(Flow('IngestFlow'))
][:10]
except MetaflowNotFound:
return []

async def render(self, request: Request, templates: Jinja2Templates) -> Response:
return templates.TemplateResponse(
'dashboard.html',
{'request': request, 'sony_ci_last_sync': self.sony_ci_last_sync()},
{
'request': request,
'sync_history': self.sync_history(),
},
)


class SonyCiAssetView(ModelView):
class SonyCiAssetView(AdminModelView):
fields: ClassVar[list[Any]] = [
'name',
'size',
Expand Down
Loading

0 comments on commit 77d249f

Please sign in to comment.