Skip to content

Commit b91c469

Browse files
authored
Merge pull request #3 from MO-RISE/login_prompt
verify_eqmx and bugs
2 parents cbcda39 + 9a80da7 commit b91c469

12 files changed

Lines changed: 569 additions & 374 deletions

.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
USER_DATABASE_URL=postgresql://admin:password@localhost:5432/users
22
ADMIN_USER_PASSWORD=password
3+
JWT_TOKEN_SECRET=thisisatokenofmyappreciation
34
ACCESS_COOKIE_DOMAIN=''
45
BASE_URL=/auth/api
56
DEVELOPMENT=true

backend/exceptions.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,9 @@
33
"""
44

55

6-
class APIException(Exception):
7-
"""Custom API Exception"""
8-
9-
def __init__(self, status_code: int, message: str):
10-
super().__init__()
11-
self.status_code = status_code
12-
self.message = message
13-
14-
156
class VerifyException(Exception):
167
"""Custom Verify Exception"""
178

189
def __init__(self, message: str):
19-
super().__init__()
10+
# super().__init__()
2011
self.message = message

backend/main.py

Lines changed: 59 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Crow's Nest Auth microservice"""
2-
import os
2+
33
import logging
44
from typing import Dict, Tuple, List
55
from datetime import datetime, timedelta
@@ -22,9 +22,10 @@
2222
from . import schemas
2323
from . import models
2424
from .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

2930
LOGGER = logging.getLogger(__name__)
3031

@@ -40,7 +41,7 @@
4041
)
4142
ACCESS_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

4546
USER_DATABASE_URL = env("USER_DATABASE_URL")
4647
ADMIN_USER_USERNAME = env("ADMIN_USERNAME", "admin")
@@ -73,20 +74,14 @@
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)
8378
async 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

154149
async 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):
492492
async 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

dev-service-config.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ http:
77
#rule: "Path(`/auth`,'/auth/')"
88

99
to-auth:
10-
service: auth
10+
service: crowsnest-auth
1111
middlewares:
1212
- "auth-api-strip"
1313
rule: "Path(`/auth/api/{case:[a-z0-9/]+}`)"
@@ -28,7 +28,7 @@ http:
2828
loadBalancer:
2929
servers:
3030
- url: "http://host.docker.internal:3000"
31-
auth:
31+
crowsnest-auth:
3232
loadBalancer:
3333
servers:
3434
- url: "http://host.docker.internal:8000"

docker-compose.dev.yml

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
version: '3'
22
services:
33

4+
45
traefik:
56
image: "traefik:v2.4"
67
command:
78
- "--log.level=DEBUG"
8-
- "--providers.docker=true"
9-
- "--api.dashboard=true"
109
- "--api.insecure=true"
11-
- "--providers.file.filename=/dev-service-config.yml"
10+
- "--api.dashboard=true"
11+
- "--providers.docker=true"
1212
- "--providers.docker.exposedbydefault=true"
1313
- "--entrypoints.web.address=:80"
1414
- "--entrypoints.web-secure.address=:443"
15-
- "--entrypoints.mqtt.address=:1883"
16-
- "--entrypoints.traefik-dashboard.address=:8080"
15+
- "--providers.file.filename=/dev-service-config.yml"
1716

1817
ports:
1918
- 80:80
2019
- 443:443
21-
- 1883:1883
20+
- 8080:8080
2221

2322
volumes:
2423
- "/var/run/docker.sock:/var/run/docker.sock:ro"
@@ -41,20 +40,31 @@ services:
4140
- POSTGRES_DB=users
4241

4342
emqx:
44-
image: emqx/emqx:4.4.4
43+
image: emqx/emqx
44+
labels:
45+
- "traefik.http.routers.emqx-ws.rule=PathPrefix(`/mqtt`)"
46+
- "traefik.http.routers.emqx-ws.service=emqx-ws"
47+
- "traefik.http.services.emqx-ws.loadbalancer.server.port=8083"
4548
ports:
46-
- 8083:8083
47-
- 8001:18083
49+
- 18083:18083
50+
restart: unless-stopped
51+
depends_on:
52+
- traefik
4853
environment:
49-
- EMQX_NAME=test
50-
- EMQX_LOADED_PLUGINS="emqx_recon,emqx_retainer,emqx_management,emqx_dashboard,emqx_auth_http"
51-
- EMQX_AUTH__HTTP__AUTH_REQ__URL=http://crowsnest-auth/login
54+
- EMQX_NAME=dev-emqx
55+
- EMQX_LOG__LEVEL=debug
56+
- EMQX_LOG__TO=console
57+
- EMQX_LOADED_PLUGINS="emqx_recon,emqx_retainer,emqx_management,emqx_dashboard,emqx_auth_jwt,emqx_auth_http"
58+
- EMQX_AUTH__HTTP__AUTH_REQ__URL=http://host.docker.internal:8000/login
5259
- EMQX_AUTH__HTTP__AUTH_REQ__METHOD=post
5360
- EMQX_AUTH__HTTP__AUTH_REQ__HEADERS__CONTENT-TYPE=application/x-www-form-urlencoded
5461
- EMQX_AUTH__HTTP__AUTH_REQ__PARAMS=username=%u,password=%P
55-
- EMQX_AUTH__HTTP__ACL_REQ__URL=http://crowsnest-auth/verify_emqx
62+
- EMQX_AUTH__HTTP__ACL_REQ__URL=http://host.docker.internal:8000/verify_emqx
5663
- EMQX_AUTH__HTTP__ACL_REQ__METHOD=get
5764
- EMQX_AUTH__HTTP__ACL_REQ__PARAMS=username=%u,topic=%t
65+
- EMQX_AUTH__JWT__SECRET=${JWT_TOKEN_SECRET}
66+
- EMQX_AUTH__JWT__FROM=username
67+
- EMQX_AUTH__JWT__VERIFY_CLAIMS=off
5868

5969

6070

0 commit comments

Comments
 (0)