Renamed AgencyConf to User
This commit is contained in:
parent
6f019020ea
commit
6af3250bea
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -144,7 +144,7 @@ data/trash/
|
|||
data/gtfs/
|
||||
data/grfs
|
||||
data/tmp
|
||||
data/agencyconf/**
|
||||
data/users/**
|
||||
|
||||
#these files are under app/static but they get copied to the outside directory on startup
|
||||
logging.conf
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ There is a special *admin* user.
|
|||
For this user, the API-Key must be passed in as an environment variable when
|
||||
Amarillo is started.
|
||||
|
||||
The admin can create additional API-Keys in the `/agencyconf` endpoint. This
|
||||
The admin can create additional API-Keys in the `/users` endpoint. This
|
||||
endpoint is always available but not always shown in `/docs`, especially not
|
||||
when running in production.
|
||||
The Swagger docs for `/agencyconf` can be seen on the MFDZ demo server.
|
||||
The Swagger docs for `/users` can be seen on the MFDZ demo server.
|
||||
|
||||
Permissions work this way
|
||||
- the admin is allowed to call all operations on all resources. Only the admin
|
||||
can create new API-Keys by POSTing an `AgencyConf` JSON object to `/agencyconf`.
|
||||
can create new API-Keys by POSTing an `users` JSON object to `/users`.
|
||||
- API-Keys for agencies are allowed to POST/PUT/GET/DELETE their own
|
||||
resources and GET some public resources.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
from amarillo.utils.container import container
|
||||
import logging
|
||||
|
||||
from amarillo.services.agencyconf import AgencyConfService, agency_conf_directory
|
||||
from amarillo.services.users import UserService, user_conf_directory
|
||||
from amarillo.services.agencies import AgencyService
|
||||
from amarillo.services.regions import RegionService
|
||||
|
||||
|
|
@ -27,12 +27,12 @@ def create_required_directories():
|
|||
assert_folder_exists(f'data/{subdir}/{agency_id}')
|
||||
|
||||
# Agency configurations
|
||||
assert_folder_exists(agency_conf_directory)
|
||||
assert_folder_exists(user_conf_directory)
|
||||
|
||||
|
||||
def configure_services():
|
||||
container['agencyconf'] = AgencyConfService()
|
||||
logger.info("Loaded %d agency configuration(s)", len(container['agencyconf'].agency_id_to_agency_conf))
|
||||
container['users'] = UserService()
|
||||
logger.info("Loaded %d user configuration(s)", len(container['users'].user_id_to_user_conf))
|
||||
|
||||
container['agencies'] = AgencyService()
|
||||
logger.info("Loaded %d agencies", len(container['agencies'].agencies))
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ copy_static_files(["conf", "static", "templates", "logging.conf", "config"])
|
|||
|
||||
import amarillo.plugins
|
||||
from amarillo.configuration import configure_services, configure_admin_token
|
||||
from amarillo.routers import carpool, agency, agencyconf, region
|
||||
from amarillo.routers import carpool, agency, region, users
|
||||
import amarillo.services.oauth2 as oauth2
|
||||
from fastapi import FastAPI
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ app = FastAPI(title="Amarillo - The Carpooling Intermediary",
|
|||
|
||||
app.include_router(carpool.router)
|
||||
app.include_router(agency.router)
|
||||
app.include_router(agencyconf.router)
|
||||
app.include_router(users.router)
|
||||
app.include_router(region.router)
|
||||
app.include_router(oauth2.router)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
from typing import Optional
|
||||
from pydantic import ConfigDict, BaseModel, Field
|
||||
|
||||
class User(BaseModel):
|
||||
#TODO: add attributes admin, permissions, fullname, email
|
||||
|
||||
class AgencyConf(BaseModel):
|
||||
agency_id: str = Field(
|
||||
user_id: str = Field(
|
||||
description="ID of the agency that uses this token.",
|
||||
min_length=1,
|
||||
max_length=20,
|
||||
|
|
@ -5,7 +5,7 @@ from typing import List
|
|||
from fastapi import APIRouter, HTTPException, status, Depends
|
||||
|
||||
from amarillo.models.Carpool import Carpool, Agency
|
||||
from amarillo.routers.agencyconf import verify_permission_for_same_agency_or_admin
|
||||
from amarillo.routers.users import verify_permission_for_same_agency_or_admin
|
||||
from amarillo.services.oauth2 import get_current_agency
|
||||
# TODO should move this to service
|
||||
from amarillo.routers.carpool import store_carpool, delete_agency_carpools_older_than
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from fastapi import APIRouter, Body, HTTPException, status, Depends
|
|||
from datetime import datetime
|
||||
|
||||
from amarillo.models.Carpool import Carpool
|
||||
from amarillo.routers.agencyconf import verify_permission_for_same_agency_or_admin
|
||||
from amarillo.routers.users import verify_permission_for_same_agency_or_admin
|
||||
from amarillo.services.oauth2 import get_current_agency
|
||||
from amarillo.tests.sampledata import examples
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ from typing import List
|
|||
|
||||
from fastapi import APIRouter, HTTPException, status, Header, Depends
|
||||
|
||||
from amarillo.models.AgencyConf import AgencyConf
|
||||
from amarillo.services.agencyconf import AgencyConfService
|
||||
from amarillo.models.User import User
|
||||
from amarillo.services.users import UserService
|
||||
from amarillo.services.oauth2 import get_current_agency, verify_admin
|
||||
from amarillo.services.config import config
|
||||
from amarillo.utils.container import container
|
||||
|
|
@ -12,8 +12,8 @@ from amarillo.utils.container import container
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/agencyconf",
|
||||
tags=["agencyconf"]
|
||||
prefix="/users",
|
||||
tags=["users"]
|
||||
)
|
||||
|
||||
# This endpoint is not shown in PROD installations, only in development
|
||||
|
|
@ -39,48 +39,48 @@ async def verify_permission_for_same_agency_or_admin(agency_id_in_path_or_body,
|
|||
|
||||
@router.get("/",
|
||||
include_in_schema=include_in_schema,
|
||||
operation_id="getAgencyIdsWhichHaveAConfiguration",
|
||||
summary="Get agency_ids which have a configuration",
|
||||
operation_id="getUserIdsWhichHaveAConfiguration",
|
||||
summary="Get user which have a configuration",
|
||||
response_model=List[str],
|
||||
description="Returns the agency_ids but not the details.",
|
||||
description="Returns the user_ids but not the details.",
|
||||
status_code=status.HTTP_200_OK)
|
||||
async def get_agency_ids(admin_api_key: str = Depends(get_current_agency)) -> [str]:
|
||||
return container['agencyconf'].get_agency_ids()
|
||||
async def get_user_ids(admin_api_key: str = Depends(get_current_agency)) -> [str]:
|
||||
return container['users'].get_user_ids()
|
||||
|
||||
|
||||
@router.post("/",
|
||||
include_in_schema=include_in_schema,
|
||||
operation_id="postNewAgencyConf",
|
||||
summary="Post a new AgencyConf")
|
||||
async def post_agency_conf(agency_conf: AgencyConf, admin_api_key: str = Depends(verify_admin)):
|
||||
agency_conf_service: AgencyConfService = container['agencyconf']
|
||||
agency_conf_service.add(agency_conf)
|
||||
operation_id="postNewUserConf",
|
||||
summary="Post a new User")
|
||||
async def post_user_conf(user_conf: User, admin_api_key: str = Depends(verify_admin)):
|
||||
user_service: UserService = container['users']
|
||||
user_service.add(user_conf)
|
||||
|
||||
# TODO 400->403
|
||||
@router.delete("/{agency_id}",
|
||||
@router.delete("/{user_id}",
|
||||
include_in_schema=include_in_schema,
|
||||
operation_id="deleteAgencyConf",
|
||||
operation_id="deleteUser",
|
||||
status_code=status.HTTP_200_OK,
|
||||
summary="Delete configuration of an agency. Returns true if the token for the agency existed, "
|
||||
summary="Delete configuration of a user. Returns true if the token for the user existed, "
|
||||
"false if it didn't exist."
|
||||
)
|
||||
async def delete_agency_conf(agency_id: str, requesting_agency_id: str = Depends(get_current_agency)):
|
||||
agency_may_delete_own = requesting_agency_id == agency_id
|
||||
admin_may_delete_everything = requesting_agency_id == "admin"
|
||||
async def delete_user(user_id: str, requesting_user_id: str = Depends(get_current_agency)):
|
||||
agency_may_delete_own = requesting_user_id == user_id
|
||||
admin_may_delete_everything = requesting_user_id == "admin"
|
||||
is_permitted = agency_may_delete_own or admin_may_delete_everything
|
||||
|
||||
if not is_permitted:
|
||||
message = f"The API key for {requesting_agency_id} can not delete the configuration for {agency_id}"
|
||||
message = f"The API key for {requesting_user_id} can not delete the configuration for {user_id}"
|
||||
logger.error(message)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
||||
|
||||
agency_conf_service: AgencyConfService = container['agencyconf']
|
||||
user_service: UserService = container['users']
|
||||
|
||||
agency_exists = agency_id in agency_conf_service.get_agency_ids()
|
||||
agency_exists = user_id in user_service.get_user_ids()
|
||||
|
||||
if not agency_exists:
|
||||
message = f"No config for {agency_id}"
|
||||
message = f"No config for {user_id}"
|
||||
logger.error(message)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
||||
|
||||
agency_conf_service.delete(agency_id)
|
||||
user_service.delete(user_id)
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
import json
|
||||
import os
|
||||
from glob import glob
|
||||
from typing import Dict, List
|
||||
import logging
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from amarillo.models.AgencyConf import AgencyConf
|
||||
from amarillo.services.config import config
|
||||
from amarillo.services.passwords import get_password_hash
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
agency_conf_directory = 'data/agencyconf'
|
||||
|
||||
|
||||
class AgencyConfService:
|
||||
|
||||
def __init__(self):
|
||||
# Both Dicts to be kept in sync always. The second api_key_to_agency_id is like a reverse
|
||||
# cache for the first for fast lookup of valid api keys, which happens on *every* request.
|
||||
self.agency_id_to_agency_conf: Dict[str, AgencyConf] = {}
|
||||
self.api_key_to_agency_id: Dict[str, str] = {}
|
||||
|
||||
for agency_conf_file_name in glob(f'{agency_conf_directory}/*.json'):
|
||||
with open(agency_conf_file_name) as agency_conf_file:
|
||||
dictionary = json.load(agency_conf_file)
|
||||
|
||||
agency_conf = AgencyConf(**dictionary)
|
||||
|
||||
agency_id = agency_conf.agency_id
|
||||
api_key = agency_conf.api_key
|
||||
|
||||
self.agency_id_to_agency_conf[agency_id] = agency_conf
|
||||
if api_key is not None:
|
||||
self.api_key_to_agency_id[api_key] = agency_conf.agency_id
|
||||
|
||||
def get_agency_conf(self, agency_id: str) -> AgencyConf:
|
||||
agency_conf = self.agency_id_to_agency_conf.get(agency_id)
|
||||
return agency_conf
|
||||
|
||||
def check_api_key(self, api_key: str) -> str:
|
||||
"""Check if the API key is valid
|
||||
|
||||
The agencies' api keys are checked first, and the admin's key.
|
||||
|
||||
The agency_id or "admin" is returned for further checks in the caller if the
|
||||
request is permitted, like {agency_id} == agency_id.
|
||||
"""
|
||||
|
||||
agency_id = self.api_key_to_agency_id.get(api_key)
|
||||
|
||||
is_agency = agency_id is not None
|
||||
|
||||
if is_agency:
|
||||
return agency_id
|
||||
|
||||
is_admin = api_key == config.admin_token
|
||||
|
||||
if is_admin:
|
||||
return "admin"
|
||||
|
||||
message = "X-API-Key header invalid"
|
||||
logger.error(message)
|
||||
raise HTTPException(status_code=400, detail=message)
|
||||
|
||||
def add(self, agency_conf: AgencyConf):
|
||||
|
||||
agency_id = agency_conf.agency_id
|
||||
api_key = agency_conf.api_key
|
||||
|
||||
agency_id_exists_already = self.agency_id_to_agency_conf.get(agency_id) is not None
|
||||
|
||||
if agency_id_exists_already:
|
||||
message = f"Agency {agency_id} exists already. To update, delete it first."
|
||||
logger.error(message)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
||||
|
||||
agency_using_this_api_key_already = self.api_key_to_agency_id.get(api_key)
|
||||
a_different_agency_is_using_this_api_key_already = \
|
||||
agency_using_this_api_key_already is not None and \
|
||||
agency_using_this_api_key_already != agency_id
|
||||
|
||||
if a_different_agency_is_using_this_api_key_already:
|
||||
message = f"Duplicate API Key for {agency_id} not permitted. Use a different key."
|
||||
logger.error(message)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
||||
|
||||
agency_conf.password = get_password_hash(agency_conf.password)
|
||||
|
||||
with open(f'{agency_conf_directory}/{agency_id}.json', 'w', encoding='utf-8') as f:
|
||||
f.write(agency_conf.json())
|
||||
|
||||
self.agency_id_to_agency_conf[agency_id] = agency_conf
|
||||
self.api_key_to_agency_id[api_key] = agency_id
|
||||
|
||||
logger.info(f"Added configuration for agency {agency_id}.")
|
||||
|
||||
def get_agency_ids(self) -> List[str]:
|
||||
return list(self.agency_id_to_agency_conf.keys())
|
||||
|
||||
def delete(self, agency_id):
|
||||
|
||||
agency_conf = self.agency_id_to_agency_conf.get(agency_id)
|
||||
|
||||
api_key = agency_conf.api_key
|
||||
|
||||
del self.api_key_to_agency_id[api_key]
|
||||
|
||||
del self.agency_id_to_agency_conf[agency_id]
|
||||
|
||||
os.remove(f'{agency_conf_directory}/{agency_id}.json')
|
||||
|
||||
logger.info(f"Deleted configuration for agency {agency_id}.")
|
||||
|
|
@ -11,7 +11,7 @@ from pydantic import BaseModel
|
|||
from amarillo.services.passwords import verify_password
|
||||
from amarillo.utils.container import container
|
||||
from amarillo.services.agencies import AgencyService
|
||||
from amarillo.services.agencyconf import AgencyConfService
|
||||
from amarillo.services.users import UserService
|
||||
from amarillo.models.Carpool import Agency
|
||||
|
||||
from amarillo.services.secrets import secrets
|
||||
|
|
@ -38,13 +38,12 @@ async def verify_optional_api_key(X_API_Key: Optional[str] = Header(None)):
|
|||
return await verify_api_key(X_API_Key)
|
||||
|
||||
def authenticate_agency(agency_id: str, password: str):
|
||||
agency_conf_service : AgencyConfService = container['agencyconf']
|
||||
agency_conf = agency_conf_service.agency_id_to_agency_conf.get(agency_id, None)
|
||||
if not agency_conf:
|
||||
user_service : UserService = container['users']
|
||||
user_conf = user_service.user_id_to_user_conf.get(agency_id, None)
|
||||
if not user_conf:
|
||||
return False
|
||||
|
||||
agency_password = agency_conf.password
|
||||
if not verify_password(password, agency_password):
|
||||
if not verify_password(password, user_conf.password):
|
||||
return False
|
||||
return agency_id
|
||||
|
||||
|
|
@ -103,9 +102,9 @@ async def verify_admin(agency: str = Depends(get_current_agency)):
|
|||
# noinspection PyPep8Naming
|
||||
# X_API_Key is upper case for OpenAPI
|
||||
async def verify_api_key(X_API_Key: str = Header(...)):
|
||||
agency_conf_service: AgencyConfService = container['agencyconf']
|
||||
user_service: UserService = container['users']
|
||||
|
||||
return agency_conf_service.check_api_key(X_API_Key)
|
||||
return user_service.check_api_key(X_API_Key)
|
||||
|
||||
@router.post("/token")
|
||||
async def login_for_access_token(
|
||||
|
|
|
|||
115
amarillo/services/users.py
Normal file
115
amarillo/services/users.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import json
|
||||
import os
|
||||
from glob import glob
|
||||
from typing import Dict, List
|
||||
import logging
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
from amarillo.models.User import User
|
||||
from amarillo.services.config import config
|
||||
from amarillo.services.passwords import get_password_hash
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
user_conf_directory = 'data/users'
|
||||
|
||||
|
||||
class UserService:
|
||||
|
||||
def __init__(self):
|
||||
# Both Dicts to be kept in sync always. The second api_key_to_agency_id is like a reverse
|
||||
# cache for the first for fast lookup of valid api keys, which happens on *every* request.
|
||||
self.user_id_to_user_conf: Dict[str, User] = {}
|
||||
self.api_key_to_user_id: Dict[str, str] = {}
|
||||
|
||||
for user_conf_file_name in glob(f'{user_conf_directory}/*.json'):
|
||||
with open(user_conf_file_name) as user_conf_file:
|
||||
dictionary = json.load(user_conf_file)
|
||||
|
||||
user_conf = User(**dictionary)
|
||||
|
||||
agency_id = user_conf.user_id
|
||||
api_key = user_conf.api_key
|
||||
|
||||
self.user_id_to_user_conf[agency_id] = user_conf
|
||||
if api_key is not None:
|
||||
self.api_key_to_user_id[api_key] = user_conf.user_id
|
||||
|
||||
def get_user(self, user_id: str) -> User:
|
||||
user_conf = self.user_id_to_user_conf.get(user_id)
|
||||
return user_conf
|
||||
|
||||
def check_api_key(self, api_key: str) -> str:
|
||||
"""Check if the API key is valid
|
||||
|
||||
The agencies' api keys are checked first, and the admin's key.
|
||||
|
||||
The agency_id or "admin" is returned for further checks in the caller if the
|
||||
request is permitted, like {agency_id} == agency_id.
|
||||
"""
|
||||
|
||||
agency_id = self.api_key_to_user_id.get(api_key)
|
||||
|
||||
is_agency = agency_id is not None
|
||||
|
||||
if is_agency:
|
||||
return agency_id
|
||||
|
||||
is_admin = api_key == config.admin_token
|
||||
|
||||
if is_admin:
|
||||
return "admin"
|
||||
|
||||
message = "X-API-Key header invalid"
|
||||
logger.error(message)
|
||||
raise HTTPException(status_code=400, detail=message)
|
||||
|
||||
def add(self, user_conf: User):
|
||||
|
||||
user_id = user_conf.user_id
|
||||
api_key = user_conf.api_key
|
||||
|
||||
agency_id_exists_already = self.user_id_to_user_conf.get(user_id) is not None
|
||||
|
||||
if agency_id_exists_already:
|
||||
message = f"Agency {user_id} exists already. To update, delete it first."
|
||||
logger.error(message)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
||||
|
||||
agency_using_this_api_key_already = self.api_key_to_user_id.get(api_key)
|
||||
a_different_agency_is_using_this_api_key_already = \
|
||||
agency_using_this_api_key_already is not None and \
|
||||
agency_using_this_api_key_already != user_id
|
||||
|
||||
if a_different_agency_is_using_this_api_key_already:
|
||||
message = f"Duplicate API Key for {user_id} not permitted. Use a different key."
|
||||
logger.error(message)
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
||||
|
||||
user_conf.password = get_password_hash(user_conf.password)
|
||||
|
||||
with open(f'{user_conf_directory}/{user_id}.json', 'w', encoding='utf-8') as f:
|
||||
f.write(user_conf.json())
|
||||
|
||||
self.user_id_to_user_conf[user_id] = user_conf
|
||||
self.api_key_to_user_id[api_key] = user_id
|
||||
|
||||
logger.info(f"Added configuration for user {user_id}.")
|
||||
|
||||
def get_user_ids(self) -> List[str]:
|
||||
return list(self.user_id_to_user_conf.keys())
|
||||
|
||||
def delete(self, user_id):
|
||||
|
||||
user_conf = self.user_id_to_user_conf.get(user_id)
|
||||
|
||||
api_key = user_conf.api_key
|
||||
|
||||
del self.api_key_to_user_id[api_key]
|
||||
|
||||
del self.user_id_to_user_conf[user_id]
|
||||
|
||||
os.remove(f'{user_conf_directory}/{user_id}.json')
|
||||
|
||||
logger.info(f"Deleted configuration for {user_id}.")
|
||||
Loading…
Reference in a new issue