11"""Crow's Nest Auth microservice"""
2- import os
2+
33import logging
44from typing import Dict , Tuple , List
55from datetime import datetime , timedelta
2222from . import schemas
2323from . import models
2424from .oauth2_password_bearer_cookie import OAuth2PasswordBearerOrCookie
25+ from .utils import mqtt_match
2526
2627# from .utils import mqtt_match
27- from .exceptions import VerifyException , APIException
28+ from .exceptions import VerifyException
2829
2930LOGGER = logging .getLogger (__name__ )
3031
4041)
4142ACCESS_TOKEN_EXPIRE_MINUTES = env .int ("ACCESS_TOKEN_EXPIRE_MINUTES" , 30 )
4243
43- JWT_TOKEN_SECRET = env ("JWT_TOKEN_SECRET" , os . urandom ( 24 ) )
44+ JWT_TOKEN_SECRET = env ("JWT_TOKEN_SECRET" )
4445
4546USER_DATABASE_URL = env ("USER_DATABASE_URL" )
4647ADMIN_USER_USERNAME = env ("ADMIN_USERNAME" , "admin" )
7374# Exception Handlers
7475
7576
76- @app .exception_handler (APIException )
77- async def api_exception_handler (exc : APIException ):
78- """Handle custom API exception"""
79- return JSONResponse (status_code = exc .status_code , content = {"detail" : exc .message })
80-
81-
8277@app .exception_handler (VerifyException )
8378async def redirect_or_exception_handler (request : Request , exc : VerifyException ):
8479 """Handle redirect or exception"""
8580 uri = request .headers .get ("X-Forwarded-Uri" , "" )
8681 host = request .headers .get ("X-Forwarded-Host" , "" )
8782
8883 if "/api/" in uri :
89- raise APIException ( exc .message )
84+ raise HTTPException ( status_code = 401 , detail = exc .message )
9085
9186 redirect_url = (
9287 "http://"
@@ -148,7 +143,7 @@ async def verify_token(
148143 """Verify that the client provides a valid token"""
149144 user , message = user_tuple
150145 if not user :
151- raise APIException ( 401 , message )
146+ raise HTTPException ( status_code = 401 , detail = message )
152147
153148
154149async def verify_token_admin (
@@ -158,9 +153,9 @@ async def verify_token_admin(
158153 user is an administrator"""
159154 user , message = user_tuple
160155 if not user :
161- raise APIException ( 401 , message )
156+ raise HTTPException ( status_code = 401 , detail = message )
162157 if not user .admin :
163- raise APIException ( 401 , "Unauthorized access" )
158+ raise HTTPException ( status_code = 401 , detail = "Unauthorized access" )
164159
165160
166161@app .on_event ("startup" )
@@ -284,7 +279,7 @@ async def get_user_from_claims(claims: Dict) -> models.User:
284279 Returns:
285280 User: A user instance
286281 """
287- username = claims .get ("sub " )
282+ username = claims .get ("username " )
288283 query = models .users .select ().where (models .User .username == username )
289284 return models .User .from_record (await database .fetch_one (query ))
290285
@@ -390,7 +385,6 @@ async def verify_request(
390385
391386 # Limit access to non-administrators
392387 if "admin" in uri and not user .admin :
393- print (f"The user { user .admin } " )
394388 raise VerifyException ("Unauthorized access" )
395389
396390 # Access Control List checks
@@ -414,40 +408,46 @@ async def verify_request(
414408 return JSONResponse (status_code = 200 , content = {"success" : True })
415409
416410
417- # @app.get("/verify_emqx")
418- # async def verify_emqx(
419- # username: str,
420- # topic: str,
421- # ):
422- # """Authenticate and authorize a request according to EMQX HTTP ACL plugin"""
423- # query = models.users.select().where(models.User.username == username)
424- # record = await database.fetch_one(query)
425- # if not record:
426- # raise HTTPException(403, "Access not allowed")
427-
428- # user = models.User.from_record(record)
411+ @app .get ("/verify_emqx" )
412+ async def verify_emqx (
413+ username : str ,
414+ topic : str ,
415+ ):
416+ """Authenticate and authorize a request according to EMQX HTTP ACL plugin"""
429417
430- # # ACL checks
431- # if patterns := user.topic_whitelist:
432- # accepted = False
433- # for pattern in patterns:
434- # if mqtt_match(pattern, topic):
435- # accepted = True
418+ query = models .users .select ().where (models .User .username == username )
419+ record = await database .fetch_one (query )
436420
437- # if not accepted :
438- # raise HTTPException(403, f"Access is not allowed to {topic}")
421+ if not record :
422+ # Check if the username is a JWT token, if so, retrieve username
439423
440- # if patterns := user.topic_blacklist:
441- # accepted = True
442- # for pattern in patterns:
443- # if mqtt_match(pattern, topic):
444- # accepted = False
424+ claims = jwt .decode (username , JWT_TOKEN_SECRET , algorithms = ["HS256" ])
425+ if "sub" not in claims :
426+ raise HTTPException (401 , "Unauthorized" )
427+ query = models .users .select ().where (models .User .username == claims ["sub" ])
428+ record = await database .fetch_one (query )
429+ if not record :
430+ raise HTTPException (401 , "Unauthorized" )
445431
446- # if not accepted:
447- # raise HTTPException(403, f"Access is not allowed to {topic}")
432+ user = models .User .from_record (record )
448433
449- # # Accepted!
450- # return JSONResponse("Authorized")
434+ # ACL checks
435+ if patterns := user .topic_whitelist :
436+ accepted = False
437+ for pattern in patterns .split ("," ):
438+ if mqtt_match (pattern , topic ):
439+ accepted = True
440+ if not accepted :
441+ raise HTTPException (403 , f"Access is not allowed to { topic } " )
442+ if patterns := user .topic_blacklist :
443+ accepted = True
444+ for pattern in patterns .split ("," ):
445+ if mqtt_match (pattern , topic ):
446+ accepted = False
447+ if not accepted :
448+ raise HTTPException (403 , f"Access is not allowed to { topic } " )
449+ # Accepted!
450+ return JSONResponse ("Authorized" )
451451
452452
453453@app .get (
@@ -482,7 +482,7 @@ async def get_user_by_id(idx: int):
482482 await database .fetch_one (models .users .select ().where (models .User .id == idx ))
483483 )
484484 except Exception as exc :
485- raise APIException ( 406 , str (exc )) from exc
485+ raise HTTPException ( status_code = 406 , detail = str (exc )) from exc
486486
487487
488488@app .post (
@@ -492,16 +492,14 @@ async def get_user_by_id(idx: int):
492492async def create_user (user : schemas .CreateUser ):
493493 """Create user"""
494494 hashed_password = pwd_context .hash (user .password )
495- print ("Create user" )
496- print (user )
497- print (user .path_whitelist )
495+
498496 try :
499497 await database .execute (
500498 models .users .insert ().values (
501499 username = user .username .lower (),
502500 firstname = user .firstname ,
503501 lastname = user .lastname ,
504- email = user .lastname ,
502+ email = user .email ,
505503 admin = user .admin ,
506504 hashed_password = hashed_password ,
507505 path_whitelist = user .path_whitelist ,
@@ -512,13 +510,14 @@ async def create_user(user: schemas.CreateUser):
512510 )
513511 return JSONResponse (status_code = 200 , content = {"success" : True })
514512 except Exception as exc :
515- raise APIException (
516- 406 , f"User with username '{ user .username .lower ()} ' already exists"
513+ raise HTTPException (
514+ status_code = 406 ,
515+ detail = f"User with username '{ user .username .lower ()} ' already exists" ,
517516 ) from exc
518517
519518
520519@app .put (
521- "/users/{id }" ,
520+ "/users/{idx }" ,
522521 response_model = schemas .UserOut ,
523522 dependencies = [Depends (verify_token_admin )],
524523)
@@ -539,6 +538,7 @@ async def modify_user(idx: int, modifications: schemas.ModifyUser):
539538 ]:
540539 if key in mods :
541540 if not validate_paths_text_string (mods [key ]):
541+
542542 raise HTTPException (status_code = 422 , detail = f"Invalid value for { key } " )
543543
544544 # Update user in the database
@@ -547,7 +547,10 @@ async def modify_user(idx: int, modifications: schemas.ModifyUser):
547547 models .users .update ().where (models .User .id == idx ).values (** mods )
548548 )
549549 except Exception as exc :
550- raise APIException (406 , f"User with id '{ idx } ' does not exist" ) from exc
550+
551+ raise HTTPException (
552+ status_code = 406 , detail = f"User with id '{ idx } ' does not exist"
553+ ) from exc
551554
552555 # Success
553556 return models .User .from_record (
@@ -571,4 +574,6 @@ async def delete_user(idx: int):
571574 await database .execute (models .users .delete ().where (models .User .id == idx ))
572575 return JSONResponse (status_code = 200 , content = {"detail" : "success" })
573576 except Exception as exc :
574- raise APIException (406 , f"User with id '{ idx } ' does not exist" ) from exc
577+ raise HTTPException (
578+ status_code = 406 , detail = f"User with id '{ idx } ' does not exist"
579+ ) from exc
0 commit comments