feat: added JWT-based authentication; introduced config file
* authorization [WIP] * refactored several sections * used dict(exclude_unset=True) * global values are now sourced from a config file (statuspagerc)
This commit is contained in:
parent
42b7bb69f1
commit
927bdd03ce
42
app/auth.py
Normal file
42
app/auth.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import bcrypt
|
||||||
|
import jwt
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from app import crud
|
||||||
|
from app.database import SessionLocal
|
||||||
|
from app.settings import globals as settings
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
KEY = settings.JWT_SECRET
|
||||||
|
ALGORITHM = settings.JWT_ALGORITHM
|
||||||
|
EXPIRY = settings.JWT_EXPIRY
|
||||||
|
|
||||||
|
|
||||||
|
def login(username: str, password: str):
|
||||||
|
db = SessionLocal()
|
||||||
|
usr = crud.login_info(db=db, name=username,password=password)
|
||||||
|
if usr is None:
|
||||||
|
return {"status": "error", "mesage": "username/password incorrect"}
|
||||||
|
description = 'access_token'
|
||||||
|
token = jwt.encode({
|
||||||
|
'sub': username,
|
||||||
|
'iat': datetime.utcnow(),
|
||||||
|
'exp': datetime.utcnow() + timedelta(seconds=EXPIRY),
|
||||||
|
'des': description
|
||||||
|
},
|
||||||
|
KEY, ALGORITHM)
|
||||||
|
return {"status": "success", "token": token.decode('utf-8')}
|
||||||
|
|
||||||
|
def validate(token):
|
||||||
|
try:
|
||||||
|
data = jwt.decode(token, KEY)
|
||||||
|
except Exception as e:
|
||||||
|
if "expired" in str(e):
|
||||||
|
raise HTTPException(status_code=401, detail={"status": "error", "message": "Token expired"})
|
||||||
|
elif "Not enough segments" in str(e):
|
||||||
|
raise HTTPException(status_code=401, detail={"status": "error", "message": "Invalid token"})
|
||||||
|
else:
|
||||||
|
raise HTTPException(status_code=400, detail={"status": "error", "message": "Exception: " + str(e)})
|
||||||
|
return data
|
||||||
|
|
22
app/crud.py
22
app/crud.py
@ -1,6 +1,6 @@
|
|||||||
import bcrypt, time
|
import bcrypt, time
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from . import models, schemas
|
from app import models, schemas
|
||||||
|
|
||||||
|
|
||||||
User=models.User
|
User=models.User
|
||||||
@ -34,7 +34,7 @@ def create_user(db: Session, user: schemas.UserCreate):
|
|||||||
|
|
||||||
|
|
||||||
def update_user(db: Session, user_id: int, user: schemas.UserUpdate):
|
def update_user(db: Session, user_id: int, user: schemas.UserUpdate):
|
||||||
updated_user = User(**user.dict())
|
updated_user = User(**user.dict(exclude_unset=True))
|
||||||
if (updated_user.password is not None):
|
if (updated_user.password is not None):
|
||||||
updated_user.password = bcrypt.hashpw(user.password.encode(), bcrypt.gensalt())
|
updated_user.password = bcrypt.hashpw(user.password.encode(), bcrypt.gensalt())
|
||||||
db.query(User).filter(User.id==user_id).update({User.password: updated_user.password})
|
db.query(User).filter(User.id==user_id).update({User.password: updated_user.password})
|
||||||
@ -74,7 +74,7 @@ def del_service(db: Session, service_id: int):
|
|||||||
|
|
||||||
|
|
||||||
def update_service(db: Session, service_id: int, s: schemas.ServiceUpdate):
|
def update_service(db: Session, service_id: int, s: schemas.ServiceUpdate):
|
||||||
updated_s = Service(**s.dict())
|
updated_s = Service(**s.dict(exclude_unset=True))
|
||||||
if (updated_s.name is not None):
|
if (updated_s.name is not None):
|
||||||
db.query(Service).filter(Service.id==service_id).update({Service.name: updated_s.name})
|
db.query(Service).filter(Service.id==service_id).update({Service.name: updated_s.name})
|
||||||
if (updated_s.owner_id is not None):
|
if (updated_s.owner_id is not None):
|
||||||
@ -93,3 +93,19 @@ def update_service(db: Session, service_id: int, s: schemas.ServiceUpdate):
|
|||||||
db.query(Service).filter(Service.id==service_id).update({Service.updated_unix: time.time()})
|
db.query(Service).filter(Service.id==service_id).update({Service.updated_unix: time.time()})
|
||||||
return db.commit()
|
return db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def login_info(db: Session, name: str, password: str):
|
||||||
|
try:
|
||||||
|
usr = db.query(User).filter(User.name == name).first()
|
||||||
|
if bcrypt.hashpw(password.encode(), usr.password) == usr.password:
|
||||||
|
print("[*] match")
|
||||||
|
else:
|
||||||
|
print("[x] no match")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print('exception', e)
|
||||||
|
return
|
||||||
|
return usr
|
||||||
|
|
||||||
|
40
app/main.py
40
app/main.py
@ -3,16 +3,18 @@
|
|||||||
|
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from fastapi import FastAPI, Depends, HTTPException
|
from fastapi import FastAPI, Depends, Security, HTTPException
|
||||||
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app import crud, models, schemas
|
from app import crud, models, schemas, auth
|
||||||
from app.database import SessionLocal, engine
|
from app.database import SessionLocal, engine
|
||||||
|
|
||||||
|
|
||||||
models.Base.metadata.create_all(bind=engine)
|
models.Base.metadata.create_all(bind=engine)
|
||||||
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='/api/v1/auth/get_token')
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI(title="Statuspage API",description="This documentation describes the Statuspage API.",version="0.0.1")
|
||||||
|
|
||||||
def get_db():
|
def get_db():
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
@ -21,7 +23,10 @@ def get_db():
|
|||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
|
def is_auth(token: str = Security(oauth2_scheme)):
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(status_code=401, detail={"status": "error", "message": "Token not provided "})
|
||||||
|
return auth.validate(token)
|
||||||
|
|
||||||
|
|
||||||
def get_usr(user_id: int, db: Session):
|
def get_usr(user_id: int, db: Session):
|
||||||
@ -49,7 +54,7 @@ async def read_services(skip: int = 0, limit: int = 100, db: Session = Depends(g
|
|||||||
return crud.get_services(db, skip=skip, limit=limit)
|
return crud.get_services(db, skip=skip, limit=limit)
|
||||||
|
|
||||||
@app.post("/api/v1/service", response_model=schemas.Service)
|
@app.post("/api/v1/service", response_model=schemas.Service)
|
||||||
async def create_service(service: schemas.ServiceCreate, db: Session = Depends(get_db)):
|
async def create_service(service: schemas.ServiceCreate, db: Session = Depends(get_db), token: str = Security(is_auth)):
|
||||||
"""Service types: icmp = 0, http = 1"""
|
"""Service types: icmp = 0, http = 1"""
|
||||||
if service.service_type not in [0, 1]:
|
if service.service_type not in [0, 1]:
|
||||||
raise HTTPException(status_code=422, detail="Invalid service type provided")
|
raise HTTPException(status_code=422, detail="Invalid service type provided")
|
||||||
@ -66,7 +71,7 @@ async def read_service(service_id: int, db: Session = Depends(get_db)):
|
|||||||
return db_service
|
return db_service
|
||||||
|
|
||||||
@app.patch("/api/v1/service/{service_id}")
|
@app.patch("/api/v1/service/{service_id}")
|
||||||
async def update_service(service_id: int, service: schemas.ServiceUpdate, db: Session = Depends(get_db)):
|
async def update_service(service_id: int, service: schemas.ServiceUpdate, db: Session = Depends(get_db), token: str = Security(is_auth)):
|
||||||
if ((service.name is None) and (service.owner_id is None) and (service.is_private is None) and (service.description is None) and (service.service_type is None) and (service.url is None) and (service.is_active is None)):
|
if ((service.name is None) and (service.owner_id is None) and (service.is_private is None) and (service.description is None) and (service.service_type is None) and (service.url is None) and (service.is_active is None)):
|
||||||
raise HTTPException(status_code=400, detail="No data provided")
|
raise HTTPException(status_code=400, detail="No data provided")
|
||||||
db_service = get_srv(service_id, db)
|
db_service = get_srv(service_id, db)
|
||||||
@ -76,7 +81,7 @@ async def update_service(service_id: int, service: schemas.ServiceUpdate, db: Se
|
|||||||
return {"update":"success"}
|
return {"update":"success"}
|
||||||
|
|
||||||
@app.delete("/api/v1/service/{service_id}")
|
@app.delete("/api/v1/service/{service_id}")
|
||||||
async def delete_service(service_id: int, db: Session = Depends(get_db)):
|
async def delete_service(service_id: int, db: Session = Depends(get_db), token: str = Security(is_auth)):
|
||||||
db_service = get_srv(service_id, db)
|
db_service = get_srv(service_id, db)
|
||||||
if db_service is None:
|
if db_service is None:
|
||||||
raise HTTPException(status_code=422, detail="Unprocessable entity")
|
raise HTTPException(status_code=422, detail="Unprocessable entity")
|
||||||
@ -86,7 +91,7 @@ async def delete_service(service_id: int, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
|
|
||||||
@app.post("/api/v1/users", response_model=schemas.User)
|
@app.post("/api/v1/users", response_model=schemas.User)
|
||||||
async def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
|
async def create_user(user: schemas.UserCreate, db: Session = Depends(get_db), token: str = Security(is_auth)):
|
||||||
db_user = crud.get_user_by_username(db, name=user.name)
|
db_user = crud.get_user_by_username(db, name=user.name)
|
||||||
if db_user:
|
if db_user:
|
||||||
raise HTTPException(status_code=400, detail="Username already registered")
|
raise HTTPException(status_code=400, detail="Username already registered")
|
||||||
@ -105,7 +110,7 @@ async def read_user(user_id: int, db: Session = Depends(get_db)):
|
|||||||
return db_user
|
return db_user
|
||||||
|
|
||||||
@app.patch("/api/v1/users/{user_id}")
|
@app.patch("/api/v1/users/{user_id}")
|
||||||
async def update_user(user_id: int, user: schemas.UserUpdate, db: Session = Depends(get_db)):
|
async def update_user(user_id: int, user: schemas.UserUpdate, db: Session = Depends(get_db), token: str = Security(is_auth)):
|
||||||
if ((user.name is None) and (user.password is None) and (user.full_name is None) and (user.is_active is None)):
|
if ((user.name is None) and (user.password is None) and (user.full_name is None) and (user.is_active is None)):
|
||||||
raise HTTPException(status_code=400, detail="No data provided")
|
raise HTTPException(status_code=400, detail="No data provided")
|
||||||
db_user = get_usr(user_id, db)
|
db_user = get_usr(user_id, db)
|
||||||
@ -115,10 +120,25 @@ async def update_user(user_id: int, user: schemas.UserUpdate, db: Session = Depe
|
|||||||
return {"update":"success"}
|
return {"update":"success"}
|
||||||
|
|
||||||
@app.delete("/api/v1/users/{user_id}")
|
@app.delete("/api/v1/users/{user_id}")
|
||||||
async def delete_user(user_id: int, db: Session = Depends(get_db)):
|
async def delete_user(user_id: int, db: Session = Depends(get_db), token: str = Security(is_auth)):
|
||||||
db_user = get_usr(user_id, db)
|
db_user = get_usr(user_id, db)
|
||||||
if db_user is None:
|
if db_user is None:
|
||||||
raise HTTPException(status_code=422, detail="Unprocessable entity")
|
raise HTTPException(status_code=422, detail="Unprocessable entity")
|
||||||
crud.del_user(db=db, user_id=user_id)
|
crud.del_user(db=db, user_id=user_id)
|
||||||
return {"delete":"success"}
|
return {"delete":"success"}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.post('/api/v1/auth/get_token',response_model=schemas.Token,response_description="Returns user access token",summary="Authenticate API user",description="Authenticate an API user and return a token for subsequent requests")
|
||||||
|
async def get_token(form_data: OAuth2PasswordRequestForm = Depends()):
|
||||||
|
a = auth.login(form_data.username, form_data.password)
|
||||||
|
if a and a["status"] == "error":
|
||||||
|
raise HTTPException(status_code=400, detail={"status": "error", "mesage": "username/password incorrect"})
|
||||||
|
return {"access_token": a["token"], "token_type": "bearer"}
|
||||||
|
|
||||||
|
@app.post('/api/v1/auth/validate')
|
||||||
|
async def validate_token(token: str):
|
||||||
|
a = auth.validate(token=token)
|
||||||
|
if a:
|
||||||
|
return a
|
||||||
|
|
||||||
|
@ -70,3 +70,7 @@ class APISimpleErrorResponse(BaseModel):
|
|||||||
success: bool = False
|
success: bool = False
|
||||||
errors: Optional[List[str]]
|
errors: Optional[List[str]]
|
||||||
|
|
||||||
|
class Token(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str
|
||||||
|
|
||||||
|
0
app/settings/__init__.py
Normal file
0
app/settings/__init__.py
Normal file
19
app/settings/globals.py
Normal file
19
app/settings/globals.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from starlette.config import Config
|
||||||
|
|
||||||
|
|
||||||
|
p: Path = Path(__file__).parents[2] / "statuspagerc"
|
||||||
|
config: Config = Config(p if p.exists() else None)
|
||||||
|
|
||||||
|
DATABASE: str = config("DATABASE", cast=str)
|
||||||
|
|
||||||
|
ALEMBIC_CONFIG: str = (
|
||||||
|
DATABASE
|
||||||
|
)
|
||||||
|
|
||||||
|
JWT_SECRET: str = config("JWT_SECRET", cast=str)
|
||||||
|
JWT_ALGORITHM: str = config("JWT_ALGORITHM", cast=str)
|
||||||
|
JWT_EXPIRY: int = config("JWT_EXPIRY", cast=int)
|
||||||
|
|
12
statuspagerc
Normal file
12
statuspagerc
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# statuspage configuration file
|
||||||
|
|
||||||
|
# database file
|
||||||
|
DATABASE=sqlite:///sql_app.db
|
||||||
|
|
||||||
|
# [JWT related parameters]
|
||||||
|
# it's imperative you CHANGE JWT_SECRET to a unique value
|
||||||
|
# you can generate a random one with 'openssl rand -hex 64'
|
||||||
|
JWT_SECRET = 'deadbeefdeafbeefd34db33fdeadbeefdeafbeefd34db33fdeadbeefdeafbeefd34db33f'
|
||||||
|
JWT_ALGORITHM = "HS256"
|
||||||
|
# token expiration time in seconds
|
||||||
|
JWT_EXPIRY = 7200
|
Reference in New Issue
Block a user