diff --git a/Dockerfile b/Dockerfile index e9fa79f..02010dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,7 @@ COPY ./scripts ./scripts COPY entrypoint.sh ./entrypoint.sh COPY mch.dbn ./mch.dbn COPY MCHtablasycampos.def ./MCHtablasycampos.def +COPY climsoft-multi-deployment.yml ./climsoft-multi-deployment.yml RUN ["chmod", "+x", "/code/scripts/load_initial_surface_data.sh"] diff --git a/climsoft-multi-deployment.yml b/climsoft-multi-deployment.yml new file mode 100644 index 0000000..5a1e44c --- /dev/null +++ b/climsoft-multi-deployment.yml @@ -0,0 +1,3 @@ +test: + NAME: Climsoft Test + DATABASE_URI: "mysql+mysqldb://root:password@mysql:3306/mariadb_climsoft_test_db_v4" diff --git a/docker-compose.yml b/docker-compose.yml index 184dc0a..96bb9c1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,7 @@ services: - PYGEOAPI_CONFIG=/code/pygeoapi-config.yml - PYGEOAPI_OPENAPI=/code/pygeoapi-openapi.yml - PYTHONPATH=/code/surface/api - - CLIMSOFT_DATABASE_URI=mysql+mysqldb://root:password@mysql:3306/mariadb_climsoft_test_db_v4 + - CLIMSOFT_DATABASE_URI=mysql+mysqldb://root:password@mysql:63306/mariadb_climsoft_test_db_v4 - CLIMSOFT_SECRET_KEY=climsoft-secret-key - AUTH_DB_URI=postgresql+psycopg2://dba:dba@opencdms_surface_db:5432/surface - SURFACE_DB_NAME=surface diff --git a/requirements.txt b/requirements.txt index ffc2403..3e00b1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ Flask-RESTful mch-api @ git+https://github.com/opencdms/mch-api.git@main mysqlclient numpy -opencdms @ git+https://github.com/opencdms/pyopencdms.git@main +opencdms@git+https://github.com/opencdms/pyopencdms.git@main climsoft_api @ git+https://github.com/openclimateinitiative/climsoft-api.git@main psycopg2 pandas @@ -37,3 +37,4 @@ starlette uvicorn Werkzeug opencdms_process @ git+https://github.com/opencdms/opencdms-process.git@main +pyyaml diff --git a/src/opencdms_api/climsoft_rbac_config.py b/src/opencdms_api/climsoft_rbac_config.py index 7175aac..e7c9e92 100644 --- a/src/opencdms_api/climsoft_rbac_config.py +++ b/src/opencdms_api/climsoft_rbac_config.py @@ -3,181 +3,422 @@ "post": {"ClimsoftAdmin"}, "get": {"ClimsoftAdmin"}, "put": {"ClimsoftAdmin"}, - "delete": {"ClimsoftAdmin"} - }, - "file-upload/image": { - "post": {"ClimsoftAdmin", "ClimsoftDeveloper"} + "delete": {"ClimsoftAdmin"}, }, + "file-upload/image": {"post": {"ClimsoftAdmin", "ClimsoftDeveloper"}}, "s3/image": { - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"} + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + } }, "acquisition-types": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"}, }, "data-forms": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"}, }, "fault-resolutions": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"}, }, "feature-geographical-positions": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"}, }, "flags": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"}, }, "instruments": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftOperator"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, }, "instrument-fault-reports": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, }, "instrument-inspections": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, }, "obselements": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"}, }, "observation-finals": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftOperator"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftOperator"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftOperator"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftOperator"}, }, "observation-initials": { - "post": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftQC", "ClimsoftOperator", "ClimsoftOperatorSupervisor"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, - "put": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftQC", "ClimsoftOperator", "ClimsoftOperatorSupervisor"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftQC", "ClimsoftOperator", "ClimsoftOperatorSupervisor"} + "post": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftQC", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + }, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, + "put": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftQC", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + }, + "delete": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftQC", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + }, }, "obs-schedule-class": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"}, }, "paper-archives": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"}, }, "paper-archive-definitions": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"}, }, "physical-features": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"}, }, "physical-feature-class": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"}, }, "qc-status-definitions": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftQC"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftQC"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftQC"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftQC"}, }, "qc-types": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftQC"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftQC"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftQC"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftQC"}, }, "reg-keys": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"}, }, "stations": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, }, "station-elements": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, }, "station-location-histories": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, }, "station-qualifiers": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"} + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata"}, }, "synop-features": { "post": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "get": {"ClimsoftAdmin", "ClimsoftDeveloper", "ClimsoftMetadata", "ClimsoftOperator", - "ClimsoftOperatorSupervisor", "ClimsoftProducts", "ClimsoftQC", "ClimsoftRainfall", "ClimsoftRainfall"}, + "get": { + "ClimsoftAdmin", + "ClimsoftDeveloper", + "ClimsoftMetadata", + "ClimsoftOperator", + "ClimsoftOperatorSupervisor", + "ClimsoftProducts", + "ClimsoftQC", + "ClimsoftRainfall", + "ClimsoftRainfall", + }, "put": {"ClimsoftAdmin", "ClimsoftDeveloper"}, - "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"} - } + "delete": {"ClimsoftAdmin", "ClimsoftDeveloper"}, + }, } diff --git a/src/opencdms_api/config.py b/src/opencdms_api/config.py index 0a899cb..ed7cb00 100644 --- a/src/opencdms_api/config.py +++ b/src/opencdms_api/config.py @@ -49,5 +49,4 @@ class Config: env_file_encoding = "utf-8" -print(os.getenv("AUTH_ENABLED")) settings = Settings() diff --git a/src/opencdms_api/db.py b/src/opencdms_api/db.py index fc19b18..66cd855 100644 --- a/src/opencdms_api/db.py +++ b/src/opencdms_api/db.py @@ -6,17 +6,27 @@ from sqlalchemy.engine import Engine from typing import Generator from sqlalchemy.ext.declarative import declarative_base - +from src.opencdms_api.utils.multi_deployment import load_deployment_configs engine = create_engine(settings.DATABASE_URI) -climsoft_engine = create_engine(settings.CLIMSOFT_DATABASE_URI) SessionLocal = sessionmaker(engine) -ClimsoftSessionLocal = sessionmaker(climsoft_engine) Base = declarative_base() ScopedSession = scoped_session(SessionLocal) +def get_climsoft_engine(deployment_key): + deployment_configs = load_deployment_configs() + if deployment_key and deployment_configs[deployment_key].get("DATABASE_URI"): + return create_engine(deployment_configs[deployment_key].get("DATABASE_URI")) + else: + return create_engine(os.getenv("CLIMSOFT_DATABASE_URI")) + + +def get_climsoft_session_local(deployment_key: str = None): + return sessionmaker(get_climsoft_engine(deployment_key)) + + @contextmanager def db_session_scope(bind: Engine = None) -> Generator[Session, None, None]: try: diff --git a/src/opencdms_api/deps.py b/src/opencdms_api/deps.py index 57ec635..4349073 100644 --- a/src/opencdms_api/deps.py +++ b/src/opencdms_api/deps.py @@ -1,5 +1,5 @@ from sqlalchemy.orm.session import Session -from src.opencdms_api.db import SessionLocal, ClimsoftSessionLocal +from src.opencdms_api.db import SessionLocal, get_climsoft_session_local def get_session(): @@ -17,17 +17,8 @@ def get_session(): session.close() -def get_climsoft_session(): +def get_climsoft_session(deployment_key: str = None): """ Api dependency to provide climsoft database session to a request """ - session: Session = ClimsoftSessionLocal() - try: - yield session - session.commit() - except Exception as e: - session.rollback() - raise e - finally: - session.close() - + return get_climsoft_session_local(deployment_key)() diff --git a/src/opencdms_api/main.py b/src/opencdms_api/main.py index ed92c68..81bb22c 100644 --- a/src/opencdms_api/main.py +++ b/src/opencdms_api/main.py @@ -25,6 +25,40 @@ from opencdms.models.climsoft import v4_1_1_core as climsoft_models from src.opencdms_api.middleware import get_authorized_climsoft_user from climsoft_api.api import api_routers +from src.opencdms_api.utils.multi_deployment import load_deployment_configs +from src.opencdms_api.db import get_climsoft_session_local + +deployment_configs = load_deployment_configs() + + +def create_default_clim_user_roles(session: Session): + clim_user_role = ( + session.query(climsoft_models.ClimsoftUser) + .filter_by(userName=settings.DEFAULT_USERNAME) + .one_or_none() + ) + + clim_mysql_default_user_role = session.query( + climsoft_models.ClimsoftUser + ).filter_by( + userName=settings.CLIMSOFT_DEFAULT_USER + ).one_or_none() + + if clim_user_role is None: + clim_user_role = climsoft_models.ClimsoftUser( + userName=settings.DEFAULT_USERNAME, + userRole="ClimsoftAdmin", + ) + session.add(clim_user_role) + session.commit() + + if clim_mysql_default_user_role is None: + clim_mysql_default_user_role = climsoft_models.ClimsoftUser( + userName=settings.CLIMSOFT_DEFAULT_USER, + userRole="ClimsoftAdmin" + ) + session.add(clim_mysql_default_user_role) + session.commit() # load controllers @@ -36,12 +70,10 @@ def get_app(): allow_origins=["*"], allow_credentials=True, allow_methods=["*"], - allow_headers=["*"] + allow_headers=["*"], ) ] ) - climsoft_app = get_climsoft_app() - if settings.SURFACE_API_ENABLED: surface_wsgi_app = WSGIMiddleware(surface_application) app.mount("/surface", surface_wsgi_app) @@ -54,20 +86,27 @@ def get_app(): app.mount("/mch", mch_wsgi_app) if settings.CLIMSOFT_API_ENABLED: + climsoft_dependencies = None if settings.AUTH_ENABLED: - for r in api_routers: - climsoft_app.include_router( - **r.dict(), - dependencies=[ - Depends(get_authorized_climsoft_user) - ] - ) + climsoft_dependencies = [Depends(get_authorized_climsoft_user)] + if deployment_configs: + for key, config in deployment_configs.items(): + climsoft_app = get_climsoft_app(config) + + for r in api_routers: + climsoft_app.include_router( + **r.dict(), dependencies=climsoft_dependencies + ) + + app.mount(f"/{key}/climsoft", climsoft_app) else: + climsoft_app = get_climsoft_app() for r in api_routers: climsoft_app.include_router( - **r.dict() + **r.dict(), dependencies=climsoft_dependencies ) - app.mount("/climsoft", climsoft_app) + + app.mount("/climsoft", climsoft_app) if settings.PYGEOAPI_ENABLED: pygeoapi_wsgi_app = WSGIMiddleware(pygeoapi_app) @@ -108,43 +147,25 @@ def create_default_user(): session.close() if settings.CLIMSOFT_API_ENABLED: - climsoft_engine = create_engine(os.getenv("CLIMSOFT_DATABASE_URI")) - ClimsoftSessionLocal = sessionmaker(climsoft_engine) - session = ClimsoftSessionLocal() - try: - clim_user_role = session.query( - climsoft_models.ClimsoftUser - ).filter_by( - userName=settings.DEFAULT_USERNAME - ).one_or_none() - - clim_mysql_default_user_role = session.query( - climsoft_models.ClimsoftUser - ).filter_by( - userName=settings.CLIMSOFT_DEFAULT_USER - ).one_or_none() - - if clim_user_role is None: - clim_user_role = climsoft_models.ClimsoftUser( - userName=settings.DEFAULT_USERNAME, - userRole="ClimsoftAdmin" - ) - session.add(clim_user_role) - session.commit() - - if clim_mysql_default_user_role is None: - clim_mysql_default_user_role = climsoft_models.ClimsoftUser( - userName=settings.CLIMSOFT_DEFAULT_USER, - userRole="ClimsoftAdmin" - ) - session.add(clim_mysql_default_user_role) - session.commit() - except Exception as e: - session.rollback() - logging.getLogger("OpenCDMSLogger").exception(e) - - - session.close() + if deployment_configs: + for dk in deployment_configs: + session = get_climsoft_session_local(dk)() + try: + create_default_clim_user_roles(session) + except Exception as e: + session.rollback() + logging.getLogger("OpenCDMSLogger").exception(e) + finally: + session.close() + else: + session = get_climsoft_session_local()() + try: + create_default_clim_user_roles(session) + except Exception as e: + session.rollback() + logging.getLogger("OpenCDMSLogger").exception(e) + finally: + session.close() return app @@ -163,7 +184,11 @@ def root(request: Request): if settings.SURFACE_API_ENABLED: supported_apis.append({"title": "Surface API", "url": "/surface"}) if settings.CLIMSOFT_API_ENABLED: - supported_apis.append({"title": "Climsoft API", "url": "/climsoft"}) + if deployment_configs: + for k, v in deployment_configs.items(): + supported_apis.append({"title": v.get("NAME"), "url": f"/{k}/climsoft"}) + else: + supported_apis.append({"title": "Climsoft API", "url": "/climsoft"}) if settings.MCH_API_ENABLED: supported_apis.append({"title": "MCH API", "url": "/mch"}) return templates.TemplateResponse( diff --git a/src/opencdms_api/middleware.py b/src/opencdms_api/middleware.py index b2aee59..21fdf17 100644 --- a/src/opencdms_api/middleware.py +++ b/src/opencdms_api/middleware.py @@ -1,25 +1,30 @@ +import logging from typing import Optional from fastapi.exceptions import HTTPException -import os from fastapi import Request from fastapi.security.utils import get_authorization_scheme_param from jose.exceptions import JWTError from starlette.types import Scope, Receive, Send, ASGIApp from jose import jwt -from sqlalchemy.orm import sessionmaker -from sqlalchemy import create_engine from src.opencdms_api import models from src.opencdms_api.config import settings -from src.opencdms_api.db import db_session_scope +from src.opencdms_api.db import db_session_scope, get_climsoft_session_local from src.opencdms_api import climsoft_rbac_config from src.opencdms_api.schema import CurrentUserSchema, CurrentClimsoftUserSchema from opencdms.models.climsoft import v4_1_1_core as climsoft_models from fastapi import Header, Depends from fastapi.security import OAuth2PasswordBearer +from src.opencdms_api.utils.multi_deployment import load_deployment_configs oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth") -climsoft_oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/climsoft-auth") +climsoft_oauth2_scheme = OAuth2PasswordBearer( + tokenUrl="/climsoft-auth", + scopes={ + f"deployment_key:{key}": f"DB access to deployment key: {key}" + for key in load_deployment_configs() + }, +) def get_user(username: str) -> Optional[CurrentUserSchema]: @@ -29,34 +34,33 @@ def get_user(username: str) -> Optional[CurrentUserSchema]: .filter(models.AuthUser.username == username) .one_or_none() ) - return CurrentUserSchema.from_orm(user) \ - if user is not None else None + return CurrentUserSchema.from_orm(user) if user is not None else None -def get_climsoft_role_for_username(username: str): - climsoft_engine = create_engine(os.getenv("CLIMSOFT_DATABASE_URI")) - ClimsoftSessionLocal = sessionmaker(climsoft_engine) +def get_climsoft_role_for_username(username: str, deployment_key: str = None): + ClimsoftSessionLocal = get_climsoft_session_local(deployment_key) session = ClimsoftSessionLocal() role = None try: - user_role = session.query(climsoft_models.ClimsoftUser).filter_by( - userName=username - ).one_or_none() + user_role = ( + session.query(climsoft_models.ClimsoftUser) + .filter_by(userName=username) + .one_or_none() + ) role = user_role.userRole except Exception as e: + logging.exception(e) pass - - session.close() + finally: + session.close() return role -def has_required_climsoft_role(username, required_role): - return get_climsoft_role_for_username( - username - ) in required_role +def has_required_climsoft_role(username, required_role, deployment_key=None): + return get_climsoft_role_for_username(username, deployment_key) in required_role def extract_resource_from_path(string, sep, start, end): @@ -100,6 +104,7 @@ def __init__(self, app: ASGIApp): super().__init__(app) def authenticate_request(self, request: Request): + user = None authorization_header = request.headers.get("authorization") if authorization_header is None: raise HTTPException(401, "Unauthorized request") @@ -111,7 +116,10 @@ def authenticate_request(self, request: Request): except JWTError: raise HTTPException(401, "Unauthorized request") username = claims["sub"] - user = CurrentClimsoftUserSchema(username=username) + if claims.get("deployment_key"): + user = CurrentClimsoftUserSchema( + username=username, deployment_key=claims.get("deployment_key") + ) if user is None: raise HTTPException(401, "Unauthorized request") return user @@ -122,19 +130,17 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send): if request.url.path not in { "/climsoft", "/climsoft/openapi.json", - "/climsoft/" + "/climsoft/", }: user = self.authenticate_request(request) resource_url = extract_resource_from_path(request.url.path, "/", 3, 4) required_role = climsoft_rbac_config.required_role_lookup.get( resource_url, {} - ).get( - request.method.lower() - ) + ).get(request.method.lower()) if (not required_role) or has_required_climsoft_role( - user.username, required_role + user.username, required_role, user.deployment_key ): await self.app(scope, receive, send) else: @@ -142,9 +148,9 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send): def get_authorized_climsoft_user( - request: Request, - token: str = Depends(climsoft_oauth2_scheme) + request: Request, token: str = Depends(climsoft_oauth2_scheme) ): + user = None try: claims = jwt.decode(token, settings.SURFACE_SECRET_KEY) except JWTError: @@ -152,20 +158,21 @@ def get_authorized_climsoft_user( username = claims["sub"] - user = CurrentClimsoftUserSchema(username=username) + if claims.get("deployment_key"): + user = CurrentClimsoftUserSchema( + username=username, deployment_key=claims.get("deployment_key") + ) if user is None: raise HTTPException(401, "Unauthorized request") resource_url = extract_resource_from_path(request.url.path, "/", 3, 4) - required_role = climsoft_rbac_config.required_role_lookup.get( - resource_url, {} - ).get( + required_role = climsoft_rbac_config.required_role_lookup.get(resource_url, {}).get( request.method.lower() ) if required_role and not has_required_climsoft_role( - user.username, required_role + user.username, required_role, user.deployment_key ): raise HTTPException(status_code=403) diff --git a/src/opencdms_api/models.py b/src/opencdms_api/models.py index c63cb47..e3c823d 100644 --- a/src/opencdms_api/models.py +++ b/src/opencdms_api/models.py @@ -5,12 +5,11 @@ class AuthUser(Base): """Auth user model defined to match that of surface db""" + __tablename__ = "auth_user" # __table_args__ = (UniqueConstraint("org_id", "datasource_id"),) auth_user_id_seq = Sequence("auth_user_id_seq", metadata=Base.metadata) - id = Column( - Integer, primary_key=True, server_default=auth_user_id_seq.next_value() - ) + id = Column(Integer, primary_key=True, server_default=auth_user_id_seq.next_value()) password = Column(String(128), nullable=False) last_login = Column(DateTime, nullable=True) is_superuser = Column(Boolean, nullable=False, default=True) diff --git a/src/opencdms_api/router.py b/src/opencdms_api/router.py index 0e8faef..236e37c 100644 --- a/src/opencdms_api/router.py +++ b/src/opencdms_api/router.py @@ -1,6 +1,8 @@ +import logging from datetime import datetime, timedelta from uuid import uuid4 from fastapi import APIRouter, Depends +from fastapi.security import SecurityScopes from fastapi.exceptions import HTTPException from sqlalchemy import text as sa_text from sqlalchemy.orm.session import Session @@ -10,11 +12,12 @@ UserCreateSchema, AuthenticationSchema, TokenSchema, - ClimsoftTokenSchema + ClimsoftTokenSchema, ) from src.opencdms_api.config import settings from jose import jwt from fastapi.security import OAuth2PasswordRequestForm +from src.opencdms_api.schema import ClimsoftPasswordRequestForm router = APIRouter() @@ -45,7 +48,8 @@ def register_new_user( @router.post("/auth", response_model=TokenSchema) def authenticate( - payload: OAuth2PasswordRequestForm = Depends(), session: Session = Depends(deps.get_session) + payload: OAuth2PasswordRequestForm = Depends(), + session: Session = Depends(deps.get_session), ): user = ( session.query(models.AuthUser) @@ -62,37 +66,51 @@ def authenticate( "exp": datetime.utcnow() + timedelta(hours=24), "token_type": "access", "jti": str(uuid4()), - "user_id": int(user.id) + "user_id": int(user.id), }, key=settings.SURFACE_SECRET_KEY, ) - return TokenSchema(access_token=access_token, first_name=user.first_name, last_name=user.last_name) + return TokenSchema( + access_token=access_token, first_name=user.first_name, last_name=user.last_name + ) @router.post("/climsoft-auth", response_model=ClimsoftTokenSchema) def authenticate( - payload: OAuth2PasswordRequestForm = Depends(), - session: Session = Depends(deps.get_climsoft_session) + payload: ClimsoftPasswordRequestForm = Depends(), ): - user = session.execute(sa_text(f''' - SELECT User - FROM mysql.user - WHERE User="{payload.username}" AND Password=password("{payload.password}") - ''')).all() + deployment_key = next( + filter(lambda x: x.startswith("deployment_key"), payload.scope) + ).split(":")[1] + session: Session = deps.get_climsoft_session(deployment_key) + try: + user = session.execute( + sa_text( + f""" + SELECT User + FROM mysql.user + WHERE User="{payload.username}" AND Password=password("{payload.password}") + """ + ) + ).all() - if not user: - raise HTTPException(400, "Invalid login credentials") - - user = user[0] + if not user: + raise HTTPException(400, "Invalid login credentials") - access_token = jwt.encode( - { - "sub": user['User'], - "exp": datetime.utcnow() + timedelta(hours=24), - "token_type": "access", - "jti": str(uuid4()) - }, - key=settings.SURFACE_SECRET_KEY, - ) - return ClimsoftTokenSchema(access_token=access_token, username=user['User']) + user = user[0] + access_token = jwt.encode( + { + "sub": user["User"], + "exp": datetime.utcnow() + timedelta(hours=24), + "token_type": "access", + "jti": str(uuid4()), + "deployment_key": deployment_key, + }, + key=settings.SURFACE_SECRET_KEY, + ) + return ClimsoftTokenSchema(access_token=access_token, username=user["User"]) + except Exception as e: + logging.exception(e) + finally: + session.close() diff --git a/src/opencdms_api/schema.py b/src/opencdms_api/schema.py index a46cfd9..6a65e23 100644 --- a/src/opencdms_api/schema.py +++ b/src/opencdms_api/schema.py @@ -1,5 +1,5 @@ -import datetime - +from fastapi import Form +from typing import Optional from pydantic.networks import EmailStr import inflection from pydantic import BaseModel @@ -75,3 +75,22 @@ class Config: class CurrentClimsoftUserSchema(BaseModel): username: str + deployment_key: str = None + + +class ClimsoftPasswordRequestForm: + def __init__( + self, + grant_type: str = Form(None, regex="password"), + username: str = Form(...), + password: str = Form(...), + scope: str = Form(""), + client_id: Optional[str] = Form(None), + client_secret: Optional[str] = Form(None), + ): + self.grant_type = grant_type + self.username = username + self.password = password + self.scope = scope.split() + self.client_id = client_id + self.client_secret = client_secret diff --git a/src/opencdms_api/utils/__init__.py b/src/opencdms_api/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/opencdms_api/utils/multi_deployment.py b/src/opencdms_api/utils/multi_deployment.py new file mode 100644 index 0000000..02ba94e --- /dev/null +++ b/src/opencdms_api/utils/multi_deployment.py @@ -0,0 +1,14 @@ +import yaml +from pathlib import Path +from typing import Dict, List + +deployment_config_file = Path.resolve(Path("./climsoft-multi-deployment.yml")) + + +def load_deployment_configs() -> Dict[str, List[Dict[str, str]]]: + deployment_configs = {} + + if deployment_config_file.exists(): + with open(deployment_config_file, "r") as stream: + deployment_configs = yaml.safe_load(stream=stream) + return deployment_configs