Skip to content
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

Fix fps-auth-fief #316

Merged
merged 2 commits into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
164 changes: 89 additions & 75 deletions plugins/auth_fief/fps_auth_fief/backend.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple

from fastapi import Depends, HTTPException, Request, Response, WebSocket, status
Expand All @@ -9,88 +10,101 @@
from .config import _AuthFiefConfig


class Backend:
def __init__(self, auth_fief_config: _AuthFiefConfig):
class CustomFiefAuth(FiefAuth):
client: FiefAsync
class CustomFiefAuth(FiefAuth):
client: FiefAsync

async def get_unauthorized_response(self, request: Request, response: Response):
redirect_uri = str(request.url_for("auth_callback"))
auth_url = await self.client.auth_url(redirect_uri, scope=["openid"])
raise HTTPException(
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
headers={"Location": auth_url},
)

self.fief = FiefAsync(
auth_fief_config.base_url,
auth_fief_config.client_id,
auth_fief_config.client_secret,
async def get_unauthorized_response(self, request: Request, response: Response):
redirect_uri = str(request.url_for("auth_callback"))
auth_url = await self.client.auth_url(redirect_uri, scope=["openid"])
raise HTTPException(
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
headers={"Location": auth_url},
)

self.SESSION_COOKIE_NAME = "fps_auth_fief_user_session"
scheme = APIKeyCookie(name=self.SESSION_COOKIE_NAME, auto_error=False)
self.auth = CustomFiefAuth(self.fief, scheme)

async def update_user(
user: FiefUserInfo = Depends(self.auth.current_user()),
access_token_info: FiefAccessTokenInfo = Depends(self.auth.authenticated()),
):
async def _(data: Dict[str, Any]) -> FiefUserInfo:
user = await self.fief.update_profile(
access_token_info["access_token"], {"fields": data}
)
return user

return _

def websocket_auth(permissions: Optional[Dict[str, List[str]]] = None):
async def _(
websocket: WebSocket,
) -> Optional[Tuple[WebSocket, Optional[Dict[str, List[str]]]]]:
accept_websocket = False
checked_permissions: Optional[Dict[str, List[str]]] = None
if self.SESSION_COOKIE_NAME in websocket._cookies:
access_token = websocket._cookies[self.SESSION_COOKIE_NAME]
if permissions is None:
accept_websocket = True
else:
checked_permissions = {}
for resource, actions in permissions.items():
allowed = checked_permissions[resource] = []
for action in actions:
try:
await self.fief.validate_access_token(
access_token, required_permissions=[f"{resource}:{action}"]
)
except BaseException:
pass
else:
allowed.append(action)
accept_websocket = True
if accept_websocket:
return websocket, checked_permissions
@dataclass
class Res:
fief: FiefAsync
session_cookie_name: str
auth: CustomFiefAuth
current_user: Any
update_user: Any
websocket_auth: Any


def get_backend(auth_fief_config: _AuthFiefConfig) -> Res:
fief = FiefAsync(
auth_fief_config.base_url,
auth_fief_config.client_id,
auth_fief_config.client_secret,
)

session_cookie_name = "fps_auth_fief_user_session"
scheme = APIKeyCookie(name=session_cookie_name, auto_error=False)
auth = CustomFiefAuth(fief, scheme)

async def update_user(
user: FiefUserInfo = Depends(auth.current_user()),
access_token_info: FiefAccessTokenInfo = Depends(auth.authenticated()),
):
async def _(data: Dict[str, Any]) -> FiefUserInfo:
user = await fief.update_profile(access_token_info["access_token"], {"fields": data})
return user

return _

def websocket_auth(permissions: Optional[Dict[str, List[str]]] = None):
async def _(
websocket: WebSocket,
) -> Optional[Tuple[WebSocket, Optional[Dict[str, List[str]]]]]:
accept_websocket = False
checked_permissions: Optional[Dict[str, List[str]]] = None
if session_cookie_name in websocket._cookies:
access_token = websocket._cookies[session_cookie_name]
if permissions is None:
accept_websocket = True
else:
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return None
checked_permissions = {}
for resource, actions in permissions.items():
allowed = checked_permissions[resource] = []
for action in actions:
try:
await fief.validate_access_token(
access_token, required_permissions=[f"{resource}:{action}"]
)
except BaseException:
pass
else:
allowed.append(action)
accept_websocket = True
if accept_websocket:
return websocket, checked_permissions
else:
await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return None

return _
return _

def current_user(permissions=None):
if permissions is not None:
permissions = [
f"{resource}:{action}"
for resource, actions in permissions.items()
for action in actions
]
def current_user(permissions=None):
if permissions is not None:
permissions = [
f"{resource}:{action}"
for resource, actions in permissions.items()
for action in actions
]

async def _(
user: FiefUserInfo = Depends(self.auth.current_user(permissions=permissions)),
):
return User(**user["fields"])
async def _(
user: FiefUserInfo = Depends(auth.current_user(permissions=permissions)),
):
return User(**user["fields"])

return _
return _

self.current_user = current_user
self.update_user = update_user
self.websocket_auth = websocket_auth
return Res(
fief=fief,
session_cookie_name=session_cookie_name,
auth=auth,
current_user=current_user,
update_user=update_user,
websocket_auth=websocket_auth,
)
4 changes: 2 additions & 2 deletions plugins/auth_fief/fps_auth_fief/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from jupyverse_api.app import App

from .config import _AuthFiefConfig
from .routes import _AuthFief
from .routes import auth_factory


class AuthFiefComponent(Component):
Expand All @@ -18,5 +18,5 @@ async def start(

app = await ctx.request_resource(App)

auth_fief = _AuthFief(app, self.auth_fief_config)
auth_fief = auth_factory(app, self.auth_fief_config)
ctx.add_resource(auth_fief, types=Auth)
137 changes: 76 additions & 61 deletions plugins/auth_fief/fps_auth_fief/routes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import json
from typing import Dict, List
from typing import Any, Callable, Dict, List, Optional, Tuple

from fastapi import APIRouter, Depends, Query, Request, Response
from fastapi.responses import RedirectResponse
Expand All @@ -8,66 +8,81 @@
from jupyverse_api.app import App
from jupyverse_api.auth import Auth, User

from .backend import Backend
from .backend import get_backend
from .config import _AuthFiefConfig


class _AuthFief(Backend, Auth, Router):
def __init__(
self,
app: App,
auth_fief_config: _AuthFiefConfig,
) -> None:
Router.__init__(self, app)
Backend.__init__(self, auth_fief_config)

router = APIRouter()

@router.get("/auth-callback", name="auth_callback")
async def auth_callback(request: Request, response: Response, code: str = Query(...)):
redirect_uri = str(request.url_for("auth_callback"))
tokens, _ = await self.fief.auth_callback(code, redirect_uri)

response = RedirectResponse(request.url_for("root"))
response.set_cookie(
self.SESSION_COOKIE_NAME,
tokens["access_token"],
max_age=tokens["expires_in"],
httponly=True,
secure=False,
)

return response

@router.get("/api/me")
async def get_api_me(
request: Request,
user: User = Depends(self.current_user()),
access_token_info: FiefAccessTokenInfo = Depends(self.auth.authenticated()),
):
checked_permissions: Dict[str, List[str]] = {}
permissions = json.loads(
dict(request.query_params).get("permissions", "{}").replace("'", '"')
)
if permissions:
user_permissions: Dict[str, List[str]] = {}
for permission in access_token_info["permissions"]:
resource, action = permission.split(":")
if resource not in user_permissions:
user_permissions[resource] = []
user_permissions[resource].append(action)
for resource, actions in permissions.items():
user_resource_permissions = user_permissions.get(resource, [])
allowed = checked_permissions[resource] = []
for action in actions:
if action in user_resource_permissions:
allowed.append(action)

keys = ["username", "name", "display_name", "initials", "avatar_url", "color"]
identity = {k: getattr(user, k) for k in keys}
return {
"identity": identity,
"permissions": checked_permissions,
}

self.include_router(router)
def auth_factory(
app: App,
auth_fief_config: _AuthFiefConfig,
):
backend = get_backend(auth_fief_config)

class _AuthFief(Auth, Router):
def __init__(self) -> None:
super().__init__(app)

router = APIRouter()

@router.get("/auth-callback", name="auth_callback")
async def auth_callback(request: Request, response: Response, code: str = Query(...)):
redirect_uri = str(request.url_for("auth_callback"))
tokens, _ = await backend.fief.auth_callback(code, redirect_uri)

response = RedirectResponse(request.url_for("root"))
response.set_cookie(
backend.session_cookie_name,
tokens["access_token"],
max_age=tokens["expires_in"],
httponly=True,
secure=False,
)

return response

@router.get("/api/me")
async def get_api_me(
request: Request,
user: User = Depends(self.current_user()),
access_token_info: FiefAccessTokenInfo = Depends(backend.auth.authenticated()),
):
checked_permissions: Dict[str, List[str]] = {}
permissions = json.loads(
dict(request.query_params).get("permissions", "{}").replace("'", '"')
)
if permissions:
user_permissions: Dict[str, List[str]] = {}
for permission in access_token_info["permissions"]:
resource, action = permission.split(":")
if resource not in user_permissions:
user_permissions[resource] = []
user_permissions[resource].append(action)
for resource, actions in permissions.items():
user_resource_permissions = user_permissions.get(resource, [])
allowed = checked_permissions[resource] = []
for action in actions:
if action in user_resource_permissions:
allowed.append(action)

keys = ["username", "name", "display_name", "initials", "avatar_url", "color"]
identity = {k: getattr(user, k) for k in keys}
return {
"identity": identity,
"permissions": checked_permissions,
}

self.include_router(router)

def current_user(self, permissions: Optional[Dict[str, List[str]]] = None) -> Callable:
return backend.current_user(permissions)

async def update_user(self, update_user=Depends(backend.update_user)) -> Callable:
return update_user

def websocket_auth(
self,
permissions: Optional[Dict[str, List[str]]] = None,
) -> Callable[[], Tuple[Any, Dict[str, List[str]]]]:
return backend.websocket_auth(permissions)

return _AuthFief()
4 changes: 1 addition & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ pre-install-commands = [
"pip install -e ./plugins/terminals",
"pip install -e ./plugins/yjs",
"pip install -e ./plugins/resource_usage",
"pip install -e ./plugins/webdav[test]",
]
features = ["test"]

Expand Down Expand Up @@ -98,7 +97,7 @@ frontend = ["jupyterlab", "retrolab"]
auth = ["noauth", "auth", "auth_fief"]

[tool.hatch.envs.dev.scripts]
test = "pytest ./tests plugins/webdav/tests -v"
test = "pytest ./tests -v"
lint = [
"black --line-length 100 jupyverse ./plugins",
"isort --profile=black jupyverse ./plugins",
Expand All @@ -113,7 +112,6 @@ typecheck0 = """mypy --no-incremental \
./plugins/terminals \
./plugins/yjs \
./plugins/resource_usage \
./plugins/webdav \
"""

[tool.hatch.envs.docs]
Expand Down