33import secrets
44import time
55from logging import getLogger
6- from typing import Dict
76from uuid import uuid4
87
98import aiohttp
1817from passlib .context import CryptContext
1918from pydantic import BaseModel
2019from sqlmodel import Session , create_engine , select
21- from typing_extensions import Annotated
20+ from typing_extensions import Annotated , Any
2221
2322from murfey .server .murfey_db import murfey_db , url
2423from murfey .util .api import url_path_for
5049 instrument_oauth2_scheme = lambda * args , ** kwargs : None
5150pwd_context = CryptContext (schemes = ["bcrypt" ], deprecated = "auto" )
5251
53- instrument_server_tokens : Dict [float , dict ] = {}
52+ instrument_server_tokens : dict [float , dict ] = {}
5453
5554# Set up database engine
5655try :
@@ -66,14 +65,30 @@ def hash_password(password: str) -> str:
6665
6766"""
6867=======================================================================================
69- TOKEN VALIDATION FUNCTIONS
68+ VALIDATION FUNCTIONS
7069=======================================================================================
7170
7271Functions and helpers used to validate incoming requests from both the client and
73- the frontend. 'validate_token()' and 'validate_instrument_token()' are imported
74- int the other FastAPI modules and attached as dependencies to the routers.
72+ the frontend.
73+
74+ 'validate_token()' and 'validate_instrument_token()' are imported in the other FastAPI
75+ modules and attached as dependencies to the routers. They validate the tokens passed
76+ around internally by Murfey to ensure that the request is valid.
77+
78+ 'validate_instrument_server_session_access()' and 'validate_frontend_session_access()'
79+ are used to verify the IDs of sessions ot be accessed, and are attached as dependencies
80+ to them.
81+
82+ 'validate_user_instrument_access()' is used to verify the instrument server name being
83+ accessed by the frontend, and is attached as a dependency as well.
7584"""
7685
86+ # Essential headers used for authentication to forward along if present
87+ AUTH_HEADERS = (
88+ "authorization" ,
89+ "x-auth-request-access-token" ,
90+ )
91+
7792
7893def check_user (username : str ) -> bool :
7994 try :
@@ -84,6 +99,39 @@ def check_user(username: str) -> bool:
8499 return username in [u .username for u in users ]
85100
86101
102+ async def submit_to_auth_endpoint (
103+ url_subpath : str ,
104+ request : Request ,
105+ token : str ,
106+ ) -> dict [str , Any ]:
107+ """
108+ Helper function to forward incoming requests to an authentication server
109+ to verify that they are allowed to inspect the
110+ """
111+
112+ # Forward only essentials auth-related headers
113+ headers = {
114+ key : value
115+ for key , value in dict (request .headers ).items ()
116+ if key .lower () in AUTH_HEADERS
117+ }
118+ if security_config .auth_type == "password" :
119+ headers ["authorization" ] = f"Bearer { token } "
120+ cookies = (
121+ {security_config .cookie_key : token }
122+ if security_config .auth_type == "cookie"
123+ else {}
124+ )
125+ async with aiohttp .ClientSession (cookies = cookies ) as session :
126+ async with session .get (
127+ f"{ auth_url } /{ url_subpath } " ,
128+ headers = headers ,
129+ ) as response :
130+ success = response .status == 200
131+ validation_outcome : dict [str , Any ] = await response .json ()
132+ return validation_outcome if success and validation_outcome else {"valid" : False }
133+
134+
87135async def validate_token (
88136 token : Annotated [str , Depends (oauth2_scheme )],
89137 request : Request ,
@@ -94,25 +142,9 @@ async def validate_token(
94142 try :
95143 # Validate using auth URL if provided; will error if invalid
96144 if auth_url :
97- # Extract and forward headers as-is
98- headers = dict (request .headers )
99- # Update/add authorization header if authenticating using password
100- if security_config .auth_type == "password" :
101- headers ["authorization" ] = f"Bearer { token } "
102- # Forward the cookie along if authenticating using cookie
103- cookies = (
104- {security_config .cookie_key : token }
105- if security_config .auth_type == "cookie"
106- else {}
107- )
108- async with aiohttp .ClientSession (cookies = cookies ) as session :
109- async with session .get (
110- f"{ auth_url } /validate_token" ,
111- headers = headers ,
112- ) as response :
113- success = response .status == 200
114- validation_outcome = await response .json ()
115- if not (success and validation_outcome .get ("valid" )):
145+ if not (
146+ await submit_to_auth_endpoint ("validate_token" , request , token )
147+ ).get ("valid" ):
116148 raise JWTError
117149 # If authenticating using cookies; an auth URL MUST be provided
118150 else :
@@ -199,20 +231,6 @@ async def validate_instrument_token(
199231 return None
200232
201233
202- """
203- =======================================================================================
204- SESSION ID VALIDATION
205- =======================================================================================
206-
207- Annotated ints are defined here that trigger validation of the session IDs in incoming
208- requests, verifying that the session is allowed to access the particular visit.
209-
210- The 'MurfeySessionID...' types are imported and used in the type hints of the endpoint
211- functions in the other FastAPI routers, depending on whether requests from the frontend
212- or the instrument are expected.
213- """
214-
215-
216234def get_visit_name (session_id : int ) -> str :
217235 with Session (engine ) as murfey_db :
218236 return (
@@ -222,46 +240,6 @@ def get_visit_name(session_id: int) -> str:
222240 )
223241
224242
225- async def submit_to_auth_endpoint (url_subpath : str , token : str ) -> None :
226- if auth_url :
227- headers = (
228- {}
229- if security_config .auth_type == "cookie"
230- else {"Authorization" : f"Bearer { token } " }
231- )
232- cookies = (
233- {security_config .cookie_key : token }
234- if security_config .auth_type == "cookie"
235- else {}
236- )
237- async with aiohttp .ClientSession (cookies = cookies ) as session :
238- async with session .get (
239- f"{ auth_url } /{ url_subpath } " ,
240- headers = headers ,
241- ) as response :
242- success = response .status == 200
243- validation_outcome : dict = await response .json ()
244- if not (success and validation_outcome .get ("valid" )):
245- logger .warning ("Unauthorised visit access request from frontend" )
246- raise HTTPException (
247- status_code = status .HTTP_401_UNAUTHORIZED ,
248- detail = "You do not have access to this visit" ,
249- headers = {"WWW-Authenticate" : "Bearer" },
250- )
251-
252-
253- async def validate_frontend_session_access (
254- session_id : int ,
255- token : Annotated [str , Depends (oauth2_scheme )],
256- ) -> int :
257- """
258- Validates whether a frontend request can access information about this session
259- """
260- visit_name = get_visit_name (session_id )
261- await submit_to_auth_endpoint (f"validate_visit_access/{ visit_name } " , token )
262- return session_id
263-
264-
265243async def validate_instrument_server_session_access (
266244 session_id : int ,
267245 token : Annotated [str , Depends (instrument_oauth2_scheme )],
@@ -294,25 +272,60 @@ async def validate_instrument_server_session_access(
294272 return session_id
295273
296274
275+ async def validate_frontend_session_access (
276+ session_id : int ,
277+ request : Request ,
278+ token : Annotated [str , Depends (oauth2_scheme )],
279+ ) -> int :
280+ """
281+ Validates whether a frontend request can access information about this session
282+ """
283+ visit_name = get_visit_name (session_id )
284+ if auth_url :
285+ if not (
286+ await submit_to_auth_endpoint (
287+ f"validate_visit_access/{ visit_name } " ,
288+ request ,
289+ token ,
290+ )
291+ ).get ("valid" ):
292+ raise HTTPException (
293+ status_code = status .HTTP_401_UNAUTHORIZED ,
294+ detail = "You do not have access to this visit" ,
295+ headers = {"WWW-Authenticate" : "Bearer" },
296+ )
297+ return session_id
298+
299+
297300async def validate_user_instrument_access (
298301 instrument_name : str ,
302+ request : Request ,
299303 token : Annotated [str , Depends (oauth2_scheme )],
300304) -> str :
301305 """
302306 Validates whether a frontend request can access information about this instrument
303307 """
304- await submit_to_auth_endpoint (
305- f"validate_instrument_access/{ instrument_name } " , token
306- )
308+ if auth_url :
309+ if not (
310+ await submit_to_auth_endpoint (
311+ f"validate_instrument_access/{ instrument_name } " ,
312+ request ,
313+ token ,
314+ )
315+ ).get ("valid" ):
316+ raise HTTPException (
317+ status_code = status .HTTP_401_UNAUTHORIZED ,
318+ detail = "You do not have access to this instrument" ,
319+ headers = {"WWW-Authenticate" : "Bearer" },
320+ )
307321 return instrument_name
308322
309323
310- # Set validation conditions for the session ID based on where the request is from
311- MurfeySessionIDFrontend = Annotated [int , Depends (validate_frontend_session_access )]
324+ # Create annotated session ID and instrument name for endpoints that need to verify them
312325MurfeySessionIDInstrument = Annotated [
313326 int , Depends (validate_instrument_server_session_access )
314327]
315-
328+ MurfeySessionIDFrontend = Annotated [ int , Depends ( validate_frontend_session_access )]
316329MurfeyInstrumentNameFrontend = Annotated [str , Depends (validate_user_instrument_access )]
317330
318331
0 commit comments