Skip to content

Commit

Permalink
Merge pull request #141 from arnoldknott/stage
Browse files Browse the repository at this point in the history
Stage: updates session management in frontend (server & client side) , CSRF, socketio testing in backend, secures cookies
  • Loading branch information
arnoldknott authored Dec 6, 2024
2 parents 5f8dc1f + 34245bd commit fdc645a
Show file tree
Hide file tree
Showing 60 changed files with 954 additions and 1,225 deletions.
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.formatOnSave": true
},
"editor.hover.enabled": true,
"svelte.plugin.css.hover.enable": true,
"svelte.plugin.html.hover.enable": true,
"svelte.plugin.svelte.hover.enable": true,
"svelte.plugin.typescript.hover.enable": true,
}
2 changes: 1 addition & 1 deletion backendAPI/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ dependencies = [
'fastapi[standard]>=0.115.4,<1',
'pyjwt[crypto]>=2.9.0,<3',
'requests>=2.32.3,<3',
'uvicorn[all]>=0.32.0,<1',
'uvicorn[all]>=0.32.1,<1',
'azure-identity>=1.19.0,<2',
'azure-keyvault-secrets>=4.9.0,<5',
'pydantic-settings>=2.6.1,<3',
Expand Down
4 changes: 2 additions & 2 deletions backendAPI/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from routers.api.v1.public_resource import router as public_resource_router
from routers.api.v1.tag import router as tag_router
from routers.socketio.v1.base import presentation_interests_router, socketio_server
from routers.socketio.v1.protected_events import protected_events_router
from routers.socketio.v1.demo_namespace import demo_namespace_router
from routers.ws.v1.websockets import router as websocket_router

# print("Current directory:", os.getcwd())
Expand Down Expand Up @@ -228,7 +228,7 @@ async def lifespan(app: FastAPI):
# TBD: consider adding a dependency here for the token
)

socketio_server.register_namespace(protected_events_router)
socketio_server.register_namespace(demo_namespace_router)
socketio_server.register_namespace(presentation_interests_router)
socketio_app = ASGIApp(socketio_server, socketio_path="socketio/v1")
app.mount("/socketio/v1", app=socketio_app)
Expand Down
65 changes: 30 additions & 35 deletions backendAPI/src/routers/socketio/v1/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,9 @@
async_mode="asgi",
cors_allowed_origins=[], # disable CORS in Socket.IO, as FastAPI handles CORS!
logger=True,
engineio_logger=True,
engineio_logger=False, # prevents the ping and pong messages from being logged
)

# print("=== routers - socketio - v1 - vars(socketio_server) ===")
# pprint(vars(socketio_server))
# print("=== routers - socketio - v1 - dir(socketio_server) ===")
# pprint(dir(socketio_server))
# print("=== routers - socketio - v1 - var(socketio_app) ===")
# pprint(socketio_app)
# print("=== routers - socketio - v1 - dir(socketio_app) ===")
# pprint(dir(socketio_app))
# print("=== routers - socketio - v1 - flush ===", flush=True)


# TBD: add auth as FastAPI Depends
@socketio_server.event
Expand Down Expand Up @@ -67,10 +57,12 @@ async def catch_all(event, sid, data):


@socketio_server.event
async def demo_message(sid, data):
"""Demo message event for socket.io."""
async def public_message(sid, data):
"""Public message event for socket.io."""
logger.info(f"Received message from client {sid}: {data}")
await socketio_server.emit("demo_message", f"Message received from client: {data}")
await socketio_server.emit(
"public_message", f"Message received from client: {data}"
)


# @socketio_server.event(namespace="/protected_events")
Expand Down Expand Up @@ -157,8 +149,7 @@ async def on_comments(self, sid, data):
presentation_interests_router = PresentationInterests("/presentation_interests")


# TBD: rename into BaseNamespace!
class BaseEvents(socketio.AsyncNamespace):
class BaseNamespace(socketio.AsyncNamespace):
"""Base class for socket.io namespaces."""

