Compare commits
10 commits
bearer-tok
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
797eff5b2a | ||
|
|
b9b47dfc2a | ||
|
|
a04397f59d | ||
|
|
c8acc46382 | ||
|
|
11d5849290 | ||
|
|
66cc746937 | ||
|
|
6af3250bea | ||
|
|
6f019020ea | ||
|
|
3ed38a959d | ||
|
|
b1aeac37df |
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -144,7 +144,8 @@ data/trash/
|
||||||
data/gtfs/
|
data/gtfs/
|
||||||
data/grfs
|
data/grfs
|
||||||
data/tmp
|
data/tmp
|
||||||
data/agencyconf/**
|
data/users/**
|
||||||
|
data/**
|
||||||
|
|
||||||
#these files are under app/static but they get copied to the outside directory on startup
|
#these files are under app/static but they get copied to the outside directory on startup
|
||||||
logging.conf
|
logging.conf
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ RUN \
|
||||||
|
|
||||||
ENV ADMIN_TOKEN=''
|
ENV ADMIN_TOKEN=''
|
||||||
ENV RIDE2GO_TOKEN=''
|
ENV RIDE2GO_TOKEN=''
|
||||||
|
ENV SECRET_KEY=''
|
||||||
ENV METRICS_USER=''
|
ENV METRICS_USER=''
|
||||||
ENV METRICS_PASSWORD=''
|
ENV METRICS_PASSWORD=''
|
||||||
|
|
||||||
|
|
|
||||||
2
Jenkinsfile
vendored
2
Jenkinsfile
vendored
|
|
@ -9,7 +9,7 @@ pipeline {
|
||||||
IMAGE_NAME = 'amarillo'
|
IMAGE_NAME = 'amarillo'
|
||||||
AMARILLO_DISTRIBUTION = '0.2'
|
AMARILLO_DISTRIBUTION = '0.2'
|
||||||
TAG = "${AMARILLO_DISTRIBUTION}.${BUILD_NUMBER}"
|
TAG = "${AMARILLO_DISTRIBUTION}.${BUILD_NUMBER}"
|
||||||
PLUGINS = 'amarillo-metrics amarillo-enhancer amarillo-grfs-export'
|
PLUGINS = 'amarillo-metrics amarillo-enhancer amarillo-grfs-exporter'
|
||||||
DEPLOY_WEBHOOK_URL = 'http://amarillo.mfdz.de:8888/mitanand'
|
DEPLOY_WEBHOOK_URL = 'http://amarillo.mfdz.de:8888/mitanand'
|
||||||
DEPLOY_SECRET = credentials('AMARILLO-JENKINS-DEPLOY-SECRET')
|
DEPLOY_SECRET = credentials('AMARILLO-JENKINS-DEPLOY-SECRET')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
For this user, the API-Key must be passed in as an environment variable when
|
||||||
Amarillo is started.
|
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
|
endpoint is always available but not always shown in `/docs`, especially not
|
||||||
when running in production.
|
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
|
Permissions work this way
|
||||||
- the admin is allowed to call all operations on all resources. Only the admin
|
- 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
|
- API-Keys for agencies are allowed to POST/PUT/GET/DELETE their own
|
||||||
resources and GET some public resources.
|
resources and GET some public resources.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
from amarillo.utils.container import container
|
from amarillo.utils.container import container
|
||||||
import logging
|
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.agencies import AgencyService
|
||||||
from amarillo.services.regions import RegionService
|
from amarillo.services.regions import RegionService
|
||||||
|
|
||||||
|
|
@ -27,12 +27,12 @@ def create_required_directories():
|
||||||
assert_folder_exists(f'data/{subdir}/{agency_id}')
|
assert_folder_exists(f'data/{subdir}/{agency_id}')
|
||||||
|
|
||||||
# Agency configurations
|
# Agency configurations
|
||||||
assert_folder_exists(agency_conf_directory)
|
assert_folder_exists(user_conf_directory)
|
||||||
|
|
||||||
|
|
||||||
def configure_services():
|
def configure_services():
|
||||||
container['agencyconf'] = AgencyConfService()
|
container['users'] = UserService()
|
||||||
logger.info("Loaded %d agency configuration(s)", len(container['agencyconf'].agency_id_to_agency_conf))
|
logger.info("Loaded %d user configuration(s)", len(container['users'].user_id_to_user_conf))
|
||||||
|
|
||||||
container['agencies'] = AgencyService()
|
container['agencies'] = AgencyService()
|
||||||
logger.info("Loaded %d agencies", len(container['agencies'].agencies))
|
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
|
import amarillo.plugins
|
||||||
from amarillo.configuration import configure_services, configure_admin_token
|
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
|
import amarillo.services.oauth2 as oauth2
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
|
@ -85,7 +85,7 @@ app = FastAPI(title="Amarillo - The Carpooling Intermediary",
|
||||||
|
|
||||||
app.include_router(carpool.router)
|
app.include_router(carpool.router)
|
||||||
app.include_router(agency.router)
|
app.include_router(agency.router)
|
||||||
app.include_router(agencyconf.router)
|
app.include_router(users.router)
|
||||||
app.include_router(region.router)
|
app.include_router(region.router)
|
||||||
app.include_router(oauth2.router)
|
app.include_router(oauth2.router)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
from typing import Optional
|
from typing import Annotated, Optional, List
|
||||||
from pydantic import ConfigDict, BaseModel, Field
|
from pydantic import ConfigDict, BaseModel, Field
|
||||||
|
class User(BaseModel):
|
||||||
|
#TODO: add attributes admin, permissions, fullname, email
|
||||||
|
|
||||||
|
user_id: str = Field(
|
||||||
class AgencyConf(BaseModel):
|
|
||||||
agency_id: str = Field(
|
|
||||||
description="ID of the agency that uses this token.",
|
description="ID of the agency that uses this token.",
|
||||||
min_length=1,
|
min_length=1,
|
||||||
max_length=20,
|
max_length=20,
|
||||||
|
|
@ -21,6 +21,11 @@ class AgencyConf(BaseModel):
|
||||||
min_length=8,
|
min_length=8,
|
||||||
max_length=256,
|
max_length=256,
|
||||||
examples=["$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"])
|
examples=["$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW"])
|
||||||
|
permissions: Optional[List[Annotated[str, Field(pattern=r'^[a-z0-9-]+(:[a-z]+)?$')]]] = Field([],
|
||||||
|
description="The permissions of this user, a list of strings in the format <agency:operation> or <operation>",
|
||||||
|
max_length=256,
|
||||||
|
# pattern=r'^[a-zA-Z0-9]+(:[a-zA-Z]+)?$', #TODO
|
||||||
|
examples=["ride2go:read", "all:read", "admin", "geojson"])
|
||||||
model_config = ConfigDict(json_schema_extra={
|
model_config = ConfigDict(json_schema_extra={
|
||||||
"title": "Agency Configuration",
|
"title": "Agency Configuration",
|
||||||
"description": "Configuration for an agency.",
|
"description": "Configuration for an agency.",
|
||||||
|
|
@ -5,8 +5,8 @@ from typing import List
|
||||||
from fastapi import APIRouter, HTTPException, status, Depends
|
from fastapi import APIRouter, HTTPException, status, Depends
|
||||||
|
|
||||||
from amarillo.models.Carpool import Carpool, Agency
|
from amarillo.models.Carpool import Carpool, Agency
|
||||||
from amarillo.routers.agencyconf import verify_permission_for_same_agency_or_admin
|
from amarillo.models.User import User
|
||||||
from amarillo.services.oauth2 import get_current_agency
|
from amarillo.services.oauth2 import get_current_user, verify_permission
|
||||||
# TODO should move this to service
|
# TODO should move this to service
|
||||||
from amarillo.routers.carpool import store_carpool, delete_agency_carpools_older_than
|
from amarillo.routers.carpool import store_carpool, delete_agency_carpools_older_than
|
||||||
from amarillo.services.agencies import AgencyService
|
from amarillo.services.agencies import AgencyService
|
||||||
|
|
@ -33,7 +33,7 @@ router = APIRouter(
|
||||||
status.HTTP_404_NOT_FOUND: {"description": "Agency not found"},
|
status.HTTP_404_NOT_FOUND: {"description": "Agency not found"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
async def get_agency(agency_id: str, admin_api_key: str = Depends(get_current_agency)) -> Agency:
|
async def get_agency(agency_id: str, requesting_user: User = Depends(get_current_user)) -> Agency:
|
||||||
agencies: AgencyService = container['agencies']
|
agencies: AgencyService = container['agencies']
|
||||||
agency = agencies.get_agency(agency_id)
|
agency = agencies.get_agency(agency_id)
|
||||||
agency_exists = agency is not None
|
agency_exists = agency is not None
|
||||||
|
|
@ -62,8 +62,8 @@ async def get_agency(agency_id: str, admin_api_key: str = Depends(get_current_ag
|
||||||
status.HTTP_500_INTERNAL_SERVER_ERROR: {
|
status.HTTP_500_INTERNAL_SERVER_ERROR: {
|
||||||
"description": "Import error"}
|
"description": "Import error"}
|
||||||
})
|
})
|
||||||
async def sync(agency_id: str, requesting_agency_id: str = Depends(get_current_agency)) -> List[Carpool]:
|
async def sync(agency_id: str, requesting_user: User = Depends(get_current_user)) -> List[Carpool]:
|
||||||
await verify_permission_for_same_agency_or_admin(agency_id, requesting_agency_id)
|
verify_permission(f"{agency_id}:sync")
|
||||||
|
|
||||||
if agency_id == "ride2go":
|
if agency_id == "ride2go":
|
||||||
import_function = import_ride2go
|
import_function = import_ride2go
|
||||||
|
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
import logging
|
|
||||||
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.services.oauth2 import get_current_agency, verify_admin
|
|
||||||
from amarillo.services.config import config
|
|
||||||
from amarillo.utils.container import container
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
router = APIRouter(
|
|
||||||
prefix="/agencyconf",
|
|
||||||
tags=["agencyconf"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# This endpoint is not shown in PROD installations, only in development
|
|
||||||
# TODO make this an explicit config option
|
|
||||||
include_in_schema = config.env != 'PROD'
|
|
||||||
|
|
||||||
|
|
||||||
# TODO Return code 403 Unauthoized (in response_status_codes as well...)
|
|
||||||
async def verify_permission_for_same_agency_or_admin(agency_id_in_path_or_body, agency_id_from_api_key):
|
|
||||||
"""Verifies that an agency is accessing something it owns or the user is admin
|
|
||||||
|
|
||||||
The agency_id is part of some paths, or when not in the path it is in the body, e.g. in PUT /carpool.
|
|
||||||
|
|
||||||
This function encapsulates the formula 'working with own stuff, or admin'.
|
|
||||||
"""
|
|
||||||
is_permitted = agency_id_in_path_or_body == agency_id_from_api_key or agency_id_from_api_key == "admin"
|
|
||||||
|
|
||||||
if not is_permitted:
|
|
||||||
message = f"Working with {agency_id_in_path_or_body} resources is not permitted for {agency_id_from_api_key}."
|
|
||||||
logger.error(message)
|
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/",
|
|
||||||
include_in_schema=include_in_schema,
|
|
||||||
operation_id="getAgencyIdsWhichHaveAConfiguration",
|
|
||||||
summary="Get agency_ids which have a configuration",
|
|
||||||
response_model=List[str],
|
|
||||||
description="Returns the agency_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()
|
|
||||||
|
|
||||||
|
|
||||||
@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)
|
|
||||||
|
|
||||||
# TODO 400->403
|
|
||||||
@router.delete("/{agency_id}",
|
|
||||||
include_in_schema=include_in_schema,
|
|
||||||
operation_id="deleteAgencyConf",
|
|
||||||
status_code=status.HTTP_200_OK,
|
|
||||||
summary="Delete configuration of an agency. Returns true if the token for the agency 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"
|
|
||||||
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}"
|
|
||||||
logger.error(message)
|
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
|
||||||
|
|
||||||
agency_conf_service: AgencyConfService = container['agencyconf']
|
|
||||||
|
|
||||||
agency_exists = agency_id in agency_conf_service.get_agency_ids()
|
|
||||||
|
|
||||||
if not agency_exists:
|
|
||||||
message = f"No config for {agency_id}"
|
|
||||||
logger.error(message)
|
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
|
||||||
|
|
||||||
agency_conf_service.delete(agency_id)
|
|
||||||
|
|
@ -9,8 +9,8 @@ from fastapi import APIRouter, Body, HTTPException, status, Depends
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from amarillo.models.Carpool import Carpool
|
from amarillo.models.Carpool import Carpool
|
||||||
from amarillo.routers.agencyconf import verify_permission_for_same_agency_or_admin
|
from amarillo.models.User import User
|
||||||
from amarillo.services.oauth2 import get_current_agency
|
from amarillo.services.oauth2 import get_current_user, verify_permission
|
||||||
from amarillo.tests.sampledata import examples
|
from amarillo.tests.sampledata import examples
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -33,8 +33,8 @@ router = APIRouter(
|
||||||
|
|
||||||
})
|
})
|
||||||
async def post_carpool(carpool: Carpool = Body(..., examples=examples),
|
async def post_carpool(carpool: Carpool = Body(..., examples=examples),
|
||||||
requesting_agency_id: str = Depends(get_current_agency)) -> Carpool:
|
requesting_user: User = Depends(get_current_user)) -> Carpool:
|
||||||
await verify_permission_for_same_agency_or_admin(carpool.agency, requesting_agency_id)
|
verify_permission(f"{carpool.agency}:write", requesting_user)
|
||||||
|
|
||||||
logger.info(f"POST trip {carpool.agency}:{carpool.id}.")
|
logger.info(f"POST trip {carpool.agency}:{carpool.id}.")
|
||||||
await assert_agency_exists(carpool.agency)
|
await assert_agency_exists(carpool.agency)
|
||||||
|
|
@ -54,7 +54,9 @@ async def post_carpool(carpool: Carpool = Body(..., examples=examples),
|
||||||
status.HTTP_404_NOT_FOUND: {"description": "Carpool not found"},
|
status.HTTP_404_NOT_FOUND: {"description": "Carpool not found"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
async def get_carpool(agency_id: str, carpool_id: str, api_key: str = Depends(get_current_agency)) -> Carpool:
|
async def get_carpool(agency_id: str, carpool_id: str, requesting_user: User = Depends(get_current_user)) -> Carpool:
|
||||||
|
verify_permission(f"{agency_id}:read", requesting_user)
|
||||||
|
|
||||||
logger.info(f"Get trip {agency_id}:{carpool_id}.")
|
logger.info(f"Get trip {agency_id}:{carpool_id}.")
|
||||||
await assert_agency_exists(agency_id)
|
await assert_agency_exists(agency_id)
|
||||||
await assert_carpool_exists(agency_id, carpool_id)
|
await assert_carpool_exists(agency_id, carpool_id)
|
||||||
|
|
@ -73,8 +75,8 @@ async def get_carpool(agency_id: str, carpool_id: str, api_key: str = Depends(ge
|
||||||
"description": "Carpool or agency not found"},
|
"description": "Carpool or agency not found"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
async def delete_carpool(agency_id: str, carpool_id: str, requesting_agency_id: str = Depends(get_current_agency)):
|
async def delete_carpool(agency_id: str, carpool_id: str, requesting_user: User = Depends(get_current_user)):
|
||||||
await verify_permission_for_same_agency_or_admin(agency_id, requesting_agency_id)
|
verify_permission(f"{agency_id}:write", requesting_user)
|
||||||
|
|
||||||
logger.info(f"Delete trip {agency_id}:{carpool_id}.")
|
logger.info(f"Delete trip {agency_id}:{carpool_id}.")
|
||||||
await assert_agency_exists(agency_id)
|
await assert_agency_exists(agency_id)
|
||||||
|
|
@ -135,7 +137,7 @@ async def save_carpool(carpool, folder: str = 'data/carpool'):
|
||||||
|
|
||||||
|
|
||||||
async def assert_agency_exists(agency_id: str):
|
async def assert_agency_exists(agency_id: str):
|
||||||
agency_exists = os.path.exists(f"conf/agency/{agency_id}.json")
|
agency_exists = os.path.exists(f"data/agency/{agency_id}.json")
|
||||||
if not agency_exists:
|
if not agency_exists:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
|
|
||||||
71
amarillo/routers/users.py
Normal file
71
amarillo/routers/users.py
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException, status, Header, Depends
|
||||||
|
|
||||||
|
from amarillo.models.User import User
|
||||||
|
from amarillo.services.users import UserService
|
||||||
|
from amarillo.services.oauth2 import get_current_user, verify_permission
|
||||||
|
from amarillo.services.config import config
|
||||||
|
from amarillo.utils.container import container
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/users",
|
||||||
|
tags=["users"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# This endpoint is not shown in PROD installations, only in development
|
||||||
|
# TODO make this an explicit config option
|
||||||
|
include_in_schema = config.env != 'PROD'
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/",
|
||||||
|
include_in_schema=include_in_schema,
|
||||||
|
operation_id="getUserIdsWhichHaveAConfiguration",
|
||||||
|
summary="Get user which have a configuration",
|
||||||
|
response_model=List[str],
|
||||||
|
description="Returns the user_ids but not the details.",
|
||||||
|
status_code=status.HTTP_200_OK)
|
||||||
|
async def get_user_ids(requesting_user: User = Depends(get_current_user)) -> [str]:
|
||||||
|
return container['users'].get_user_ids()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/",
|
||||||
|
include_in_schema=include_in_schema,
|
||||||
|
operation_id="postNewUserConf",
|
||||||
|
summary="Post a new User")
|
||||||
|
async def post_user_conf(user_conf: User, requesting_user: User = Depends(get_current_user)):
|
||||||
|
verify_permission("admin", requesting_user)
|
||||||
|
user_service: UserService = container['users']
|
||||||
|
user_service.add(user_conf)
|
||||||
|
|
||||||
|
# TODO 400->403
|
||||||
|
@router.delete("/{user_id}",
|
||||||
|
include_in_schema=include_in_schema,
|
||||||
|
operation_id="deleteUser",
|
||||||
|
status_code=status.HTTP_200_OK,
|
||||||
|
summary="Delete configuration of a user. Returns true if the token for the user existed, "
|
||||||
|
"false if it didn't exist."
|
||||||
|
)
|
||||||
|
async def delete_user(user_id: str, requesting_user: User = Depends(get_current_user)):
|
||||||
|
user_may_delete_own = requesting_user.user_id == user_id
|
||||||
|
admin_may_delete_everything = "admin" in requesting_user.permissions
|
||||||
|
is_permitted = user_may_delete_own or admin_may_delete_everything
|
||||||
|
|
||||||
|
if not is_permitted:
|
||||||
|
message = f"User '{requesting_user.user_id} can not delete the configuration for {user_id}"
|
||||||
|
logger.error(message)
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
||||||
|
|
||||||
|
user_service: UserService = container['users']
|
||||||
|
|
||||||
|
agency_exists = user_id in user_service.get_user_ids()
|
||||||
|
|
||||||
|
if not agency_exists:
|
||||||
|
message = f"No config for {user_id}"
|
||||||
|
logger.error(message)
|
||||||
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
||||||
|
|
||||||
|
user_service.delete(user_id)
|
||||||
|
|
@ -12,8 +12,7 @@ class AgencyService:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.agencies: Dict[str, Agency] = {}
|
self.agencies: Dict[str, Agency] = {}
|
||||||
|
for agency_file_name in glob('data/agency/*.json'):
|
||||||
for agency_file_name in glob('conf/agency/*.json'):
|
|
||||||
with open(agency_file_name) as agency_file:
|
with open(agency_file_name) as agency_file:
|
||||||
dict = json.load(agency_file)
|
dict = json.load(agency_file)
|
||||||
agency = Agency(**dict)
|
agency = Agency(**dict)
|
||||||
|
|
|
||||||
|
|
@ -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}.")
|
|
||||||
|
|
@ -7,6 +7,6 @@ class Config(BaseSettings):
|
||||||
ride2go_query_data: str
|
ride2go_query_data: str
|
||||||
env: str = 'DEV'
|
env: str = 'DEV'
|
||||||
graphhopper_base_url: str = 'https://api.mfdz.de/gh'
|
graphhopper_base_url: str = 'https://api.mfdz.de/gh'
|
||||||
stop_sources_file: str = 'conf/stop_sources.json'
|
stop_sources_file: str = 'data/stop_sources.json'
|
||||||
|
|
||||||
config = Config(_env_file='config', _env_file_encoding='utf-8')
|
config = Config(_env_file='config', _env_file_encoding='utf-8')
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,24 @@
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Annotated, Optional, Union
|
from typing import Annotated, Optional, Union
|
||||||
import logging
|
import logging
|
||||||
|
import logging.config
|
||||||
|
|
||||||
from fastapi import Depends, HTTPException, Header, status, APIRouter
|
from fastapi import Depends, HTTPException, Header, status, APIRouter
|
||||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||||
from jose import JWTError, jwt
|
from jose import JWTError, jwt
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from amarillo.models.User import User
|
||||||
from amarillo.services.passwords import verify_password
|
from amarillo.services.passwords import verify_password
|
||||||
from amarillo.utils.container import container
|
from amarillo.utils.container import container
|
||||||
from amarillo.services.agencies import AgencyService
|
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.models.Carpool import Agency
|
||||||
|
|
||||||
from amarillo.services.secrets import secrets
|
from amarillo.services.secrets import secrets
|
||||||
|
|
||||||
SECRET_KEY = secrets.secret_key
|
SECRET_KEY = secrets.secret_key
|
||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
ACCESS_TOKEN_EXPIRE_MINUTES = 7*24*60
|
||||||
|
|
||||||
logging.config.fileConfig('logging.conf', disable_existing_loggers=False)
|
logging.config.fileConfig('logging.conf', disable_existing_loggers=False)
|
||||||
logger = logging.getLogger("main")
|
logger = logging.getLogger("main")
|
||||||
|
|
@ -30,23 +32,22 @@ class Token(BaseModel):
|
||||||
token_type: str
|
token_type: str
|
||||||
|
|
||||||
class TokenData(BaseModel):
|
class TokenData(BaseModel):
|
||||||
agency_id: Union[str, None] = None
|
user_id: Union[str, None] = None
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False)
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token", auto_error=False)
|
||||||
async def verify_optional_api_key(X_API_Key: Optional[str] = Header(None)):
|
async def verify_optional_api_key(X_API_Key: Optional[str] = Header(None)):
|
||||||
if X_API_Key == None: return None
|
if X_API_Key == None: return None
|
||||||
return await verify_api_key(X_API_Key)
|
return await verify_api_key(X_API_Key)
|
||||||
|
|
||||||
def authenticate_agency(agency_id: str, password: str):
|
def authenticate_user(user_id: str, password: str):
|
||||||
agency_conf_service : AgencyConfService = container['agencyconf']
|
user_service : UserService = container['users']
|
||||||
agency_conf = agency_conf_service.agency_id_to_agency_conf.get(agency_id, None)
|
user_conf = user_service.user_id_to_user_conf.get(user_id, None)
|
||||||
if not agency_conf:
|
if not user_conf:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
agency_password = agency_conf.password
|
if not verify_password(password, user_conf.password):
|
||||||
if not verify_password(password, agency_password):
|
|
||||||
return False
|
return False
|
||||||
return agency_id
|
return user_id
|
||||||
|
|
||||||
|
|
||||||
def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None):
|
def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None):
|
||||||
|
|
@ -59,7 +60,8 @@ def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None
|
||||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||||
return encoded_jwt
|
return encoded_jwt
|
||||||
|
|
||||||
async def get_current_agency(token: str = Depends(oauth2_scheme), agency_from_api_key: str = Depends(verify_optional_api_key)):
|
|
||||||
|
async def get_current_user(token: str = Depends(oauth2_scheme), user_from_api_key: str = Depends(verify_optional_api_key)) -> User:
|
||||||
if token:
|
if token:
|
||||||
credentials_exception = HTTPException(
|
credentials_exception = HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
|
@ -68,19 +70,22 @@ async def get_current_agency(token: str = Depends(oauth2_scheme), agency_from_ap
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
agency_id: str = payload.get("sub")
|
user_id: str = payload.get("sub")
|
||||||
if agency_id is None:
|
if user_id is None:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
token_data = TokenData(agency_id=agency_id)
|
token_data = TokenData(user_id=user_id)
|
||||||
except JWTError:
|
except JWTError:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
user = token_data.agency_id
|
user_id = token_data.user_id
|
||||||
if user is None:
|
if user_id is None:
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
return user
|
|
||||||
elif agency_from_api_key:
|
user_service : UserService = container['users']
|
||||||
logger.info(f"API Key provided: {agency_from_api_key}")
|
return user_service.get_user(user_id)
|
||||||
return agency_from_api_key
|
elif user_from_api_key:
|
||||||
|
logger.info(f"API Key provided: {user_from_api_key}")
|
||||||
|
user_service : UserService = container['users']
|
||||||
|
return user_service.get_user(user_from_api_key)
|
||||||
else:
|
else:
|
||||||
credentials_exception = HTTPException(
|
credentials_exception = HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
|
@ -89,29 +94,49 @@ async def get_current_agency(token: str = Depends(oauth2_scheme), agency_from_ap
|
||||||
)
|
)
|
||||||
raise credentials_exception
|
raise credentials_exception
|
||||||
|
|
||||||
|
def verify_permission(permission: str, user: User):
|
||||||
|
|
||||||
async def verify_admin(agency: str = Depends(get_current_agency)):
|
def permissions_exception():
|
||||||
#TODO: maybe separate error for when admin credentials are invalid vs valid but not admin?
|
return HTTPException(
|
||||||
if(agency != "admin"):
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
message="This operation requires admin privileges"
|
detail=f"User '{user.user_id}' does not have the permission '{permission}'",
|
||||||
logger.error(message)
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=message)
|
)
|
||||||
|
|
||||||
return "admin"
|
#user is admin
|
||||||
|
if "admin" in user.permissions: return
|
||||||
|
|
||||||
|
#permission is an operation
|
||||||
|
if ":" not in permission:
|
||||||
|
if permission not in user.permissions:
|
||||||
|
raise permissions_exception()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
#permission is in agency:operation format
|
||||||
|
def permission_matches(permission, user_permission):
|
||||||
|
prescribed_agency, prescribed_operation = permission.split(":")
|
||||||
|
given_agency, given_operation = user_permission.split(":")
|
||||||
|
|
||||||
|
return (prescribed_agency == given_agency or given_agency == "all") and (prescribed_operation == given_operation or given_operation == "all")
|
||||||
|
|
||||||
|
if any(permission_matches(permission, p) for p in user.permissions if ":" in p): return
|
||||||
|
|
||||||
|
raise permissions_exception()
|
||||||
|
|
||||||
|
|
||||||
# noinspection PyPep8Naming
|
# noinspection PyPep8Naming
|
||||||
# X_API_Key is upper case for OpenAPI
|
# X_API_Key is upper case for OpenAPI
|
||||||
async def verify_api_key(X_API_Key: str = Header(...)):
|
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")
|
@router.post("/token")
|
||||||
async def login_for_access_token(
|
async def login_for_access_token(
|
||||||
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
|
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
|
||||||
) -> Token:
|
) -> Token:
|
||||||
agency = authenticate_agency(form_data.username, form_data.password)
|
agency = authenticate_user(form_data.username, form_data.password)
|
||||||
if not agency:
|
if not agency:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
|
@ -127,7 +152,7 @@ async def login_for_access_token(
|
||||||
# TODO: eventually remove this
|
# TODO: eventually remove this
|
||||||
@router.get("/users/me/", response_model=Agency)
|
@router.get("/users/me/", response_model=Agency)
|
||||||
async def read_users_me(
|
async def read_users_me(
|
||||||
current_agency: Annotated[Agency, Depends(get_current_agency)]
|
current_agency: Annotated[Agency, Depends(get_current_user)]
|
||||||
):
|
):
|
||||||
agency_service : AgencyService = container['agencies']
|
agency_service : AgencyService = container['agencies']
|
||||||
return agency_service.get_agency(agency_id=current_agency)
|
return agency_service.get_agency(agency_id=current_agency)
|
||||||
|
|
@ -135,6 +160,6 @@ async def read_users_me(
|
||||||
# TODO: eventually remove this
|
# TODO: eventually remove this
|
||||||
@router.get("/users/me/items/")
|
@router.get("/users/me/items/")
|
||||||
async def read_own_items(
|
async def read_own_items(
|
||||||
current_agency: Annotated[str, Depends(get_current_agency)]
|
current_agency: Annotated[str, Depends(get_current_user)]
|
||||||
):
|
):
|
||||||
return [{"item_id": "Foo", "owner": current_agency}]
|
return [{"item_id": "Foo", "owner": current_agency}]
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,7 @@ class RegionService:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.regions: Dict[str, Region] = {}
|
self.regions: Dict[str, Region] = {}
|
||||||
|
for region_file_name in glob('data/region/*.json'):
|
||||||
for region_file_name in glob('conf/region/*.json'):
|
|
||||||
with open(region_file_name) as region_file:
|
with open(region_file_name) as region_file:
|
||||||
dict = json.load(region_file)
|
dict = json.load(region_file)
|
||||||
region = Region(**dict)
|
region = Region(**dict)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ from pydantic_settings import BaseSettings
|
||||||
class Secrets(BaseSettings):
|
class Secrets(BaseSettings):
|
||||||
model_config = ConfigDict(extra='allow')
|
model_config = ConfigDict(extra='allow')
|
||||||
ride2go_token: str = Field(None, env = 'RIDE2GO_TOKEN')
|
ride2go_token: str = Field(None, env = 'RIDE2GO_TOKEN')
|
||||||
secret_key: str
|
secret_key: str = Field(None, env = 'SECRET_KEY')
|
||||||
|
|
||||||
|
|
||||||
# Read if file exists, otherwise no error (it's in .gitignore)
|
# Read if file exists, otherwise no error (it's in .gitignore)
|
||||||
|
|
|
||||||
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}.")
|
||||||
|
|
@ -2,14 +2,14 @@
|
||||||
keys=root
|
keys=root
|
||||||
|
|
||||||
[handlers]
|
[handlers]
|
||||||
keys=consoleHandler
|
keys=consoleHandler, fileHandler
|
||||||
|
|
||||||
[formatters]
|
[formatters]
|
||||||
keys=simpleFormatter
|
keys=simpleFormatter
|
||||||
|
|
||||||
[logger_root]
|
[logger_root]
|
||||||
level=INFO
|
level=INFO
|
||||||
handlers=consoleHandler
|
handlers=consoleHandler, fileHandler
|
||||||
propagate=yes
|
propagate=yes
|
||||||
|
|
||||||
[handler_consoleHandler]
|
[handler_consoleHandler]
|
||||||
|
|
@ -18,5 +18,11 @@ level=DEBUG
|
||||||
formatter=simpleFormatter
|
formatter=simpleFormatter
|
||||||
args=(sys.stdout,)
|
args=(sys.stdout,)
|
||||||
|
|
||||||
|
[handler_fileHandler]
|
||||||
|
class=handlers.RotatingFileHandler
|
||||||
|
level=ERROR
|
||||||
|
formatter=simpleFormatter
|
||||||
|
args=('error.log', 'a', 1000000, 3) # Filename, mode, maxBytes, backupCount
|
||||||
|
|
||||||
[formatter_simpleFormatter]
|
[formatter_simpleFormatter]
|
||||||
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
|
format=%(asctime)s - %(name)s - %(levelname)s - %(message)s
|
||||||
37
amarillo/tests/test_permissions.py
Normal file
37
amarillo/tests/test_permissions.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
from fastapi import HTTPException
|
||||||
|
import pytest
|
||||||
|
from amarillo.services.oauth2 import verify_permission
|
||||||
|
from amarillo.models.User import User
|
||||||
|
|
||||||
|
test_user = User(user_id="test", password="testpassword", permissions=["all:read", "mfdz:write", "ride2go:all", "gtfs"])
|
||||||
|
admin_user = User(user_id="admin", password="testpassword", permissions=["admin"])
|
||||||
|
|
||||||
|
def test_operation():
|
||||||
|
verify_permission("gtfs", test_user)
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException):
|
||||||
|
verify_permission("geojson", test_user)
|
||||||
|
|
||||||
|
def test_agency_permission():
|
||||||
|
verify_permission("mvv:read", test_user)
|
||||||
|
verify_permission("mfdz:read", test_user)
|
||||||
|
verify_permission("mfdz:write", test_user)
|
||||||
|
verify_permission("ride2go:write", test_user)
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException):
|
||||||
|
verify_permission("mvv:write", test_user)
|
||||||
|
verify_permission("mvv:all", test_user)
|
||||||
|
|
||||||
|
|
||||||
|
def test_admin():
|
||||||
|
verify_permission("admin", admin_user)
|
||||||
|
verify_permission("gtfs", admin_user)
|
||||||
|
verify_permission("all:all", admin_user)
|
||||||
|
verify_permission("mvv:all", admin_user)
|
||||||
|
verify_permission("mfdz:read", admin_user)
|
||||||
|
verify_permission("mfdz:write", admin_user)
|
||||||
|
verify_permission("ride2go:write", admin_user)
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException):
|
||||||
|
verify_permission("admin", test_user)
|
||||||
|
verify_permission("all:all", test_user)
|
||||||
Loading…
Reference in a new issue