Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
e871330041
|
|||
|
2d64edde64
|
|||
|
17b99041db
|
|||
|
17842a14fd
|
|||
|
bc663582b5
|
|||
|
5f0e5b1bbb
|
|||
|
62bbbec1d3
|
|||
|
336c6aa34e
|
|||
|
5e8c826714
|
|||
|
9985251584
|
|||
|
871e3f4d0a
|
|||
|
427e47534a
|
|||
| d00c5855b7 | |||
| 44ea17d87a | |||
|
9160b312c7
|
|||
|
07669fc1fc
|
|||
|
94d428c9d0
|
|||
|
a4bedba628
|
|||
|
e6a248529b
|
|||
|
713d41e81c
|
|||
|
6ec3bc5f14
|
|||
|
9e6510f465
|
|||
|
5a6cd970dd
|
|||
|
3d1893d84e
|
|||
|
495535a6de
|
|||
|
30b86dad5e
|
|||
|
92eef82e0a
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,3 +2,4 @@ __pycache__
|
|||||||
gatekeeper.db
|
gatekeeper.db
|
||||||
.env
|
.env
|
||||||
.coverage
|
.coverage
|
||||||
|
result
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -10,8 +10,19 @@ start prod server `uv run fastapi run`
|
|||||||
You need to set services.pcscd.enable = true; for the smartcard reader to work
|
You need to set services.pcscd.enable = true; for the smartcard reader to work
|
||||||
|
|
||||||
Issues:
|
Issues:
|
||||||
|
- documentation missing
|
||||||
- `nix run` currently broken
|
- `nix run` currently broken
|
||||||
|
- raspberry pi image not working
|
||||||
- no door state
|
- no door state
|
||||||
- no door operations
|
- no door operations
|
||||||
- card system is dummy until I get hardware
|
- hardcoded secret key in auth.py -> centralise env var loading
|
||||||
|
- i don't like the error handling in the scanner - doesn't pass errors correctly
|
||||||
|
- cors for frontend: https://fastapi.tiangolo.com/tutorial/cors
|
||||||
|
- Load cors from env var or something
|
||||||
|
- BackgroundScanner shouldn't get a single session for the whole lifecycle
|
||||||
|
- input validation maybe
|
||||||
|
- too many imports
|
||||||
|
- inconsistent logging (request logging?)
|
||||||
|
- rate limiting maybe
|
||||||
|
- pretty sure the controllers are doing too much stuff
|
||||||
|
- hardware call tests or sth
|
||||||
9
app/.env.example
Normal file
9
app/.env.example
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
MIFARE_APP_MASTER_KEY="7b195adb892073c9e34ebe7e0ca28b2b"
|
||||||
|
# 16 bytes AES key
|
||||||
|
MIFARE_ACL_READ_BASE_KEY="741a50f2b189cc9f151e0600f50a6e1a"
|
||||||
|
# 16 bytes AES key
|
||||||
|
MIFARE_ACL_WRITE_BASE_KEY="f1aa99f81cca268de98d422ee0ccb65c"
|
||||||
|
# 16 bytes AES key
|
||||||
|
SECRET_KEY="8b14d0b447bff7efa24d5019cc59a999786e31f6f865173bbd642bf18de5ad85"
|
||||||
|
#Key for oauth
|
||||||
|
#THESE ARE TESTING KEYS - DO NOT USE IN PROD
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
from typing import List
|
from typing import List
|
||||||
@@ -8,11 +10,11 @@ from ..services.database import engine, get_session, add_and_refresh
|
|||||||
from ..services.auth import auth_is_admin
|
from ..services.auth import auth_is_admin
|
||||||
import uuid as gen_uuid
|
import uuid as gen_uuid
|
||||||
|
|
||||||
aa_router = APIRouter(prefix="/aa", tags=["AccessAuth"])
|
aa_router = APIRouter(prefix="/api/v1/aa", tags=["AccessAuth"])
|
||||||
|
|
||||||
@aa_router.post("/", response_model=AccessAuthorizationResponse)
|
@aa_router.post("/", response_model=AccessAuthorizationResponse)
|
||||||
def add_accessauth(*, db: Session = Depends(get_session), aa: AccessAuthorizationCreate, admin: bool = Depends(auth_is_admin)):
|
def add_accessauth(*, db: Session = Depends(get_session), aa: AccessAuthorizationCreate, admin: bool = Depends(auth_is_admin)):
|
||||||
print("Creating accessauth with data: ", aa)
|
logger.info(f"Creating accessauth with data: {aa}")
|
||||||
timetables = [Timetable.model_validate(t) for t in aa.timetables]
|
timetables = [Timetable.model_validate(t) for t in aa.timetables]
|
||||||
db_aa = AccessAuthorizationDB(
|
db_aa = AccessAuthorizationDB(
|
||||||
name=aa.name,
|
name=aa.name,
|
||||||
@@ -44,7 +46,7 @@ def assign_accessauth(*, db: Session = Depends(get_session), group_id: int, aa_i
|
|||||||
if db_aa is None:
|
if db_aa is None:
|
||||||
raise HTTPException(status_code=404, detail="AA not found")
|
raise HTTPException(status_code=404, detail="AA not found")
|
||||||
if db_aa in db_group.accessauths:
|
if db_aa in db_group.accessauths:
|
||||||
raise HTTPException(status_code=200, detail="AA already assigned to group")
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="AA already assigned to group")
|
||||||
db_group.accessauths.append(db_aa)
|
db_group.accessauths.append(db_aa)
|
||||||
return add_and_refresh(db, db_group)
|
return add_and_refresh(db, db_group)
|
||||||
|
|
||||||
@@ -57,7 +59,7 @@ def unassign_accessauth(*, db: Session = Depends(get_session), group_id: int, aa
|
|||||||
if db_aa is None:
|
if db_aa is None:
|
||||||
raise HTTPException(status_code=404, detail="AA not found")
|
raise HTTPException(status_code=404, detail="AA not found")
|
||||||
if db_aa not in db_group.accessauths:
|
if db_aa not in db_group.accessauths:
|
||||||
raise HTTPException(status_code=200, detail="AA not assigned to group")
|
raise HTTPException(status_code=404, detail="AA not assigned to group")
|
||||||
db_group.accessauths.remove(db_aa)
|
db_group.accessauths.remove(db_aa)
|
||||||
return add_and_refresh(db, db_group)
|
return add_and_refresh(db, db_group)
|
||||||
|
|
||||||
@@ -66,7 +68,12 @@ def change_accessauth(*, db: Session = Depends(get_session), aa_id: int, aa: Acc
|
|||||||
db_aa = db.get(AccessAuthorizationDB, aa_id)
|
db_aa = db.get(AccessAuthorizationDB, aa_id)
|
||||||
if db_aa is None:
|
if db_aa is None:
|
||||||
raise HTTPException(status_code=404, detail="AccessAuthorization not found")
|
raise HTTPException(status_code=404, detail="AccessAuthorization not found")
|
||||||
aa_data = aa.dict(exclude_unset=True)
|
aa_data = aa.model_dump(exclude_unset=True)
|
||||||
|
if "timetables" in aa_data and aa_data["timetables"] is not None:
|
||||||
|
db_aa.timetables.clear()
|
||||||
|
timetables = [Timetable.model_validate(t) for t in aa_data["timetables"]]
|
||||||
|
db_aa.timetables = timetables
|
||||||
|
aa_data.pop("timetables")
|
||||||
db_aa.sqlmodel_update(aa_data)
|
db_aa.sqlmodel_update(aa_data)
|
||||||
return add_and_refresh(db, db_aa)
|
return add_and_refresh(db, db_aa)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from sqlalchemy.exc import NoResultFound
|
||||||
|
|
||||||
from ..model.models import Card
|
from ..model.models import Card
|
||||||
from ..services.database import engine, get_session, add_and_refresh
|
from ..services.database import engine, get_session, add_and_refresh
|
||||||
@@ -8,12 +11,12 @@ from ..services.auth import auth_is_admin
|
|||||||
import uuid as gen_uuid
|
import uuid as gen_uuid
|
||||||
from app.services.scanner import WriteNewCard, DeleteCard
|
from app.services.scanner import WriteNewCard, DeleteCard
|
||||||
|
|
||||||
card_router = APIRouter(prefix="/cards", tags=["Card"])
|
card_router = APIRouter(prefix="/api/v1/cards", tags=["Card"])
|
||||||
|
|
||||||
def register_card(group_id: int):
|
def register_card(group_id: int):
|
||||||
key = WriteNewCard()
|
key = WriteNewCard()
|
||||||
if key == None:
|
if key == None:
|
||||||
print("No card registered. Check logs!")
|
logger.info("No card registered. Check logs!")
|
||||||
raise HTTPException(status.HTTP_417_EXPECTATION_FAILED, detail="No card registered. Check logs!")
|
raise HTTPException(status.HTTP_417_EXPECTATION_FAILED, detail="No card registered. Check logs!")
|
||||||
card = Card(group_id=group_id, uuid=key)
|
card = Card(group_id=group_id, uuid=key)
|
||||||
return card
|
return card
|
||||||
@@ -26,13 +29,16 @@ def add_card(*, db: Session = Depends(get_session), group_id: int, admin: bool =
|
|||||||
@card_router.get("/delete")
|
@card_router.get("/delete")
|
||||||
def del_card(*, db: Session = Depends(get_session), admin: bool = Depends(auth_is_admin)):
|
def del_card(*, db: Session = Depends(get_session), admin: bool = Depends(auth_is_admin)):
|
||||||
key = DeleteCard()
|
key = DeleteCard()
|
||||||
# card = db.get(Card, card_id)
|
logger.info(key)
|
||||||
# if card is None:
|
try:
|
||||||
# raise HTTPException(status_code=404, detail="Card not found")
|
card = db.exec(select(Card).where(Card.uuid == key)).one()
|
||||||
# db.delete(card)
|
except NoResultFound:
|
||||||
# db.commit()
|
logger.info(f"The key:'{key}' was not found in db!")
|
||||||
# return {"message": "Card deleted successfully"}
|
raise HTTPException(status_code=500, detail="Key on card not found in DB. Please tell an admin about this. KEY={key}")
|
||||||
##TBH not a big fan of having creation using group_id but deletion using card_id
|
db.delete(card)
|
||||||
|
db.commit()
|
||||||
|
return {"message": "Card deleted successfully"}
|
||||||
|
|
||||||
@card_router.get("/{group_id}", response_model=List[Card])
|
@card_router.get("/{group_id}", response_model=List[Card])
|
||||||
def get_cards(*, db: Session = Depends(get_session), group_id: int, admin: bool = Depends(auth_is_admin)):
|
def get_cards(*, db: Session = Depends(get_session), group_id: int, admin: bool = Depends(auth_is_admin)):
|
||||||
cards = db.exec(select(Card).where(Card.group_id == group_id)).all()
|
cards = db.exec(select(Card).where(Card.group_id == group_id)).all()
|
||||||
|
|||||||
37
app/controllers/debugManager.py
Normal file
37
app/controllers/debugManager.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from app.services.auth import auth_is_admin
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
from app.model.models import *
|
||||||
|
from app.services.database import get_session, add_and_refresh
|
||||||
|
|
||||||
|
debug_router = APIRouter(
|
||||||
|
prefix="/api/v1/debug",
|
||||||
|
tags=["Debug items - maybe dont show this in UI"],
|
||||||
|
dependencies=[Depends(auth_is_admin)],
|
||||||
|
)
|
||||||
|
|
||||||
|
@debug_router.get("/addcard/{groupid}/{card_key}")
|
||||||
|
def add_card_manually(groupid: int, card_key: str, db: Session=Depends(get_session)):
|
||||||
|
"""Add cards manually (you also have to delete them manually)"""
|
||||||
|
logger.critical(f"Manual db change: adding a card with key: {card_key} to group: {groupid}")
|
||||||
|
card = Card(group_id=groupid, uuid=card_key)
|
||||||
|
add_and_refresh(db, card)
|
||||||
|
return card
|
||||||
|
|
||||||
|
@debug_router.get("/rmcard/{card_key}")
|
||||||
|
def remove_card_manually(card_key: str, db: Session = Depends(get_session)):
|
||||||
|
logger.critical(f"Manual db change: removing a card with key: {card_key}")
|
||||||
|
card = db.exec(select(Card).where(Card.uuid == card_key)).one()
|
||||||
|
add_and_refresh(db, card)
|
||||||
|
|
||||||
|
@debug_router.put("/getcards")
|
||||||
|
def list_all_cards(db: Session = Depends(get_session)):
|
||||||
|
logger.info(f"Debug Setting: Getting cards.")
|
||||||
|
cards = db.exec(select(Card)).all()
|
||||||
|
print(cards)
|
||||||
|
out = []
|
||||||
|
for i in cards:
|
||||||
|
out.append({i.uuid: i.group.name})
|
||||||
|
return out
|
||||||
@@ -5,7 +5,7 @@ from app.services.database import get_session
|
|||||||
from app.services.auth import auth_is_admin
|
from app.services.auth import auth_is_admin
|
||||||
import app.services.door as doorService
|
import app.services.door as doorService
|
||||||
|
|
||||||
door_router = APIRouter(prefix="/door",tags=["Door"])
|
door_router = APIRouter(prefix="/api/v1/door",tags=["Door"])
|
||||||
|
|
||||||
@door_router.put("/open")
|
@door_router.put("/open")
|
||||||
def open_door(db: Session = Depends(get_session), admin: bool = Depends(auth_is_admin)):
|
def open_door(db: Session = Depends(get_session), admin: bool = Depends(auth_is_admin)):
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from ..model.models import GroupDB, GroupResponse, GroupCreate
|
|||||||
from ..services.database import engine, get_session, add_and_refresh
|
from ..services.database import engine, get_session, add_and_refresh
|
||||||
from ..services.auth import auth_is_admin
|
from ..services.auth import auth_is_admin
|
||||||
|
|
||||||
group_router = APIRouter(prefix="/groups", tags=["Group"])
|
group_router = APIRouter(prefix="/api/v1/groups", tags=["Group"])
|
||||||
|
|
||||||
@group_router.get("/", response_model=List[GroupResponse])
|
@group_router.get("/", response_model=List[GroupResponse])
|
||||||
def get_groups(*, db: Session = Depends(get_session), admin: bool = Depends(auth_is_admin)):
|
def get_groups(*, db: Session = Depends(get_session), admin: bool = Depends(auth_is_admin)):
|
||||||
|
|||||||
@@ -1,33 +1,39 @@
|
|||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
from fastapi import APIRouter, HTTPException, Depends
|
from fastapi import APIRouter, HTTPException, Depends
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from ..model.models import UserResponse, UserCreate, UserDB, UserUpdate
|
from ..model.models import UserResponse, UserCreate, UserDB, UserUpdate
|
||||||
from ..services.database import engine, get_session, add_and_refresh
|
from ..services.database import engine, get_session, add_and_refresh
|
||||||
from ..services.auth import get_password_hash, get_current_user, auth_is_admin
|
from ..services.auth import get_password_hash, get_current_user as auth_user, auth_is_admin
|
||||||
|
|
||||||
user_router = APIRouter(tags=["Users"])
|
user_router = APIRouter(tags=["Users"], prefix="/api/v1/users")
|
||||||
|
|
||||||
@user_router.post("/users/", response_model=UserResponse)
|
@user_router.post("/", response_model=UserResponse)
|
||||||
def create_user(*, db: Session = Depends(get_session), user: UserCreate, admin: bool = Depends(auth_is_admin)):
|
def create_user(*, db: Session = Depends(get_session), user: UserCreate, admin: bool = Depends(auth_is_admin)):
|
||||||
print("creating user with data ", user)
|
logger.info(f"creating user with data: {user}")
|
||||||
hashed_password = {"passwordhash": get_password_hash(user.password)}
|
hashed_password = {"passwordhash": get_password_hash(user.password)}
|
||||||
db_user = UserDB.model_validate(user, update=hashed_password)
|
db_user = UserDB.model_validate(user, update=hashed_password)
|
||||||
return add_and_refresh(db, db_user)
|
return add_and_refresh(db, db_user)
|
||||||
|
|
||||||
@user_router.get("/users/", response_model=List[UserResponse])
|
@user_router.get("/", response_model=List[UserResponse])
|
||||||
def read_users(*, db: Session = Depends(get_session), admin: bool = Depends(auth_is_admin)):
|
def read_users(*, db: Session = Depends(get_session), admin: bool = Depends(auth_is_admin)):
|
||||||
users = db.exec(select(UserDB)).all()
|
users = db.exec(select(UserDB)).all()
|
||||||
return users
|
return users
|
||||||
|
|
||||||
@user_router.get("/users/{user_id}", response_model=UserResponse)
|
@user_router.get("/current", response_model=UserResponse)
|
||||||
|
def get_current_user(db: Session = Depends(get_session), user: UserDB = Depends(auth_user)):
|
||||||
|
return user
|
||||||
|
|
||||||
|
@user_router.get("/{user_id}", response_model=UserResponse)
|
||||||
def read_user(*, db: Session = Depends(get_session), user_id: int, admin: bool = Depends(auth_is_admin)):
|
def read_user(*, db: Session = Depends(get_session), user_id: int, admin: bool = Depends(auth_is_admin)):
|
||||||
db_user = db.get(UserDB, user_id)
|
db_user = db.get(UserDB, user_id)
|
||||||
if db_user is None:
|
if db_user is None:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
return db_user
|
return db_user
|
||||||
|
|
||||||
@user_router.patch("/users/{user_id}", response_model=UserResponse)
|
@user_router.patch("/{user_id}", response_model=UserResponse)
|
||||||
def update_user(*, db: Session = Depends(get_session), user_id: int, user: UserUpdate, admin: bool = Depends(auth_is_admin)):
|
def update_user(*, db: Session = Depends(get_session), user_id: int, user: UserUpdate, admin: bool = Depends(auth_is_admin)):
|
||||||
db_user = db.get(UserDB, user_id)
|
db_user = db.get(UserDB, user_id)
|
||||||
if db_user is None:
|
if db_user is None:
|
||||||
@@ -40,11 +46,12 @@ def update_user(*, db: Session = Depends(get_session), user_id: int, user: UserU
|
|||||||
db_user.sqlmodel_update(user_data, update=hashed_password)
|
db_user.sqlmodel_update(user_data, update=hashed_password)
|
||||||
return add_and_refresh(db, db_user)
|
return add_and_refresh(db, db_user)
|
||||||
|
|
||||||
@user_router.delete("/users/{user_id}")
|
@user_router.delete("/{user_id}")
|
||||||
def delete_user(*, db: Session = Depends(get_session), user_id: int, admin: bool = Depends(auth_is_admin)):
|
def delete_user(*, db: Session = Depends(get_session), user_id: int, admin: bool = Depends(auth_is_admin)):
|
||||||
db_user = db.get(UserDB, user_id)
|
db_user = db.get(UserDB, user_id)
|
||||||
if db_user is None:
|
if db_user is None:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
db.delete(db_user)
|
db.delete(db_user)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"message": "User deleted successfully"}
|
return {"message": "User deleted successfully"}
|
||||||
|
|
||||||
|
|||||||
50
app/main.py
50
app/main.py
@@ -1,33 +1,65 @@
|
|||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
import os
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.security import OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
from .controllers import userManager, cardManager, groupManager, aaManager, doorManager
|
from .controllers import userManager, cardManager, groupManager, aaManager, doorManager, debugManager
|
||||||
from .services.database import create_db_and_tables, get_session
|
from .services.database import create_db_and_tables, get_db_session
|
||||||
from .services.auth import token_router, create_first_user
|
from .services.auth import token_router, create_first_user
|
||||||
from app.services.scanner import BackgroundScanner
|
from app.services.scanner import BackgroundScanner
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||||
scanner = BackgroundScanner(db=get_session())
|
scanner = BackgroundScanner(db=get_db_session())
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
def checkDeps():
|
||||||
|
load_dotenv()
|
||||||
|
MIFARE_APP_MASTER_KEY = os.getenv('MIFARE_APP_MASTER_KEY')
|
||||||
|
if not MIFARE_APP_MASTER_KEY:
|
||||||
|
logger.critical(f"MIFARE APP MASTER KEY not found!")
|
||||||
|
logger.critical("Writing and reading cards is disabled!")
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
load_dotenv()
|
checkDeps()
|
||||||
create_db_and_tables()
|
create_db_and_tables()
|
||||||
create_first_user()
|
create_first_user(db=get_db_session())
|
||||||
print("Database created and tables initialized.")
|
logger.info("Database created and tables initialized.")
|
||||||
scanner.start()
|
disableCards = os.getenv("DISABLE_CARDS")
|
||||||
|
if not disableCards:
|
||||||
|
scanner.start()
|
||||||
yield
|
yield
|
||||||
#scanner.stop()
|
#scanner.stop()
|
||||||
|
|
||||||
app = FastAPI(lifespan=lifespan)
|
app = FastAPI(
|
||||||
|
lifespan=lifespan,
|
||||||
|
docs_url="/api/v1/docs",
|
||||||
|
openapi_url="/api/v1/openapi.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
origins = [
|
||||||
|
"http://127.0.0.1",
|
||||||
|
"http://localhost",
|
||||||
|
"http://localhost:8080",
|
||||||
|
]
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=origins,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["GET" "PUT" "POST" "DELETE" "PATCH"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
app.include_router(token_router)
|
app.include_router(token_router)
|
||||||
app.include_router(userManager.user_router)
|
app.include_router(userManager.user_router)
|
||||||
app.include_router(groupManager.group_router)
|
app.include_router(groupManager.group_router)
|
||||||
app.include_router(cardManager.card_router)
|
app.include_router(cardManager.card_router)
|
||||||
app.include_router(aaManager.aa_router)
|
app.include_router(aaManager.aa_router)
|
||||||
app.include_router(doorManager.door_router)
|
app.include_router(doorManager.door_router)
|
||||||
|
app.include_router(debugManager.debug_router)
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from fastapi import APIRouter, HTTPException, Depends, status
|
from fastapi import APIRouter, HTTPException, Depends, status
|
||||||
@@ -8,15 +10,18 @@ import jwt
|
|||||||
from jwt.exceptions import InvalidTokenError
|
from jwt.exceptions import InvalidTokenError
|
||||||
from ..model.models import UserDB, Token, TokenData, UserCreate
|
from ..model.models import UserDB, Token, TokenData, UserCreate
|
||||||
from ..services.database import *
|
from ..services.database import *
|
||||||
import secrets, string
|
import secrets, string, os
|
||||||
|
|
||||||
SECRET_KEY = "8b14d0b447bff7efa24d5019cc59a999786e31f6f865173bbd642bf18de5ad85" #Encrypt and change later or store in env file or somehthing
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
SECRET_KEY = os.getenv("SECRET_KEY", default="ff"*16)
|
||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
ACCESS_TOKEN_EXPIRE_MINUTES = 120
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/token")
|
||||||
|
|
||||||
token_router = APIRouter(tags=["Token"])
|
token_router = APIRouter(tags=["Token"], prefix="/api/v1")
|
||||||
|
|
||||||
password_hash = PasswordHash.recommended()
|
password_hash = PasswordHash.recommended()
|
||||||
|
|
||||||
@@ -83,20 +88,19 @@ def auth_is_admin(
|
|||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def create_first_user():
|
def create_first_user(db: Session):
|
||||||
print("Checking for admin user")
|
logger.info("Checking for admin user")
|
||||||
with Session(engine) as db:
|
admin_user = db.exec(select(UserDB)).first()
|
||||||
admin_user = db.exec(select(UserDB)).first()
|
if admin_user is None:
|
||||||
if admin_user is None:
|
password = ''.join(secrets.choice(string.digits) for i in range(8))
|
||||||
password = ''.join(secrets.choice(string.digits) for i in range(8))
|
logger.info(f"Creating first admin user with password: {password}")
|
||||||
print("Creating first admin user with password", password)
|
user = UserDB(
|
||||||
user = UserDB(
|
name="admin",
|
||||||
name="admin",
|
passwordhash=get_password_hash(password),
|
||||||
passwordhash=get_password_hash(password),
|
is_admin=True
|
||||||
is_admin=True
|
)
|
||||||
)
|
return add_and_refresh(db, user)
|
||||||
return add_and_refresh(db, user)
|
logger.info(f"Admin user already exists: {admin_user.name}")
|
||||||
print(f"Admin user already exists: {admin_user.name}")
|
|
||||||
|
|
||||||
|
|
||||||
@token_router.post("/token")
|
@token_router.post("/token")
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ def get_session():
|
|||||||
with Session(engine) as db:
|
with Session(engine) as db:
|
||||||
yield db
|
yield db
|
||||||
|
|
||||||
|
def get_db_session():
|
||||||
|
return Session(engine)
|
||||||
|
|
||||||
def add_and_refresh(db: Session, obj):
|
def add_and_refresh(db: Session, obj):
|
||||||
db.add(obj)
|
db.add(obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
|
import logging
|
||||||
import paho.mqtt.client as mqttClient
|
logger = logging.getLogger(__name__)
|
||||||
import paho.mqtt.publish as publish
|
|
||||||
|
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
from fastapi import Depends, HTTPException, status
|
from fastapi import Depends, HTTPException, status
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
@@ -12,43 +10,43 @@ from app.services.database import Session, get_session
|
|||||||
from app.model.models import *
|
from app.model.models import *
|
||||||
|
|
||||||
doorIsOpen = True
|
doorIsOpen = True
|
||||||
client = mqttClient.Client(client_id="", userdata=None, protocol=mqttClient.MQTTv5)
|
|
||||||
client.tls_set(tls_version=mqttClient.ssl.PROTOCOL_TLS)
|
|
||||||
client.username_pw_set("username", "passwort")
|
|
||||||
#client.connect("host", port=8883)
|
|
||||||
# I think this could also be gpio controlled
|
# I think this could also be gpio controlled
|
||||||
#See: https://github.com/technyon/nuki_hub#gpio-lock-control-optional
|
#See: https://github.com/technyon/nuki_hub#gpio-lock-control-optional
|
||||||
|
|
||||||
def openDoor():
|
def openDoor():
|
||||||
|
global doorIsOpen
|
||||||
doorIsOpen = True
|
doorIsOpen = True
|
||||||
publish.single(topic="/lock/action", payload="unlock")
|
logger.info("Still needs gpio out")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def closeDoor():
|
def closeDoor():
|
||||||
|
global doorIsOpen
|
||||||
doorIsOpen = False
|
doorIsOpen = False
|
||||||
publish.single(topic="/lock/action", payload="lock")
|
logger.info("Still needs gpio out")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def isDoorOpen():
|
def isDoorOpen():
|
||||||
return doorIsOpen
|
return doorIsOpen
|
||||||
|
|
||||||
def checkAccess(uuid: str, db: Session = Depends(get_session)):
|
def checkAccess(uuid: str, db: Session):
|
||||||
try:
|
try:
|
||||||
current_weekday = datetime.datetime.weekday(datetime.date.today())
|
current_weekday = datetime.datetime.weekday(datetime.date.today())
|
||||||
current_time = datetime.datetime.now()
|
current_time = datetime.datetime.now()
|
||||||
card = db.exec(select(Card).where(Card.uuid == uuid)).one()
|
card = db.exec(select(Card).where(Card.uuid == uuid)).one()
|
||||||
for auth in card.group.accessauths:
|
for auth in card.group.accessauths:
|
||||||
print(f"checking auth: {auth.name}")
|
logger.info(f"checking auth: {auth.name}")
|
||||||
for timetable in auth.timetables:
|
for timetable in auth.timetables:
|
||||||
print(f" checking timetable {timetable.id}")
|
logger.info(f" checking timetable {timetable.id}")
|
||||||
print(f" comparing weekday: CUR:{current_weekday} TT:{timetable.weekday}")
|
logger.info(f" comparing weekday: CUR:{current_weekday} TT:{timetable.weekday}")
|
||||||
if current_weekday == timetable.weekday:
|
if current_weekday == timetable.weekday:
|
||||||
starttime = datetime.datetime.combine(datetime.date.today(), timetable.starttime)
|
starttime = datetime.datetime.combine(datetime.date.today(), timetable.starttime)
|
||||||
endtime = starttime + datetime.timedelta(minutes=timetable.duration)
|
endtime = starttime + datetime.timedelta(minutes=timetable.duration)
|
||||||
print(f" comparing time: Start:{starttime} Current:{current_time} End:{endtime}")
|
logger.info(f" comparing time: Start:{starttime} Current:{current_time} End:{endtime}")
|
||||||
if starttime < current_time < endtime:
|
if starttime < current_time < endtime:
|
||||||
|
logger.info("Access Valid!")
|
||||||
return True
|
return True
|
||||||
|
logger.info("No more auths found")
|
||||||
return False
|
return False
|
||||||
except exc.NoResultFound:
|
except exc.NoResultFound:
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
raise Exception("No Access with that key found, this might be a db error")
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
@@ -18,6 +20,8 @@ from desfire.enums import DESFireCommunicationMode, DESFireFileType, DESFireKeyS
|
|||||||
from desfire.schemas import FilePermissions, FileSettings, KeySettings
|
from desfire.schemas import FilePermissions, FileSettings, KeySettings
|
||||||
import desfire.exceptions as desExceptions
|
import desfire.exceptions as desExceptions
|
||||||
|
|
||||||
|
from app.services.door import openDoor, closeDoor, isDoorOpen, checkAccess
|
||||||
|
|
||||||
|
|
||||||
#ENV vars
|
#ENV vars
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -32,10 +36,6 @@ MIFARE_ACL_WRITE_BASE_KEY_ID = 0x2
|
|||||||
MIFARE_SYS_ID = "FF0000" # 3 bytes, can essentially be anything
|
MIFARE_SYS_ID = "FF0000" # 3 bytes, can essentially be anything
|
||||||
MIFARE_ENCRYPTED_FILE_ID = 0x1
|
MIFARE_ENCRYPTED_FILE_ID = 0x1
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
def checkForKey():
|
def checkForKey():
|
||||||
if MIFARE_APP_MASTER_KEY == None:
|
if MIFARE_APP_MASTER_KEY == None:
|
||||||
logger.critical("NO MASTER KEY LOADED")
|
logger.critical("NO MASTER KEY LOADED")
|
||||||
@@ -44,34 +44,83 @@ def checkForKey():
|
|||||||
def getCardService(timeout: int = 10):
|
def getCardService(timeout: int = 10):
|
||||||
cardtype = AnyCardType()
|
cardtype = AnyCardType()
|
||||||
cardrequest = CardRequest(timeout=timeout, cardType=cardtype)
|
cardrequest = CardRequest(timeout=timeout, cardType=cardtype)
|
||||||
print("Please present DESfire tag...")
|
cardservice = cardrequest.waitforcard()
|
||||||
try:
|
cardservice.connection.connect()
|
||||||
cardservice = cardrequest.waitforcard()
|
|
||||||
except CardRequestTimeoutException:
|
|
||||||
logger.error("No tag detected within the timeout.")
|
|
||||||
raise Exception
|
|
||||||
return cardservice
|
return cardservice
|
||||||
|
|
||||||
|
def readFileOnCard(desfire: DESFire):
|
||||||
|
if not MIFARE_ACL_READ_BASE_KEY:
|
||||||
|
logger.critical("MIFARE_ACL_READ_BASE_KEY not found! Reading skipped!")
|
||||||
|
return
|
||||||
|
#create keys
|
||||||
|
#desfire = DESFire(PCSCDevice(cardservice.connection.component))
|
||||||
|
aes_keysettings = KeySettings(key_type=DESFireKeyType.DF_KEY_AES)
|
||||||
|
keysettings = desfire.get_key_setting()
|
||||||
|
desKey = DESFireKey(keysettings, "00" * 8)
|
||||||
|
|
||||||
|
# Get real UID
|
||||||
|
desfire.authenticate(0x0, desKey)
|
||||||
|
#To get the uid you have to auth with an empty (default) key
|
||||||
|
uid = desfire.get_real_uid()
|
||||||
|
applications = desfire.get_application_ids()
|
||||||
|
try:
|
||||||
|
assert len(applications) == 1
|
||||||
|
assert applications[0] == get_list(MIFARE_APP_ID)
|
||||||
|
except AssertionError:
|
||||||
|
logger.error("No application found!")
|
||||||
|
time.sleep(4)
|
||||||
|
return
|
||||||
|
#Then use the key derivation with that uid, the appid, the sysid
|
||||||
|
diversification_data = [0x01] + uid + get_list(MIFARE_APP_ID) + get_list(MIFARE_SYS_ID)
|
||||||
|
read_div_key_bytes = diversify_key(get_list(MIFARE_ACL_READ_BASE_KEY), diversification_data, pad_to_32=False)
|
||||||
|
|
||||||
|
#Log in with derived read key
|
||||||
|
logger.debug("Start auth")
|
||||||
|
aes_app_read_key = DESFireKey(aes_keysettings, read_div_key_bytes)
|
||||||
|
desfire.select_application(MIFARE_APP_ID)
|
||||||
|
|
||||||
|
desfire.authenticate(MIFARE_ACL_READ_BASE_KEY_ID, aes_app_read_key)
|
||||||
|
|
||||||
|
logger.debug(f"Read data from {MIFARE_ENCRYPTED_FILE_ID}")
|
||||||
|
file_data = desfire.get_file_settings(MIFARE_ENCRYPTED_FILE_ID)
|
||||||
|
rdata = desfire.read_file_data(MIFARE_ENCRYPTED_FILE_ID, file_data)
|
||||||
|
#convert list of int to str
|
||||||
|
rdata = to_hex_string(rdata).replace(" ", "").lower()
|
||||||
|
logger.debug(f"Data on card: {rdata}")
|
||||||
|
return rdata
|
||||||
|
|
||||||
def DeleteCard():
|
def DeleteCard():
|
||||||
try:
|
try:
|
||||||
checkForKey()
|
checkForKey()
|
||||||
from app.main import scanner as scannerThread
|
from app.main import scanner as scannerThread
|
||||||
scannerThread.stop()
|
scannerThread.stop()
|
||||||
cardservice = getCardService(15)
|
cardservice = getCardService(15)
|
||||||
cardservice.connection.connect()
|
|
||||||
|
|
||||||
# Create Desfire object
|
# Create Desfire object
|
||||||
desfire = DESFire(PCSCDevice(cardservice.connection.component))
|
desfire = DESFire(PCSCDevice(cardservice.connection.component))
|
||||||
|
|
||||||
|
rdata = readFileOnCard(desfire=desfire)
|
||||||
|
|
||||||
# Create Key objects
|
# Create Key objects
|
||||||
AES_NULL_KEY_DATA = "00" * 8
|
aes_keysettings = KeySettings(key_type=DESFireKeyType.DF_KEY_AES)
|
||||||
aes_keysettings = KeySettings(
|
des_keysettings = KeySettings(key_type=DESFireKeyType.DF_KEY_2K3DES)
|
||||||
key_type=DESFireKeyType.DF_KEY_AES,
|
desKey = DESFireKey(des_keysettings, "00" * 8)
|
||||||
)
|
|
||||||
key_settings = desfire.get_key_setting()
|
|
||||||
aes_null_key = DESFireKey(key_settings, AES_NULL_KEY_DATA)
|
|
||||||
aes_master_key = DESFireKey(aes_keysettings, MIFARE_APP_MASTER_KEY)
|
aes_master_key = DESFireKey(aes_keysettings, MIFARE_APP_MASTER_KEY)
|
||||||
desfire.authenticate(0x0, aes_null_key)
|
aes_null_key = DESFireKey(aes_keysettings, "00" * 16)
|
||||||
|
|
||||||
|
desfire.select_application(0x0)
|
||||||
|
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
logger.debug("Auth1")#
|
||||||
|
desfire.authenticate(0x0, aes_master_key)
|
||||||
|
except:
|
||||||
|
logger.debug("Auth2")
|
||||||
|
desfire.authenticate(0x0, aes_null_key)
|
||||||
|
except:
|
||||||
|
logger.debug("Auth3")
|
||||||
|
desfire.authenticate(0x0, desKey)
|
||||||
|
|
||||||
applications = desfire.get_application_ids()
|
applications = desfire.get_application_ids()
|
||||||
logger.debug(f"Applications: {applications}")
|
logger.debug(f"Applications: {applications}")
|
||||||
if len(applications) == 0:
|
if len(applications) == 0:
|
||||||
@@ -79,13 +128,15 @@ def DeleteCard():
|
|||||||
|
|
||||||
desfire.select_application(MIFARE_APP_ID)
|
desfire.select_application(MIFARE_APP_ID)
|
||||||
desfire.authenticate(0x0, aes_master_key)
|
desfire.authenticate(0x0, aes_master_key)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
desfire.delete_application(MIFARE_APP_ID)
|
desfire.delete_application(MIFARE_APP_ID)
|
||||||
|
logger.info("App deleted!")
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
scannerThread.start()
|
scannerThread.start()
|
||||||
|
return rdata
|
||||||
except Exception as e:
|
except(Exception, AssertionError) as e:
|
||||||
logger.error(f"Error in deletion function: {e}", exc_info=True)
|
logger.error(f"Error in deletion function: {e}", exc_info=True)
|
||||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error: {e}")
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Error: {e}")
|
||||||
|
|
||||||
@@ -95,43 +146,29 @@ def WriteNewCard():
|
|||||||
from app.main import scanner as scannerThread
|
from app.main import scanner as scannerThread
|
||||||
scannerThread.stop()
|
scannerThread.stop()
|
||||||
|
|
||||||
cardtype = AnyCardType()
|
cardservice = getCardService(20)
|
||||||
cardrequest = CardRequest(timeout=10, cardType=cardtype)
|
|
||||||
print("Please present DESfire tag...")
|
|
||||||
try:
|
|
||||||
cardservice = cardrequest.waitforcard()
|
|
||||||
except CardRequestTimeoutException:
|
|
||||||
logger.error("No tag detected within the timeout.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
cardservice.connection.connect()
|
|
||||||
|
|
||||||
# Create Desfire object
|
|
||||||
desfire = DESFire(PCSCDevice(cardservice.connection.component))
|
desfire = DESFire(PCSCDevice(cardservice.connection.component))
|
||||||
|
|
||||||
# Create Key objects
|
# Create Key objects
|
||||||
AES_NULL_KEY_DATA = "00" * 16
|
aes_keysettings = KeySettings(key_type=DESFireKeyType.DF_KEY_AES)
|
||||||
aes_keysettings = KeySettings(
|
aes_null_key = DESFireKey(aes_keysettings, "00" * 16)
|
||||||
key_type=DESFireKeyType.DF_KEY_AES,
|
|
||||||
)
|
|
||||||
aes_null_key = DESFireKey(aes_keysettings, AES_NULL_KEY_DATA)
|
|
||||||
aes_master_key = DESFireKey(aes_keysettings, MIFARE_APP_MASTER_KEY)
|
aes_master_key = DESFireKey(aes_keysettings, MIFARE_APP_MASTER_KEY)
|
||||||
keysetting = desfire.get_key_setting()
|
desKey = DESFireKey(desfire.get_key_setting(), "00" * 8)
|
||||||
desKey = DESFireKey(keysetting, "00" * 8)
|
|
||||||
|
|
||||||
# Authenticate with default DES key
|
# Authenticate with default DES key
|
||||||
print("Authenticating with default DES key...")
|
logger.debug("Authenticating with default DES key...")
|
||||||
desfire.authenticate(0x0, desKey)
|
desfire.authenticate(0x0, desKey)
|
||||||
|
|
||||||
#get uid
|
#get uid
|
||||||
uid = desfire.get_real_uid()
|
uid = desfire.get_real_uid()
|
||||||
|
|
||||||
# Set default key
|
# Set default key
|
||||||
print("Setting default key...")
|
logger.debug("Setting default key...")
|
||||||
desfire.change_default_key(aes_null_key, 0x0)
|
desfire.change_default_key(aes_null_key, 0x0)
|
||||||
|
|
||||||
# Create application
|
# Create application
|
||||||
print("Creating application...")
|
logger.debug("Creating application...")
|
||||||
app_settings = KeySettings(
|
app_settings = KeySettings(
|
||||||
settings=[
|
settings=[
|
||||||
DESFireKeySettings.KS_ALLOW_CHANGE_MK,
|
DESFireKeySettings.KS_ALLOW_CHANGE_MK,
|
||||||
@@ -147,34 +184,34 @@ def WriteNewCard():
|
|||||||
applications = desfire.get_application_ids()
|
applications = desfire.get_application_ids()
|
||||||
assert len(applications) == 1
|
assert len(applications) == 1
|
||||||
assert applications[0] == get_list(MIFARE_APP_ID)
|
assert applications[0] == get_list(MIFARE_APP_ID)
|
||||||
print(" - Application created successfully.")
|
logger.debug(" - Application created successfully.")
|
||||||
|
|
||||||
# Select application
|
# Select application
|
||||||
desfire.select_application(MIFARE_APP_ID)
|
desfire.select_application(MIFARE_APP_ID)
|
||||||
#Auth again as 0key
|
|
||||||
aes_null_auth_key = DESFireKey(aes_keysettings, AES_NULL_KEY_DATA)
|
|
||||||
desfire.authenticate(0x0, aes_null_auth_key)
|
|
||||||
# Authenticate with AES key, as this has been set as the default key
|
|
||||||
aes_app_mk = DESFireKey(aes_keysettings, MIFARE_APP_MASTER_KEY)
|
|
||||||
desfire.change_key(0x0, aes_null_key, aes_app_mk, 0x1)
|
|
||||||
|
|
||||||
print("new key auth")
|
#recreate key object
|
||||||
desfire.authenticate(0x0, aes_app_mk)
|
desfire.authenticate(0x0, aes_null_key)
|
||||||
|
desfire.change_key(0x0, aes_null_key, aes_master_key, 0x1)
|
||||||
|
|
||||||
|
logger.debug("new key auth")
|
||||||
|
desfire.authenticate(0x0, aes_master_key)
|
||||||
|
|
||||||
|
aes_null_key = DESFireKey(aes_keysettings, "00" * 16)
|
||||||
|
|
||||||
#generate div data
|
#generate div data
|
||||||
diversification_data = [0x01] + uid + get_list(MIFARE_APP_ID) + get_list(MIFARE_SYS_ID)
|
diversification_data = [0x01] + uid + get_list(MIFARE_APP_ID) + get_list(MIFARE_SYS_ID)
|
||||||
read_div_key_bytes = diversify_key(get_list(MIFARE_ACL_READ_BASE_KEY), diversification_data, pad_to_32=False)
|
read_div_key_bytes = diversify_key(get_list(MIFARE_ACL_READ_BASE_KEY), diversification_data, pad_to_32=False)
|
||||||
write_div_key_bytes = diversify_key(get_list(MIFARE_ACL_WRITE_BASE_KEY), diversification_data, pad_to_32=False)
|
write_div_key_bytes = diversify_key(get_list(MIFARE_ACL_WRITE_BASE_KEY), diversification_data, pad_to_32=False)
|
||||||
|
|
||||||
print("Changing file read key...")
|
logger.debug("Changing file read key...")
|
||||||
aes_file_read_key = DESFireKey(aes_keysettings, read_div_key_bytes)
|
aes_file_read_key = DESFireKey(aes_keysettings, read_div_key_bytes)
|
||||||
desfire.change_key(MIFARE_ACL_READ_BASE_KEY_ID, aes_null_key, aes_file_read_key, 0x1)
|
desfire.change_key(MIFARE_ACL_READ_BASE_KEY_ID, aes_null_key, aes_file_read_key, 0x1)
|
||||||
|
|
||||||
print("Changing file write key...")
|
logger.debug("Changing file write key...")
|
||||||
aes_file_write_key = DESFireKey(aes_keysettings, write_div_key_bytes)
|
aes_file_write_key = DESFireKey(aes_keysettings, write_div_key_bytes)
|
||||||
desfire.change_key(MIFARE_ACL_WRITE_BASE_KEY_ID, aes_null_key, aes_file_write_key, 0x1)
|
desfire.change_key(MIFARE_ACL_WRITE_BASE_KEY_ID, aes_null_key, aes_file_write_key, 0x1)
|
||||||
|
|
||||||
print("Create encrypted file containing UUID...")
|
logger.debug("Create encrypted file containing key...")
|
||||||
file_settings = FileSettings(
|
file_settings = FileSettings(
|
||||||
file_size=16,
|
file_size=16,
|
||||||
encryption=DESFireCommunicationMode.ENCRYPTED,
|
encryption=DESFireCommunicationMode.ENCRYPTED,
|
||||||
@@ -185,25 +222,16 @@ def WriteNewCard():
|
|||||||
file_type=DESFireFileType.MDFT_STANDARD_DATA_FILE,
|
file_type=DESFireFileType.MDFT_STANDARD_DATA_FILE,
|
||||||
)
|
)
|
||||||
desfire.create_standard_file(MIFARE_ENCRYPTED_FILE_ID, file_settings)
|
desfire.create_standard_file(MIFARE_ENCRYPTED_FILE_ID, file_settings)
|
||||||
|
|
||||||
print("Read and verify file settings again...")
|
|
||||||
file_data = desfire.get_file_settings(MIFARE_ENCRYPTED_FILE_ID)
|
file_data = desfire.get_file_settings(MIFARE_ENCRYPTED_FILE_ID)
|
||||||
assert file_data.file_size == 16
|
|
||||||
assert file_data.encryption == DESFireCommunicationMode.ENCRYPTED
|
|
||||||
assert file_data.permissions is not None
|
|
||||||
assert file_data.permissions.read_access == MIFARE_ACL_READ_BASE_KEY_ID
|
|
||||||
assert file_data.permissions.write_access == MIFARE_ACL_WRITE_BASE_KEY_ID
|
|
||||||
assert file_data.file_type == DESFireFileType.MDFT_STANDARD_DATA_FILE
|
|
||||||
print(" - File created successfully.")
|
|
||||||
|
|
||||||
print("Writing UID to encrypted file...")
|
logger.debug("Writing UID to encrypted file...")
|
||||||
key = secrets.token_hex(16)
|
key = secrets.token_hex(16)
|
||||||
desfire.write_file_data(MIFARE_ENCRYPTED_FILE_ID, 0x0, file_data.encryption, get_list(key))
|
desfire.write_file_data(MIFARE_ENCRYPTED_FILE_ID, 0x0, file_data.encryption, get_list(key))
|
||||||
|
|
||||||
print("Reading from encrypted file...")
|
logger.debug("Reading from encrypted file...")
|
||||||
rdata = desfire.read_file_data(MIFARE_ENCRYPTED_FILE_ID, file_data)
|
rdata = desfire.read_file_data(MIFARE_ENCRYPTED_FILE_ID, file_data)
|
||||||
assert rdata == get_list(key)
|
assert rdata == get_list(key)
|
||||||
print(" - Data written successfully.")
|
logger.debug(" - Data written successfully.")
|
||||||
scannerThread.start()
|
scannerThread.start()
|
||||||
return key
|
return key
|
||||||
|
|
||||||
@@ -220,7 +248,7 @@ class BackgroundScanner:
|
|||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
if self.is_running:
|
if self.is_running:
|
||||||
logger.info("Scanner already running")
|
logger.error("Scanner already running")
|
||||||
return
|
return
|
||||||
self.is_running = True
|
self.is_running = True
|
||||||
self.thread = threading.Thread(target=self._scan_loop, daemon=True)
|
self.thread = threading.Thread(target=self._scan_loop, daemon=True)
|
||||||
@@ -238,12 +266,11 @@ class BackgroundScanner:
|
|||||||
try:
|
try:
|
||||||
card_content = self._read_card()
|
card_content = self._read_card()
|
||||||
if card_content:
|
if card_content:
|
||||||
logger.info(to_hex_string(card_content))
|
self._check_db(card_content)
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
logger.debug("READY after success")
|
logger.debug("READY after success")
|
||||||
#self._check_db(card_content)
|
|
||||||
else:
|
else:
|
||||||
time.sleep(0.5)
|
time.sleep(0.1)
|
||||||
logger.debug("READY after timout")
|
logger.debug("READY after timout")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -251,53 +278,25 @@ class BackgroundScanner:
|
|||||||
time.sleep(6)
|
time.sleep(6)
|
||||||
|
|
||||||
def _read_card(self):
|
def _read_card(self):
|
||||||
cardtype = AnyCardType()
|
time.sleep(0.5)
|
||||||
cardrequest = CardRequest(timeout=5, cardType=cardtype)
|
|
||||||
try:
|
try:
|
||||||
cardservice = cardrequest.waitforcard()
|
cardservice = getCardService(3)
|
||||||
except CardRequestTimeoutException:
|
except CardRequestTimeoutException:
|
||||||
logger.debug("No tag detected within the timeout.")
|
logger.debug("No tag detected within the timeout.")
|
||||||
return
|
return
|
||||||
|
# Create Desfire object
|
||||||
|
desfire = DESFire(PCSCDevice(cardservice.connection.component))
|
||||||
try:
|
try:
|
||||||
cardservice.connection.connect()
|
rdata = readFileOnCard(desfire=desfire)
|
||||||
|
|
||||||
# Create Desfire object
|
|
||||||
desfire = DESFire(PCSCDevice(cardservice.connection.component))
|
|
||||||
aes_keysettings = KeySettings(
|
|
||||||
key_type=DESFireKeyType.DF_KEY_AES,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get real UID
|
|
||||||
mk = DESFireKey(desfire.get_key_setting(), "00" * 8)
|
|
||||||
desfire.authenticate(0x0, mk)
|
|
||||||
#To get the uid you have to auth with an empty (default) key
|
|
||||||
uid = desfire.get_real_uid()
|
|
||||||
applications = desfire.get_application_ids()
|
|
||||||
try:
|
|
||||||
assert len(applications) == 1
|
|
||||||
assert applications[0] == get_list(MIFARE_APP_ID)
|
|
||||||
except AssertionError:
|
|
||||||
logger.error("No application found!")
|
|
||||||
time.sleep(4)
|
|
||||||
return None
|
|
||||||
#Then use the key derivation with that uid, the appid, the sysid
|
|
||||||
diversification_data = [0x01] + uid + get_list(MIFARE_APP_ID) + get_list(MIFARE_SYS_ID)
|
|
||||||
read_div_key_bytes = diversify_key(get_list(MIFARE_ACL_READ_BASE_KEY), diversification_data, pad_to_32=False)
|
|
||||||
|
|
||||||
#Log in with derived read key
|
|
||||||
logger.info("Start auth")
|
|
||||||
aes_app_read_key = DESFireKey(aes_keysettings, read_div_key_bytes)
|
|
||||||
desfire.select_application(MIFARE_APP_ID)
|
|
||||||
|
|
||||||
desfire.authenticate(MIFARE_ACL_READ_BASE_KEY_ID, aes_app_read_key)
|
|
||||||
|
|
||||||
logger.info("Read data")
|
|
||||||
file_data = desfire.get_file_settings(MIFARE_ENCRYPTED_FILE_ID)
|
|
||||||
logger.info(f"File settings: {file_data}")
|
|
||||||
rdata = desfire.read_file_data(MIFARE_ENCRYPTED_FILE_ID, file_data)
|
|
||||||
return rdata
|
return rdata
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"something went wrong: {e}")
|
logger.error(f"something went wrong: {e}")
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
|
|
||||||
|
def _check_db(self, key):
|
||||||
|
check = checkAccess(key, self.db)
|
||||||
|
if check == True:
|
||||||
|
openDoor()
|
||||||
|
logger.info("Access granted!")
|
||||||
|
else:
|
||||||
|
logger.error("Access denied!")
|
||||||
|
|||||||
128
flake.lock
generated
128
flake.lock
generated
@@ -1,12 +1,114 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
|
"argononed": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1729566243,
|
||||||
|
"narHash": "sha256-DPNI0Dpk5aym3Baf5UbEe5GENDrSmmXVdriRSWE+rgk=",
|
||||||
|
"owner": "nvmd",
|
||||||
|
"repo": "argononed",
|
||||||
|
"rev": "16dbee54d49b66d5654d228d1061246b440ef7cf",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nvmd",
|
||||||
|
"repo": "argononed",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-compat": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1767039857,
|
||||||
|
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixos-hardware": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1782562157,
|
||||||
|
"narHash": "sha256-a7+T6QSeowynwZ1ZJJbP8T8ntAytvrui8kFGJmIZt2c=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixos-hardware",
|
||||||
|
"rev": "a9cf7546a938c737b079e738de73934a13de9784",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "master",
|
||||||
|
"repo": "nixos-hardware",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixos-images": {
|
||||||
|
"inputs": {
|
||||||
|
"nixos-stable": [
|
||||||
|
"nixos-raspberrypi",
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"nixos-unstable": [
|
||||||
|
"nixos-raspberrypi",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1747747741,
|
||||||
|
"narHash": "sha256-LUOH27unNWbGTvZFitHonraNx0JF/55h30r9WxqrznM=",
|
||||||
|
"owner": "nvmd",
|
||||||
|
"repo": "nixos-images",
|
||||||
|
"rev": "cbbd6db325775096680b65e2a32fb6187c09bbb4",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nvmd",
|
||||||
|
"ref": "sdimage-installer",
|
||||||
|
"repo": "nixos-images",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixos-raspberrypi": {
|
||||||
|
"inputs": {
|
||||||
|
"argononed": "argononed",
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"nixos-images": "nixos-images",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1779023229,
|
||||||
|
"narHash": "sha256-MInilg7B/06c34SwOuGSBho4l0H1EZcmvxTkSWCs5pE=",
|
||||||
|
"owner": "nvmd",
|
||||||
|
"repo": "nixos-raspberrypi",
|
||||||
|
"rev": "06c6e3513e1ee64b651913193fc6ac38aa4963f5",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nvmd",
|
||||||
|
"ref": "main",
|
||||||
|
"repo": "nixos-raspberrypi",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1778869304,
|
"lastModified": 1782467914,
|
||||||
"narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=",
|
"narHash": "sha256-pGvFkM8N0xEkIIXDe5YYfbEAvHrk4IxBrjB/x8OomhE=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "d233902339c02a9c334e7e593de68855ad26c4cb",
|
"rev": "e73de5be04e0eff4190a1432b946d469c794e7b4",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -29,11 +131,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1776659114,
|
"lastModified": 1782093830,
|
||||||
"narHash": "sha256-qapCOQmR++yZSY43dzrp3wCrkOTLpod+ONtJWBk6iKU=",
|
"narHash": "sha256-6gmEVe69+KlRkZD4PEEV5xAlB9CB0Y9TiuEgQjDrKTQ=",
|
||||||
"owner": "pyproject-nix",
|
"owner": "pyproject-nix",
|
||||||
"repo": "build-system-pkgs",
|
"repo": "build-system-pkgs",
|
||||||
"rev": "ffaa2161dd5d63e0e94591f86b54fc239660fb2e",
|
"rev": "430680a19bc85a3bda55f12e4cc1a1aadcf2e478",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -49,11 +151,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1778901413,
|
"lastModified": 1782089418,
|
||||||
"narHash": "sha256-GSKXTAnFqRAMlZkJrIPcQMYf+lpMr66K3i60mB9STvc=",
|
"narHash": "sha256-LRD1SuQWr49fGq3A+8GLXfsLE2xqIpQA440YDZwms3M=",
|
||||||
"owner": "pyproject-nix",
|
"owner": "pyproject-nix",
|
||||||
"repo": "pyproject.nix",
|
"repo": "pyproject.nix",
|
||||||
"rev": "a228447c3e179d477c1b6246ef3efa8cfe3c469a",
|
"rev": "43f0b40edd0a74c63f66b7b48d969ae6b740d611",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -64,6 +166,8 @@
|
|||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"nixos-hardware": "nixos-hardware",
|
||||||
|
"nixos-raspberrypi": "nixos-raspberrypi",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"pyproject-build-systems": "pyproject-build-systems",
|
"pyproject-build-systems": "pyproject-build-systems",
|
||||||
"pyproject-nix": "pyproject-nix",
|
"pyproject-nix": "pyproject-nix",
|
||||||
@@ -80,11 +184,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1779269674,
|
"lastModified": 1782100052,
|
||||||
"narHash": "sha256-P1LHCRdYpdtHAEzuEsNHrI6d9mVPl5a2fyFDZGHNVbI=",
|
"narHash": "sha256-UfyLY3Hfwb3JqxCcj0953GxTFI5dEL85EKEe6DAFiVs=",
|
||||||
"owner": "pyproject-nix",
|
"owner": "pyproject-nix",
|
||||||
"repo": "uv2nix",
|
"repo": "uv2nix",
|
||||||
"rev": "69aec536f6d1acc415ed2e20299312802aba98c6",
|
"rev": "920fc6dfaf9f10ec56de93b184055e1a9f380d5e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
127
flake.nix
127
flake.nix
@@ -21,6 +21,15 @@
|
|||||||
inputs.uv2nix.follows = "uv2nix";
|
inputs.uv2nix.follows = "uv2nix";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
nixos-hardware = {
|
||||||
|
url = "github:NixOS/nixos-hardware/master";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
nixos-raspberrypi = {
|
||||||
|
url = "github:nvmd/nixos-raspberrypi/main";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs =
|
outputs =
|
||||||
@@ -29,6 +38,7 @@
|
|||||||
pyproject-nix,
|
pyproject-nix,
|
||||||
uv2nix,
|
uv2nix,
|
||||||
pyproject-build-systems,
|
pyproject-build-systems,
|
||||||
|
nixos-hardware,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
@@ -105,5 +115,122 @@
|
|||||||
packages = forAllSystems (system: {
|
packages = forAllSystems (system: {
|
||||||
default = pythonSets.${system}.mkVirtualEnv "gatekeeper" workspace.deps.default;
|
default = pythonSets.${system}.mkVirtualEnv "gatekeeper" workspace.deps.default;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
nixosConfigurations = {
|
||||||
|
gatekeeper-rpi = nixpkgs.lib.nixosSystem {
|
||||||
|
system = "aarch64-linux";
|
||||||
|
specialArgs = {
|
||||||
|
inherit workspace overlay;
|
||||||
|
};
|
||||||
|
modules = [
|
||||||
|
"${nixpkgs}/nixos/modules/profiles/headless.nix"
|
||||||
|
"${nixpkgs}/nixos/modules/profiles/minimal.nix"
|
||||||
|
"${nixpkgs}/nixos/modules/installer/sd-card/sd-image-aarch64.nix"
|
||||||
|
nixos-hardware.nixosModules.raspberry-pi-3
|
||||||
|
|
||||||
|
({ pkgs, ... }: {
|
||||||
|
nix.nixPath = [
|
||||||
|
"nixpkgs=${pkgs.path}"
|
||||||
|
];
|
||||||
|
|
||||||
|
nix.settings = {
|
||||||
|
trusted-users = [ "root" "@wheel" ];
|
||||||
|
experimental-features = [ "nix-command" "flakes" ];
|
||||||
|
auto-optimise-store = true;
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
({ config, pkgs, packages, ...}:
|
||||||
|
let
|
||||||
|
gatekeeper-app = pkgs.python3Packages.buildPythonPackage {
|
||||||
|
pname = "gatekeeper-app";
|
||||||
|
version = "0.0.1";
|
||||||
|
propagatedBuildInputs = [packages.gatekeeper];
|
||||||
|
src = ./app;
|
||||||
|
format = "pyproject";
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
boot.loader.grub.enable = false;
|
||||||
|
boot.loader.generic-extlinux-compatible.enable = true;
|
||||||
|
boot.kernelPackages = pkgs.linuxPackages;
|
||||||
|
boot.tmp.cleanOnBoot = true;
|
||||||
|
hardware.enableRedistributableFirmware = true;
|
||||||
|
networking.hostName = "gatekeeper-rpi";
|
||||||
|
|
||||||
|
#minimal
|
||||||
|
documentation.man.enable = false;
|
||||||
|
xdg.icons.enable = false;
|
||||||
|
xdg.mime.enable = false;
|
||||||
|
xdg.sounds.enable = false;
|
||||||
|
|
||||||
|
users.users.root.openssh.authorizedKeys.keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICKaEcGaSKU0xC5qCwzj2oCLLG4PYjWHZ7/CXHw4urVk atlan@nixos"];
|
||||||
|
users.groups = { gatekeeper = {}; };
|
||||||
|
users.users.admin = {
|
||||||
|
isNormalUser = true;
|
||||||
|
extraGroups = [ "wheel" "networkmanager" "gatekeeper" ];
|
||||||
|
};
|
||||||
|
users.users.gatekeeper = {
|
||||||
|
isNormalUser = true;
|
||||||
|
extraGroups = ["gatekeeper"];
|
||||||
|
};
|
||||||
|
|
||||||
|
services.openssh = {
|
||||||
|
enable = true;
|
||||||
|
ports = [ 22 ];
|
||||||
|
settings = {
|
||||||
|
PasswordAuthentication = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
git
|
||||||
|
htop
|
||||||
|
pcsclite
|
||||||
|
gatekeeper-app
|
||||||
|
];
|
||||||
|
environment.etc."gatekeeper-env".source =
|
||||||
|
pythonSets.aarch64-linux.mkVirtualEnv "gatekeeper" workspace.deps.default;
|
||||||
|
services.pcscd.enable = true;
|
||||||
|
|
||||||
|
systemd.services.gatekeeper = {
|
||||||
|
enable = true;
|
||||||
|
description = "Gatekeeper service";
|
||||||
|
after = [ "network.target" "pcscd.service"];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
LD_LIBRARY_PATH = "${lib.getLib pkgs.pcsclite}/lib";
|
||||||
|
PYTHONPATH = "/etc/gatekeeper-env/lib/python3.x/site-packages";
|
||||||
|
};
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
User = "gatekeeper";
|
||||||
|
Group = "gatekeeper";
|
||||||
|
WorkingDirectory = "/var/lib/gatekeeper";
|
||||||
|
ExecStart = "${pythonSets.aarch64-linux.mkVirtualEnv "gatekeeper" workspace.deps.default}/bin/python -m uvicorn main:app --host 0.0.0.0 --port 8000";
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = "10";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Create working directory and log directory
|
||||||
|
preStart = ''
|
||||||
|
mkdir -p /var/lib/gatekeeper
|
||||||
|
mkdir -p /var/log/gatekeeper
|
||||||
|
chown -R gatekeeper:gatekeeper /var/lib/gatekeeper
|
||||||
|
chown -R gatekeeper:gatekeeper /var/log/gatekeeper
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
networking.firewall.allowedTCPPorts = [ 8000 ];
|
||||||
|
|
||||||
|
time.timeZone = "Europe/Berlin";
|
||||||
|
system.stateVersion = "25.11";
|
||||||
|
})
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ dependencies = [
|
|||||||
"sqlmodel>=0.0.38",
|
"sqlmodel>=0.0.38",
|
||||||
"poetry>=2.3.4",
|
"poetry>=2.3.4",
|
||||||
"python-desfire",
|
"python-desfire",
|
||||||
"paho-mqtt>=2.1.0",
|
|
||||||
"pyjwt[crypto]>=2.12.1",
|
"pyjwt[crypto]>=2.12.1",
|
||||||
"pwdlib[argon2]>=0.3.0",
|
"pwdlib[argon2]>=0.3.0",
|
||||||
"pytest>=9.0.3",
|
"pytest>=9.0.3",
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ def regular_user(db_session):
|
|||||||
def auth_headers(client, admin_user):
|
def auth_headers(client, admin_user):
|
||||||
"""Get authentication headers for admin user."""
|
"""Get authentication headers for admin user."""
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/token",
|
"/api/v1/token",
|
||||||
data={"username": admin_user.name, "password": "admin123"}
|
data={"username": admin_user.name, "password": "admin123"}
|
||||||
)
|
)
|
||||||
token = response.json()["access_token"]
|
token = response.json()["access_token"]
|
||||||
@@ -79,7 +79,7 @@ def auth_headers(client, admin_user):
|
|||||||
def user_auth_headers(client, regular_user):
|
def user_auth_headers(client, regular_user):
|
||||||
"""Get authentication headers for regular user."""
|
"""Get authentication headers for regular user."""
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/token",
|
"/api/v1/token",
|
||||||
data={"username": regular_user.name, "password": "user123"}
|
data={"username": regular_user.name, "password": "user123"}
|
||||||
)
|
)
|
||||||
token = response.json()["access_token"]
|
token = response.json()["access_token"]
|
||||||
|
|||||||
@@ -4,13 +4,6 @@ def test_app_startup(client):
|
|||||||
# Application should respond (even if it's a 404)
|
# Application should respond (even if it's a 404)
|
||||||
assert response.status_code in [404, 200]
|
assert response.status_code in [404, 200]
|
||||||
|
|
||||||
|
|
||||||
def test_health_check(client):
|
|
||||||
"""Test basic health check endpoint if it exists."""
|
|
||||||
# Note: This would require adding a health check endpoint
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def test_router_includes():
|
def test_router_includes():
|
||||||
"""Test that all routers are included in the app."""
|
"""Test that all routers are included in the app."""
|
||||||
from app.main import app
|
from app.main import app
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ def test_create_access_auth(client, auth_headers):
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/aa/", json=aa_data, headers=auth_headers)
|
response = client.post("/api/v1/aa/", json=aa_data, headers=auth_headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -25,7 +25,7 @@ def test_create_access_auth(client, auth_headers):
|
|||||||
|
|
||||||
def test_get_all_access_auths(client, auth_headers, test_aa):
|
def test_get_all_access_auths(client, auth_headers, test_aa):
|
||||||
"""Test retrieving all access authorizations."""
|
"""Test retrieving all access authorizations."""
|
||||||
response = client.get("/aa/", headers=auth_headers)
|
response = client.get("/api/v1/aa/", headers=auth_headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
aa_list = response.json()
|
aa_list = response.json()
|
||||||
@@ -37,7 +37,7 @@ def test_get_all_access_auths(client, auth_headers, test_aa):
|
|||||||
|
|
||||||
def test_get_access_auth_by_id(client, auth_headers, test_aa):
|
def test_get_access_auth_by_id(client, auth_headers, test_aa):
|
||||||
"""Test retrieving a specific access authorization by ID."""
|
"""Test retrieving a specific access authorization by ID."""
|
||||||
response = client.get(f"/aa/{test_aa.id}", headers=auth_headers)
|
response = client.get(f"/api/v1/aa/{test_aa.id}", headers=auth_headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -47,14 +47,14 @@ def test_get_access_auth_by_id(client, auth_headers, test_aa):
|
|||||||
|
|
||||||
def test_get_nonexistent_access_auth(client, auth_headers):
|
def test_get_nonexistent_access_auth(client, auth_headers):
|
||||||
"""Test retrieving a non-existent access authorization."""
|
"""Test retrieving a non-existent access authorization."""
|
||||||
response = client.get("/aa/99999", headers=auth_headers)
|
response = client.get("/api/v1/aa/99999", headers=auth_headers)
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
def test_assign_access_auth_to_group(client, auth_headers, test_group, test_aa):
|
def test_assign_access_auth_to_group(client, auth_headers, test_group, test_aa):
|
||||||
"""Test assigning an access authorization to a group."""
|
"""Test assigning an access authorization to a group."""
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/aa/assign/{test_group.id}/{test_aa.id}",
|
f"/api/v1/aa/assign/{test_group.id}/{test_aa.id}",
|
||||||
headers=auth_headers
|
headers=auth_headers
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -68,25 +68,26 @@ def test_assign_access_auth_to_group(client, auth_headers, test_group, test_aa):
|
|||||||
def test_assign_already_assigned_access_auth(client, auth_headers, test_group, test_aa):
|
def test_assign_already_assigned_access_auth(client, auth_headers, test_group, test_aa):
|
||||||
"""Test assigning an already assigned access authorization."""
|
"""Test assigning an already assigned access authorization."""
|
||||||
# First assignment
|
# First assignment
|
||||||
client.put(f"/aa/assign/{test_group.id}/{test_aa.id}", headers=auth_headers)
|
client.put(f"/api/v1/aa/assign/{test_group.id}/{test_aa.id}", headers=auth_headers)
|
||||||
|
|
||||||
# Second assignment should indicate it's already assigned
|
# Second assignment should indicate it's already assigned
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/aa/assign/{test_group.id}/{test_aa.id}",
|
f"/api/v1/aa/assign/{test_group.id}/{test_aa.id}",
|
||||||
headers=auth_headers
|
headers=auth_headers
|
||||||
)
|
)
|
||||||
# According to the code, this returns 200 with "already assigned" message
|
# According to the code, this returns 409 with "already assigned" message
|
||||||
assert response.status_code == 200
|
assert response.status_code == 409
|
||||||
|
assert "already assigned" in response.json()["detail"].lower()
|
||||||
|
|
||||||
|
|
||||||
def test_unassign_access_auth_from_group(client, auth_headers, test_group, test_aa):
|
def test_unassign_access_auth_from_group(client, auth_headers, test_group, test_aa):
|
||||||
"""Test unassigning an access authorization from a group."""
|
"""Test unassigning an access authorization from a group."""
|
||||||
# First assign
|
# First assign
|
||||||
client.put(f"/aa/assign/{test_group.id}/{test_aa.id}", headers=auth_headers)
|
client.put(f"/api/v1/aa/assign/{test_group.id}/{test_aa.id}", headers=auth_headers)
|
||||||
|
|
||||||
# Then unassign
|
# Then unassign
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/aa/unassign/{test_group.id}/{test_aa.id}",
|
f"/api/v1/aa/unassign/{test_group.id}/{test_aa.id}",
|
||||||
headers=auth_headers
|
headers=auth_headers
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -95,22 +96,21 @@ def test_unassign_access_auth_from_group(client, auth_headers, test_group, test_
|
|||||||
def test_unassign_nonexistent_assignment(client, auth_headers, test_group, test_aa):
|
def test_unassign_nonexistent_assignment(client, auth_headers, test_group, test_aa):
|
||||||
"""Test unassigning a non-existent assignment."""
|
"""Test unassigning a non-existent assignment."""
|
||||||
response = client.put(
|
response = client.put(
|
||||||
f"/aa/unassign/{test_group.id}/{test_aa.id}",
|
f"/api/v1/aa/unassign/{test_group.id}/{test_aa.id}",
|
||||||
headers=auth_headers
|
headers=auth_headers
|
||||||
)
|
)
|
||||||
# According to the code, this returns 200 with "not assigned" message
|
assert response.status_code == 404
|
||||||
assert response.status_code == 200
|
|
||||||
|
|
||||||
|
|
||||||
def test_assign_to_nonexistent_group(client, auth_headers, test_aa):
|
def test_assign_to_nonexistent_group(client, auth_headers, test_aa):
|
||||||
"""Test assigning an AA to a non-existent group."""
|
"""Test assigning an AA to a non-existent group."""
|
||||||
response = client.put(f"/aa/assign/99999/{test_aa.id}", headers=auth_headers)
|
response = client.put(f"/api/v1/aa/assign/99999/{test_aa.id}", headers=auth_headers)
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
def test_assign_nonexistent_aa(client, auth_headers, test_group):
|
def test_assign_nonexistent_aa(client, auth_headers, test_group):
|
||||||
"""Test assigning a non-existent AA to a group."""
|
"""Test assigning a non-existent AA to a group."""
|
||||||
response = client.put(f"/aa/assign/{test_group.id}/99999", headers=auth_headers)
|
response = client.put(f"/api/v1/aa/assign/{test_group.id}/99999", headers=auth_headers)
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ def test_update_access_auth(client, auth_headers, test_aa):
|
|||||||
}
|
}
|
||||||
|
|
||||||
response = client.patch(
|
response = client.patch(
|
||||||
f"/aa/{test_aa.id}",
|
f"/api/v1/aa/{test_aa.id}",
|
||||||
json=update_data,
|
json=update_data,
|
||||||
headers=auth_headers
|
headers=auth_headers
|
||||||
)
|
)
|
||||||
@@ -142,34 +142,39 @@ def test_update_access_auth_with_timetables(client, auth_headers, test_aa):
|
|||||||
}
|
}
|
||||||
|
|
||||||
response = client.patch(
|
response = client.patch(
|
||||||
f"/aa/{test_aa.id}",
|
f"/api/v1/aa/{test_aa.id}",
|
||||||
json=update_data,
|
json=update_data,
|
||||||
headers=auth_headers
|
headers=auth_headers
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
jresponse = response.json()
|
||||||
|
assert len(jresponse["timetables"]) == 1
|
||||||
|
assert jresponse["timetables"][0]["weekday"] == 5
|
||||||
|
assert jresponse["timetables"][0]["starttime"] == "10:00:00"
|
||||||
|
assert jresponse["timetables"][0]["duration"] == 120
|
||||||
|
|
||||||
|
|
||||||
def test_update_nonexistent_access_auth(client, auth_headers):
|
def test_update_nonexistent_access_auth(client, auth_headers):
|
||||||
"""Test updating a non-existent access authorization."""
|
"""Test updating a non-existent access authorization."""
|
||||||
update_data = {"name": "Updated"}
|
update_data = {"name": "Updated"}
|
||||||
response = client.patch("/aa/99999", json=update_data, headers=auth_headers)
|
response = client.patch("/api/v1/aa/99999", json=update_data, headers=auth_headers)
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
def test_delete_access_auth(client, auth_headers, test_aa):
|
def test_delete_access_auth(client, auth_headers, test_aa):
|
||||||
"""Test deleting an access authorization."""
|
"""Test deleting an access authorization."""
|
||||||
response = client.delete(f"/aa/{test_aa.id}", headers=auth_headers)
|
response = client.delete(f"/api/v1/aa/{test_aa.id}", headers=auth_headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "deleted successfully" in response.json()["message"].lower()
|
assert "deleted successfully" in response.json()["message"].lower()
|
||||||
|
|
||||||
# Verify AA is deleted
|
# Verify AA is deleted
|
||||||
response = client.get(f"/aa/{test_aa.id}", headers=auth_headers)
|
response = client.get(f"/api/v1/aa/{test_aa.id}", headers=auth_headers)
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
def test_delete_nonexistent_access_auth(client, auth_headers):
|
def test_delete_nonexistent_access_auth(client, auth_headers):
|
||||||
"""Test deleting a non-existent access authorization."""
|
"""Test deleting a non-existent access authorization."""
|
||||||
response = client.delete("/aa/99999", headers=auth_headers)
|
response = client.delete("/api/v1/aa/99999", headers=auth_headers)
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
@@ -177,16 +182,16 @@ def test_aa_operations_by_non_admin(client, test_aa, user_auth_headers):
|
|||||||
"""Test that non-admin users cannot perform AA operations."""
|
"""Test that non-admin users cannot perform AA operations."""
|
||||||
# Try to create an AA
|
# Try to create an AA
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/aa/",
|
"/api/v1/aa/",
|
||||||
json={"name": "test", "is_active": True, "timetables": []},
|
json={"name": "test", "is_active": True, "timetables": []},
|
||||||
headers=user_auth_headers
|
headers=user_auth_headers
|
||||||
)
|
)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
# Try to get all AAs
|
# Try to get all AAs
|
||||||
response = client.get("/aa/", headers=user_auth_headers)
|
response = client.get("/api/v1/aa/", headers=user_auth_headers)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
# Try to assign AA
|
# Try to assign AA
|
||||||
response = client.put(f"/aa/assign/1/{test_aa.id}", headers=user_auth_headers)
|
response = client.put(f"/api/v1/aa/assign/1/{test_aa.id}", headers=user_auth_headers)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ def test_auth_is_admin(db_session, admin_user, regular_user):
|
|||||||
|
|
||||||
def test_create_first_user(db_session):
|
def test_create_first_user(db_session):
|
||||||
"""Test automatic creation of first admin user."""
|
"""Test automatic creation of first admin user."""
|
||||||
#Currently broken because this uses the prod db because of how i wrote the create_first_user function
|
|
||||||
# Clear any existing users
|
# Clear any existing users
|
||||||
from sqlmodel import select
|
from sqlmodel import select
|
||||||
db_session.exec(select(UserDB)).all()
|
db_session.exec(select(UserDB)).all()
|
||||||
@@ -140,7 +139,7 @@ def test_create_first_user(db_session):
|
|||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
||||||
# Create first user
|
# Create first user
|
||||||
result = create_first_user()
|
result = create_first_user(db=db_session)
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert result.name == "admin"
|
assert result.name == "admin"
|
||||||
assert result.is_admin is True
|
assert result.is_admin is True
|
||||||
@@ -151,7 +150,7 @@ def test_create_first_user(db_session):
|
|||||||
assert user.is_admin is True
|
assert user.is_admin is True
|
||||||
|
|
||||||
# Test that it doesn't create another admin if one exists
|
# Test that it doesn't create another admin if one exists
|
||||||
second_result = create_first_user()
|
second_result = create_first_user(db=db_session)
|
||||||
assert second_result is None # Should print "Admin user already exists"
|
assert second_result is None # Should print "Admin user already exists"
|
||||||
|
|
||||||
|
|
||||||
@@ -159,7 +158,7 @@ def test_token_endpoint(client, admin_user):
|
|||||||
"""Test the token endpoint for login."""
|
"""Test the token endpoint for login."""
|
||||||
# Test successful login
|
# Test successful login
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/token",
|
"/api/v1/token",
|
||||||
data={"username": admin_user.name, "password": "admin123"}
|
data={"username": admin_user.name, "password": "admin123"}
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -169,14 +168,14 @@ def test_token_endpoint(client, admin_user):
|
|||||||
|
|
||||||
# Test failed login with wrong password
|
# Test failed login with wrong password
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/token",
|
"/api/v1/token",
|
||||||
data={"username": admin_user.name, "password": "wrongpassword"}
|
data={"username": admin_user.name, "password": "wrongpassword"}
|
||||||
)
|
)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
|
|
||||||
# Test failed login with non-existent user
|
# Test failed login with non-existent user
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/token",
|
"/api/v1/token",
|
||||||
data={"username": "nonexistent", "password": "password"}
|
data={"username": "nonexistent", "password": "password"}
|
||||||
)
|
)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
@@ -185,12 +184,12 @@ def test_token_endpoint(client, admin_user):
|
|||||||
def test_test_login_endpoint(client, admin_user, auth_headers):
|
def test_test_login_endpoint(client, admin_user, auth_headers):
|
||||||
"""Test the test login endpoint."""
|
"""Test the test login endpoint."""
|
||||||
# Test with valid token
|
# Test with valid token
|
||||||
response = client.get("/test/login", headers=auth_headers)
|
response = client.get("/api/v1/test/login", headers=auth_headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["name"] == admin_user.name
|
assert data["name"] == admin_user.name
|
||||||
assert data["is_admin"] is True
|
assert data["is_admin"] is True
|
||||||
|
|
||||||
# Test without token
|
# Test without token
|
||||||
response = client.get("/test/login")
|
response = client.get("/api/v1/test/login")
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
|
|||||||
@@ -1,44 +1,9 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from fastapi import status
|
from fastapi import status
|
||||||
|
|
||||||
|
|
||||||
def test_add_card(client, auth_headers, test_group):
|
|
||||||
"""Test adding a card to a group."""
|
|
||||||
response = client.post(f"/cards/{test_group.id}", headers=auth_headers)
|
|
||||||
assert response.status_code == 200
|
|
||||||
|
|
||||||
data = response.json()
|
|
||||||
assert "id" in data
|
|
||||||
assert "uuid" in data
|
|
||||||
assert data["group_id"] == test_group.id
|
|
||||||
assert len(data["uuid"]) > 0 # UUID should be generated
|
|
||||||
|
|
||||||
|
|
||||||
def test_add_card_to_nonexistent_group(client, auth_headers):
|
|
||||||
"""Test adding a card to a non-existent group."""
|
|
||||||
response = client.post("/cards/99999", headers=auth_headers)
|
|
||||||
# This might succeed and create a card with a non-existent group_id
|
|
||||||
# or fail depending on foreign key constraints
|
|
||||||
# For now, let's assume it might fail
|
|
||||||
# assert response.status_code == 404
|
|
||||||
|
|
||||||
|
|
||||||
def test_delete_card(client, auth_headers, test_card):
|
|
||||||
"""Test deleting a card."""
|
|
||||||
response = client.delete(f"/cards/{test_card.id}", headers=auth_headers)
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert "deleted successfully" in response.json()["message"].lower()
|
|
||||||
|
|
||||||
|
|
||||||
def test_delete_nonexistent_card(client, auth_headers):
|
|
||||||
"""Test deleting a non-existent card."""
|
|
||||||
response = client.delete("/cards/99999", headers=auth_headers)
|
|
||||||
assert response.status_code == 404
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_cards_for_group(client, auth_headers, test_group, test_card):
|
def test_get_cards_for_group(client, auth_headers, test_group, test_card):
|
||||||
"""Test getting all cards for a group."""
|
"""Test getting all cards for a group."""
|
||||||
response = client.get(f"/cards/{test_group.id}", headers=auth_headers)
|
response = client.get(f"/api/v1/cards/{test_group.id}", headers=auth_headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
cards = response.json()
|
cards = response.json()
|
||||||
@@ -48,7 +13,7 @@ def test_get_cards_for_group(client, auth_headers, test_group, test_card):
|
|||||||
|
|
||||||
def test_get_cards_for_nonexistent_group(client, auth_headers):
|
def test_get_cards_for_nonexistent_group(client, auth_headers):
|
||||||
"""Test getting cards for a non-existent group."""
|
"""Test getting cards for a non-existent group."""
|
||||||
response = client.get("/cards/99999", headers=auth_headers)
|
response = client.get("/api/v1/cards/99999", headers=auth_headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
cards = response.json()
|
cards = response.json()
|
||||||
@@ -58,9 +23,9 @@ def test_get_cards_for_nonexistent_group(client, auth_headers):
|
|||||||
def test_card_operations_by_non_admin(client, test_group, user_auth_headers):
|
def test_card_operations_by_non_admin(client, test_group, user_auth_headers):
|
||||||
"""Test that non-admin users cannot perform card operations."""
|
"""Test that non-admin users cannot perform card operations."""
|
||||||
# Try to add a card
|
# Try to add a card
|
||||||
response = client.post(f"/cards/{test_group.id}", headers=user_auth_headers)
|
response = client.post(f"/api/v1/cards/{test_group.id}", headers=user_auth_headers)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
# Try to get cards
|
# Try to get cards
|
||||||
response = client.get(f"/cards/{test_group.id}", headers=user_auth_headers)
|
response = client.get(f"/api/v1/cards/{test_group.id}", headers=user_auth_headers)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|||||||
61
test/test_services/test_door.py
Normal file
61
test/test_services/test_door.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import pytest
|
||||||
|
import datetime
|
||||||
|
from app.services.door import checkAccess
|
||||||
|
from app.model.models import Card, GroupDB, AccessAuthorizationDB, Timetable
|
||||||
|
|
||||||
|
def test_check_access_with_valid_timetable(db_session):
|
||||||
|
# Setup: create card with valid access
|
||||||
|
group = GroupDB(name="Test Group")
|
||||||
|
db_session.add(group)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
card = Card(uuid="test-uuid-123", group_id=group.id)
|
||||||
|
db_session.add(card)
|
||||||
|
|
||||||
|
timetable = Timetable(
|
||||||
|
weekday=datetime.datetime.weekday(datetime.date.today()),
|
||||||
|
starttime=datetime.datetime.now().time(),
|
||||||
|
duration=120 # 2 hours
|
||||||
|
)
|
||||||
|
db_session.add(timetable)
|
||||||
|
|
||||||
|
aa = AccessAuthorizationDB(name="Test AA", is_active=True)
|
||||||
|
db_session.add(aa)
|
||||||
|
aa.timetables = [timetable]
|
||||||
|
group.accessauths = [aa]
|
||||||
|
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
# Test: access should be granted within time window
|
||||||
|
result = checkAccess("test-uuid-123", db_session)
|
||||||
|
assert result == True
|
||||||
|
|
||||||
|
def test_check_access_outside_hours(db_session):
|
||||||
|
# Test when current time is outside valid hours
|
||||||
|
group = GroupDB(name="Test Group")
|
||||||
|
db_session.add(group)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
card = Card(uuid="test-uuid-123", group_id=group.id)
|
||||||
|
db_session.add(card)
|
||||||
|
|
||||||
|
timetable = Timetable(
|
||||||
|
weekday=datetime.datetime.weekday(datetime.date.today()),
|
||||||
|
starttime=datetime.time(1, 0),
|
||||||
|
duration=1 # 2 hours
|
||||||
|
)
|
||||||
|
db_session.add(timetable)
|
||||||
|
|
||||||
|
aa = AccessAuthorizationDB(name="Test AA", is_active=True)
|
||||||
|
db_session.add(aa)
|
||||||
|
aa.timetables = [timetable]
|
||||||
|
group.accessauths = [aa]
|
||||||
|
|
||||||
|
db_session.commit()
|
||||||
|
result = checkAccess("test-uuid-123", db_session)
|
||||||
|
assert result == False
|
||||||
|
|
||||||
|
def test_check_access_invalid_card(db_session):
|
||||||
|
# Should raise exception for non-existent card
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
checkAccess("non-existent-uuid", db_session)
|
||||||
@@ -6,7 +6,7 @@ def test_create_group(client, auth_headers):
|
|||||||
"""Test creating a new group."""
|
"""Test creating a new group."""
|
||||||
group_data = {"name": "New Test Group"}
|
group_data = {"name": "New Test Group"}
|
||||||
|
|
||||||
response = client.post("/groups/", json=group_data, headers=auth_headers)
|
response = client.post("/api/v1/groups/", json=group_data, headers=auth_headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -18,14 +18,14 @@ def test_create_duplicate_group(client, auth_headers, test_group):
|
|||||||
"""Test creating a group with a duplicate name."""
|
"""Test creating a group with a duplicate name."""
|
||||||
group_data = {"name": test_group.name}
|
group_data = {"name": test_group.name}
|
||||||
|
|
||||||
response = client.post("/groups/", json=group_data, headers=auth_headers)
|
response = client.post("/api/v1/groups/", json=group_data, headers=auth_headers)
|
||||||
# This should fail due to unique constraint
|
# This should fail due to unique constraint
|
||||||
assert response.status_code == 409 # Validation error
|
assert response.status_code == 409 # Validation error
|
||||||
|
|
||||||
|
|
||||||
def test_get_groups(client, auth_headers, test_group):
|
def test_get_groups(client, auth_headers, test_group):
|
||||||
"""Test retrieving all groups."""
|
"""Test retrieving all groups."""
|
||||||
response = client.get("/groups/", headers=auth_headers)
|
response = client.get("/api/v1/groups/", headers=auth_headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
groups = response.json()
|
groups = response.json()
|
||||||
@@ -37,19 +37,19 @@ def test_get_groups(client, auth_headers, test_group):
|
|||||||
|
|
||||||
def test_delete_group(client, auth_headers, test_group):
|
def test_delete_group(client, auth_headers, test_group):
|
||||||
"""Test deleting a group."""
|
"""Test deleting a group."""
|
||||||
response = client.delete(f"/groups/{test_group.id}", headers=auth_headers)
|
response = client.delete(f"/api/v1/groups/{test_group.id}", headers=auth_headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "deleted successfully" in response.json()["message"].lower()
|
assert "deleted successfully" in response.json()["message"].lower()
|
||||||
|
|
||||||
# Verify group is deleted
|
# Verify group is deleted
|
||||||
response = client.get("/groups/", headers=auth_headers)
|
response = client.get("/api/v1/groups/", headers=auth_headers)
|
||||||
groups = response.json()
|
groups = response.json()
|
||||||
assert not any(group["id"] == test_group.id for group in groups)
|
assert not any(group["id"] == test_group.id for group in groups)
|
||||||
|
|
||||||
|
|
||||||
def test_delete_nonexistent_group(client, auth_headers):
|
def test_delete_nonexistent_group(client, auth_headers):
|
||||||
"""Test deleting a non-existent group."""
|
"""Test deleting a non-existent group."""
|
||||||
response = client.delete("/groups/99999", headers=auth_headers)
|
response = client.delete("/api/v1/groups/99999", headers=auth_headers)
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
@@ -57,12 +57,12 @@ def test_group_operations_by_non_admin(client, user_auth_headers):
|
|||||||
"""Test that non-admin users cannot perform group operations."""
|
"""Test that non-admin users cannot perform group operations."""
|
||||||
# Try to create a group
|
# Try to create a group
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/groups/",
|
"/api/v1/groups/",
|
||||||
json={"name": "test"},
|
json={"name": "test"},
|
||||||
headers=user_auth_headers
|
headers=user_auth_headers
|
||||||
)
|
)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
# Try to get groups
|
# Try to get groups
|
||||||
response = client.get("/groups/", headers=user_auth_headers)
|
response = client.get("/api/v1/groups/", headers=user_auth_headers)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ def test_create_user(client, auth_headers):
|
|||||||
"password": "newpassword123"
|
"password": "newpassword123"
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/users/", json=user_data, headers=auth_headers)
|
response = client.post("/api/v1/users/", json=user_data, headers=auth_headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@@ -30,13 +30,13 @@ def test_create_user_unauthorized(client):
|
|||||||
"password": "password123"
|
"password": "password123"
|
||||||
}
|
}
|
||||||
|
|
||||||
response = client.post("/users/", json=user_data)
|
response = client.post("/api/v1/users/", json=user_data)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
def test_get_users(client, auth_headers, admin_user, regular_user):
|
def test_get_users(client, auth_headers, admin_user, regular_user):
|
||||||
"""Test retrieving all users."""
|
"""Test retrieving all users."""
|
||||||
response = client.get("/users/", headers=auth_headers)
|
response = client.get("/api/v1/users/", headers=auth_headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
users = response.json()
|
users = response.json()
|
||||||
@@ -49,17 +49,26 @@ def test_get_users(client, auth_headers, admin_user, regular_user):
|
|||||||
|
|
||||||
def test_get_user_by_id(client, auth_headers, regular_user):
|
def test_get_user_by_id(client, auth_headers, regular_user):
|
||||||
"""Test retrieving a specific user by ID."""
|
"""Test retrieving a specific user by ID."""
|
||||||
response = client.get(f"/users/{regular_user.id}", headers=auth_headers)
|
response = client.get(f"/api/v1/users/{regular_user.id}", headers=auth_headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["id"] == regular_user.id
|
assert data["id"] == regular_user.id
|
||||||
assert data["name"] == regular_user.name
|
assert data["name"] == regular_user.name
|
||||||
|
|
||||||
|
def test_get_current_user(client, auth_headers, admin_user):
|
||||||
|
"""Test getting the special url current"""
|
||||||
|
response = client.get("/api/v1/users/current", headers=auth_headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
assert data["id"] == admin_user.id
|
||||||
|
assert data["name"] == admin_user.name
|
||||||
|
assert data["is_admin"] == admin_user.is_admin
|
||||||
|
|
||||||
def test_get_nonexistent_user(client, auth_headers):
|
def test_get_nonexistent_user(client, auth_headers):
|
||||||
"""Test retrieving a non-existent user."""
|
"""Test retrieving a non-existent user."""
|
||||||
response = client.get("/users/99999", headers=auth_headers)
|
response = client.get("/api/v1/users/99999", headers=auth_headers)
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
assert "not found" in response.json()["detail"].lower()
|
assert "not found" in response.json()["detail"].lower()
|
||||||
|
|
||||||
@@ -72,7 +81,7 @@ def test_update_user(client, auth_headers, regular_user):
|
|||||||
}
|
}
|
||||||
|
|
||||||
response = client.patch(
|
response = client.patch(
|
||||||
f"/users/{regular_user.id}",
|
f"/api/v1/users/{regular_user.id}",
|
||||||
json=update_data,
|
json=update_data,
|
||||||
headers=auth_headers
|
headers=auth_headers
|
||||||
)
|
)
|
||||||
@@ -92,7 +101,7 @@ def test_update_user_password(client, auth_headers, regular_user):
|
|||||||
}
|
}
|
||||||
|
|
||||||
response = client.patch(
|
response = client.patch(
|
||||||
f"/users/{regular_user.id}",
|
f"/api/v1/users/{regular_user.id}",
|
||||||
json=update_data,
|
json=update_data,
|
||||||
headers=auth_headers
|
headers=auth_headers
|
||||||
)
|
)
|
||||||
@@ -100,7 +109,7 @@ def test_update_user_password(client, auth_headers, regular_user):
|
|||||||
|
|
||||||
# Verify password can be used for login
|
# Verify password can be used for login
|
||||||
login_response = client.post(
|
login_response = client.post(
|
||||||
"/token",
|
"/api/v1/token",
|
||||||
data={"username": regular_user.name, "password": "new_password_456"}
|
data={"username": regular_user.name, "password": "new_password_456"}
|
||||||
)
|
)
|
||||||
assert login_response.status_code == 200
|
assert login_response.status_code == 200
|
||||||
@@ -109,24 +118,24 @@ def test_update_user_password(client, auth_headers, regular_user):
|
|||||||
def test_update_nonexistent_user(client, auth_headers):
|
def test_update_nonexistent_user(client, auth_headers):
|
||||||
"""Test updating a non-existent user."""
|
"""Test updating a non-existent user."""
|
||||||
update_data = {"name": "updated"}
|
update_data = {"name": "updated"}
|
||||||
response = client.patch("/users/99999", json=update_data, headers=auth_headers)
|
response = client.patch("/api/v1/users/99999", json=update_data, headers=auth_headers)
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
def test_delete_user(client, auth_headers, regular_user):
|
def test_delete_user(client, auth_headers, regular_user):
|
||||||
"""Test deleting a user."""
|
"""Test deleting a user."""
|
||||||
response = client.delete(f"/users/{regular_user.id}", headers=auth_headers)
|
response = client.delete(f"/api/v1/users/{regular_user.id}", headers=auth_headers)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert "deleted successfully" in response.json()["message"].lower()
|
assert "deleted successfully" in response.json()["message"].lower()
|
||||||
|
|
||||||
# Verify user is deleted
|
# Verify user is deleted
|
||||||
response = client.get(f"/users/{regular_user.id}", headers=auth_headers)
|
response = client.get(f"/api/v1/users/{regular_user.id}", headers=auth_headers)
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
def test_delete_nonexistent_user(client, auth_headers):
|
def test_delete_nonexistent_user(client, auth_headers):
|
||||||
"""Test deleting a non-existent user."""
|
"""Test deleting a non-existent user."""
|
||||||
response = client.delete("/users/99999", headers=auth_headers)
|
response = client.delete("/api/v1/users/99999", headers=auth_headers)
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
@@ -134,17 +143,17 @@ def test_user_operations_by_non_admin(client, user_auth_headers):
|
|||||||
"""Test that non-admin users cannot perform admin operations."""
|
"""Test that non-admin users cannot perform admin operations."""
|
||||||
# Try to create a user
|
# Try to create a user
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/users/",
|
"/api/v1/users/",
|
||||||
json={"name": "test", "password": "pass"},
|
json={"name": "test", "password": "pass"},
|
||||||
headers=user_auth_headers
|
headers=user_auth_headers
|
||||||
)
|
)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
# Try to get users
|
# Try to get users
|
||||||
response = client.get("/users/", headers=user_auth_headers)
|
response = client.get("/api/v1/users/", headers=user_auth_headers)
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
# Try to delete the admin user (if ID is known)
|
# Try to delete the admin user (if ID is known)
|
||||||
# This would require knowing the admin user ID
|
# This would require knowing the admin user ID
|
||||||
# response = client.delete(f"/users/{admin_id}", headers=user_auth_headers)
|
# response = client.delete(f"/api/v1/users/{admin_id}", headers=user_auth_headers)
|
||||||
# assert response.status_code == 403
|
# assert response.status_code == 403
|
||||||
|
|||||||
11
uv.lock
generated
11
uv.lock
generated
@@ -609,7 +609,6 @@ version = "0.1.0"
|
|||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "fastapi", extra = ["standard"] },
|
{ name = "fastapi", extra = ["standard"] },
|
||||||
{ name = "paho-mqtt" },
|
|
||||||
{ name = "poetry" },
|
{ name = "poetry" },
|
||||||
{ name = "pwdlib", extra = ["argon2"] },
|
{ name = "pwdlib", extra = ["argon2"] },
|
||||||
{ name = "pyjwt", extra = ["crypto"] },
|
{ name = "pyjwt", extra = ["crypto"] },
|
||||||
@@ -625,7 +624,6 @@ dependencies = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.135.3" },
|
{ name = "fastapi", extras = ["standard"], specifier = ">=0.135.3" },
|
||||||
{ name = "paho-mqtt", specifier = ">=2.1.0" },
|
|
||||||
{ name = "poetry", specifier = ">=2.3.4" },
|
{ name = "poetry", specifier = ">=2.3.4" },
|
||||||
{ name = "pwdlib", extras = ["argon2"], specifier = ">=0.3.0" },
|
{ name = "pwdlib", extras = ["argon2"], specifier = ">=0.3.0" },
|
||||||
{ name = "pyjwt", extras = ["crypto"], specifier = ">=2.12.1" },
|
{ name = "pyjwt", extras = ["crypto"], specifier = ">=2.12.1" },
|
||||||
@@ -952,15 +950,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" },
|
{ url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "paho-mqtt"
|
|
||||||
version = "2.1.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/39/15/0a6214e76d4d32e7f663b109cf71fb22561c2be0f701d67f93950cd40542/paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834", size = 148848, upload-time = "2024-04-29T19:52:55.591Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/cb/00451c3cf31790287768bb12c6bec834f5d292eaf3022afc88e14b8afc94/paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee", size = 67219, upload-time = "2024-04-29T19:52:48.345Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pbs-installer"
|
name = "pbs-installer"
|
||||||
version = "2026.4.7"
|
version = "2026.4.7"
|
||||||
|
|||||||
Reference in New Issue
Block a user