def __init__(
Expand All @@ -185,33 +176,37 @@ async def on_connect(
auth=None,
):
"""Connect event for socket.io namespaces."""
# TBD: add a try-except block around the authentication and return specific authentication failed error.
guards = self.guards
print("=== base - on_connect - sid ===")
print(sid, flush=True)
# print("=== base - on_connect - environ ===")
# pprint(environ)
print("=== base - on_connect - auth ===")
print(auth, flush=True)
print("=== base - on_connect - guards ===")
print(guards, flush=True)
logger.info(f"Client connected with session id: {sid}.")

token_payload = await get_azure_token_payload(auth)
print("=== base - on_connect - token_payload ===")
print(token_payload, flush=True)
try:
guards = self.guards
print("=== base - on_connect - sid ===")
print(sid, flush=True)
# print("=== base - on_connect - environ ===")
# pprint(environ)
print("=== base - on_connect - auth ===")
print(auth, flush=True)
print("=== base - on_connect - guards ===")
print(guards, flush=True)
logger.info(f"Client connected with session id: {sid}.")

token_payload = await get_azure_token_payload(auth)
print("=== base - on_connect - token_payload ===")
print(token_payload, flush=True)
except Exception as err:
logger.error(f"Client with session id {sid} failed to authenticate.")
print("=== base - on_connect - Exception ===")
print(err, flush=True)
raise ConnectionRefusedError("Authorization failed")

# current_user = await check_token_against_guards(token_payload, self.guards)
# print("=== base - on_connect - sid - current_user ===")
# print(current_user, flush=True)
emit_response = await self.server.emit(
"protected_message",
await self.server.emit(
"demo_message",
f"Hello new client with session id {sid}",
namespace=self.namespace,
callback=self.callback,
)
print("=== base - on_connect - emit_response ===")
print(emit_response, flush=True)
# TBD: should not return anything or potentially true?
return "OK from server"

async def on_disconnect(self, sid):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from core.types import GuardTypes
from crud.protected_resource import ProtectedResourceCRUD

from .base import BaseEvents
from .base import BaseNamespace

logger = logging.getLogger(__name__)

Expand All @@ -16,28 +16,26 @@
# # await sio.emit("message", f"Message received from client {sid}: {data}")


# TBD: rename into ProtectedNamespace(BaseNamespace)!
class ProtectedEvents(BaseEvents):
class DemoNamespace(BaseNamespace):
"""Protected class for socket.io namespaces."""

def __init__(self, namespace=None):
super().__init__(
namespace=namespace,
guards=GuardTypes(scopes=["sockets" "api.write"], roles=["User"]),
guards=GuardTypes(scopes=["sockets", "api.write"], roles=["User"]),
crud=ProtectedResourceCRUD,
)
self.namespace = namespace

async def on_protected_message(self, sid, data):
async def on_demo_message(self, sid, data):
"""Demo message event for socket.io namespaces with guards."""
logger.info(f"Received message from client {sid}: {data}")
await self.server.emit(
"protected_message",
f"Protected message received from client: {data}",
"demo_message",
f"Demo message received from client: {data}",
namespace=self.namespace,
)


# TBD: rename into protected_namespace_router!
protected_events_router = ProtectedEvents("/protected_events")
demo_namespace_router = DemoNamespace("/demo_namespace")
# socketio_server.register_namespace(ProtectedEvents())
130 changes: 98 additions & 32 deletions backendAPI/src/routers/socketio/v1/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,82 @@
import asyncio
from typing import List
from unittest.mock import patch

import pytest
import socketio
import uvicorn


@pytest.fixture(scope="function")
async def mock_token_payload(request):
"""Returns a mocked token payload."""

# print("=== mock_token_payload ===")
# print(request.param)

with patch("core.security.decode_token") as mock:
mock.return_value = request.param
yield mock


@pytest.fixture()
async def socketio_server(mock_token_payload):
"""Provide a socket.io server."""

sio = socketio.AsyncServer(async_mode="asgi", logger=True, engineio_logger=True)
app = socketio.ASGIApp(sio, socketio_path="socketio/v1")

# @sio.event
# def connect(sid, environ):
# print("connect ", sid, flush=True)

# @sio.event
# async def chat_message(sid, data):
# await sio.emit("chat_message", f"You are connected to test server with {sid}")
# print("=== chat_message - data ===")
# print(data)

# @sio.event
# def disconnect(sid):
# print("disconnect ", sid)

config = uvicorn.Config(app, host="127.0.0.1", port=8669, log_level="info")
server = uvicorn.Server(config)

### Works with aiohttp, too:

# sio = socketio.AsyncServer()
# app = web.Application()# add the path "/socketio/v1" here?
# sio.attach(app)

# @sio.event
# def connect(sid, environ):
# print("connect ", sid, flush=True)

# @sio.event
# async def chat_message(sid, data):
# await sio.emit("chat_message", f"You are connected to test server with {sid}")
# print("=== chat_message - data ===")
# print(data)

# @sio.event
# def disconnect(sid):
# print("disconnect ", sid)

# runner = web.AppRunner(app)
# await runner.setup()
# site = web.TCPSite(runner, "localhost", 8669)
# await site.start()

### end WORKS with aiohttp

asyncio.create_task(server.serve())
await asyncio.sleep(1)
yield sio
await server.shutdown()


# This one connects to the socketio server in the main FastAPI application:
@pytest.fixture
async def socketio_simple_client():
"""Provide a simple socket.io client."""
Expand All @@ -18,27 +90,27 @@ async def socketio_simple_client():
await client.disconnect()


# This one connects to the production socketio server in FastAPI:
@pytest.fixture
async def socketio_client():
"""Provides a socket.io client and connects to it."""
# Note, this one can only make real connections without authentication
# The server cannot be patched, as this client is running on a different machine

async def _socketio_client(namespaces: List[str] = None):
client = socketio.AsyncClient(logger=True, engineio_logger=True)

@client.event
def connect(namespace="/protected_events"):
"""Connect event for socket.io."""
return "OK from client"
# pass
# @client.event
# def connect(namespace=namespaces[0]):
# """Connect event for socket.io."""
# return "OK from client"
# # pass

@client.event
def protected_message(data, namespace="/protected_events"):
print("=== protected_message - listening to server here ===")
print("=== conftest - socketio_client - protected_message - data ===")
print(data)
pass
# @client.event
# def demo_message(data, namespace=namespaces[0]):
# print("=== demo_message - listening to server here ===")
# print("=== conftest - socketio_client - protected_message - data ===")
# print(data)
# pass

await client.connect(
"http://127.0.0.1:80",
Expand All @@ -51,26 +123,20 @@ def protected_message(data, namespace="/protected_events"):
return _socketio_client


@pytest.fixture(scope="function")
async def mock_token_payload(request):
"""Returns a mocked token payload."""

print("=== mock_token_payload ===")
print(request.param)
# This one connects to the mocked test socketio server from the fixture socketio_server:
@pytest.fixture
async def socketio_patched_client():
"""Provides a socket.io client and connects to it."""

with patch("core.security.decode_token") as mock:
mock.return_value = request.param
# mock.return_value = {
# "some": "payload"
# } # TBD: replace with parameterization for different payloads
yield mock
async def _socketio_client(namespaces: List[str] = None):
client = socketio.AsyncClient(logger=True, engineio_logger=True)

await client.connect(
"http://127.0.0.1:8669",
socketio_path="socketio/v1",
namespaces=namespaces,
)
yield client
await client.disconnect()

@pytest.fixture(scope="function")
async def provide_socketio_connection(mock_token_payload):
"""Provide a socket.io connection with a mocked token payload."""
pass
# TBD: all server-side - no client!
# TBD: implement: call on_connect() with mocked token payload
# TBD: yield the connection
# TBD: implement: call on_disconnect()
return _socketio_client
Loading

0 comments on commit fdc645a

Please sign in to comment.