Skip to content

Commit

Permalink
Fix fps-auth-fief (#316)
Browse files Browse the repository at this point in the history
* Fix fps-auth-fief

* Disable fps-webdav
  • Loading branch information
davidbrochart committed Jun 6, 2023
1 parent c6d7f87 commit 79353a0
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 141 deletions.
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

0 comments on commit 79353a0

Please sign in to comment.