nmdc-runtime 2.9.0__py3-none-any.whl → 2.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nmdc-runtime might be problematic. Click here for more details.
- nmdc_runtime/Dockerfile +167 -0
- nmdc_runtime/api/analytics.py +90 -0
- nmdc_runtime/api/boot/capabilities.py +9 -0
- nmdc_runtime/api/boot/object_types.py +126 -0
- nmdc_runtime/api/boot/triggers.py +84 -0
- nmdc_runtime/api/boot/workflows.py +116 -0
- nmdc_runtime/api/core/auth.py +208 -0
- nmdc_runtime/api/core/idgen.py +200 -0
- nmdc_runtime/api/core/metadata.py +788 -0
- nmdc_runtime/api/core/util.py +109 -0
- nmdc_runtime/api/db/mongo.py +435 -0
- nmdc_runtime/api/db/s3.py +37 -0
- nmdc_runtime/api/endpoints/capabilities.py +25 -0
- nmdc_runtime/api/endpoints/find.py +634 -0
- nmdc_runtime/api/endpoints/jobs.py +143 -0
- nmdc_runtime/api/endpoints/lib/helpers.py +274 -0
- nmdc_runtime/api/endpoints/lib/linked_instances.py +180 -0
- nmdc_runtime/api/endpoints/lib/path_segments.py +165 -0
- nmdc_runtime/api/endpoints/metadata.py +260 -0
- nmdc_runtime/api/endpoints/nmdcschema.py +502 -0
- nmdc_runtime/api/endpoints/object_types.py +38 -0
- nmdc_runtime/api/endpoints/objects.py +270 -0
- nmdc_runtime/api/endpoints/operations.py +78 -0
- nmdc_runtime/api/endpoints/queries.py +701 -0
- nmdc_runtime/api/endpoints/runs.py +98 -0
- nmdc_runtime/api/endpoints/search.py +38 -0
- nmdc_runtime/api/endpoints/sites.py +205 -0
- nmdc_runtime/api/endpoints/triggers.py +25 -0
- nmdc_runtime/api/endpoints/users.py +214 -0
- nmdc_runtime/api/endpoints/util.py +796 -0
- nmdc_runtime/api/endpoints/workflows.py +353 -0
- nmdc_runtime/api/entrypoint.sh +7 -0
- nmdc_runtime/api/main.py +425 -0
- nmdc_runtime/api/middleware.py +43 -0
- nmdc_runtime/api/models/capability.py +14 -0
- nmdc_runtime/api/models/id.py +92 -0
- nmdc_runtime/api/models/job.py +37 -0
- nmdc_runtime/api/models/lib/helpers.py +78 -0
- nmdc_runtime/api/models/metadata.py +11 -0
- nmdc_runtime/api/models/nmdc_schema.py +146 -0
- nmdc_runtime/api/models/object.py +180 -0
- nmdc_runtime/api/models/object_type.py +20 -0
- nmdc_runtime/api/models/operation.py +66 -0
- nmdc_runtime/api/models/query.py +246 -0
- nmdc_runtime/api/models/query_continuation.py +111 -0
- nmdc_runtime/api/models/run.py +161 -0
- nmdc_runtime/api/models/site.py +87 -0
- nmdc_runtime/api/models/trigger.py +13 -0
- nmdc_runtime/api/models/user.py +140 -0
- nmdc_runtime/api/models/util.py +260 -0
- nmdc_runtime/api/models/workflow.py +15 -0
- nmdc_runtime/api/openapi.py +178 -0
- nmdc_runtime/api/swagger_ui/assets/custom-elements.js +522 -0
- nmdc_runtime/api/swagger_ui/assets/script.js +247 -0
- nmdc_runtime/api/swagger_ui/assets/style.css +155 -0
- nmdc_runtime/api/swagger_ui/swagger_ui.py +34 -0
- nmdc_runtime/config.py +7 -8
- nmdc_runtime/minter/adapters/repository.py +22 -2
- nmdc_runtime/minter/config.py +2 -0
- nmdc_runtime/minter/domain/model.py +55 -1
- nmdc_runtime/minter/entrypoints/fastapi_app.py +1 -1
- nmdc_runtime/mongo_util.py +1 -2
- nmdc_runtime/site/backup/nmdcdb_mongodump.py +1 -1
- nmdc_runtime/site/backup/nmdcdb_mongoexport.py +1 -3
- nmdc_runtime/site/changesheets/data/OmicsProcessing-to-catted-Biosamples.tsv +1561 -0
- nmdc_runtime/site/changesheets/scripts/missing_neon_soils_ecosystem_data.py +311 -0
- nmdc_runtime/site/changesheets/scripts/neon_soils_add_ncbi_ids.py +210 -0
- nmdc_runtime/site/dagster.yaml +53 -0
- nmdc_runtime/site/entrypoint-daemon.sh +26 -0
- nmdc_runtime/site/entrypoint-dagit-readonly.sh +26 -0
- nmdc_runtime/site/entrypoint-dagit.sh +26 -0
- nmdc_runtime/site/export/ncbi_xml.py +633 -13
- nmdc_runtime/site/export/ncbi_xml_utils.py +115 -1
- nmdc_runtime/site/graphs.py +8 -22
- nmdc_runtime/site/ops.py +147 -181
- nmdc_runtime/site/repository.py +2 -112
- nmdc_runtime/site/resources.py +16 -3
- nmdc_runtime/site/translation/gold_translator.py +4 -12
- nmdc_runtime/site/translation/neon_benthic_translator.py +0 -1
- nmdc_runtime/site/translation/neon_soil_translator.py +4 -5
- nmdc_runtime/site/translation/neon_surface_water_translator.py +0 -2
- nmdc_runtime/site/translation/submission_portal_translator.py +84 -68
- nmdc_runtime/site/translation/translator.py +63 -1
- nmdc_runtime/site/util.py +8 -3
- nmdc_runtime/site/validation/util.py +10 -5
- nmdc_runtime/site/workspace.yaml +13 -0
- nmdc_runtime/static/NMDC_logo.svg +1073 -0
- nmdc_runtime/static/ORCID-iD_icon_vector.svg +4 -0
- nmdc_runtime/static/README.md +5 -0
- nmdc_runtime/static/favicon.ico +0 -0
- nmdc_runtime/util.py +90 -48
- nmdc_runtime-2.11.0.dist-info/METADATA +46 -0
- nmdc_runtime-2.11.0.dist-info/RECORD +128 -0
- {nmdc_runtime-2.9.0.dist-info → nmdc_runtime-2.11.0.dist-info}/WHEEL +1 -2
- nmdc_runtime/containers.py +0 -14
- nmdc_runtime/core/db/Database.py +0 -15
- nmdc_runtime/core/exceptions/__init__.py +0 -23
- nmdc_runtime/core/exceptions/base.py +0 -47
- nmdc_runtime/core/exceptions/token.py +0 -13
- nmdc_runtime/domain/users/queriesInterface.py +0 -18
- nmdc_runtime/domain/users/userSchema.py +0 -37
- nmdc_runtime/domain/users/userService.py +0 -14
- nmdc_runtime/infrastructure/database/db.py +0 -3
- nmdc_runtime/infrastructure/database/models/user.py +0 -10
- nmdc_runtime/lib/__init__.py +0 -1
- nmdc_runtime/lib/extract_nmdc_data.py +0 -41
- nmdc_runtime/lib/load_nmdc_data.py +0 -121
- nmdc_runtime/lib/nmdc_dataframes.py +0 -829
- nmdc_runtime/lib/nmdc_etl_class.py +0 -402
- nmdc_runtime/lib/transform_nmdc_data.py +0 -1117
- nmdc_runtime/site/drsobjects/ingest.py +0 -93
- nmdc_runtime/site/drsobjects/registration.py +0 -131
- nmdc_runtime/site/translation/emsl.py +0 -43
- nmdc_runtime/site/translation/gold.py +0 -53
- nmdc_runtime/site/translation/jgi.py +0 -32
- nmdc_runtime/site/translation/util.py +0 -132
- nmdc_runtime/site/validation/jgi.py +0 -43
- nmdc_runtime-2.9.0.dist-info/METADATA +0 -214
- nmdc_runtime-2.9.0.dist-info/RECORD +0 -84
- nmdc_runtime-2.9.0.dist-info/top_level.txt +0 -1
- /nmdc_runtime/{client → api}/__init__.py +0 -0
- /nmdc_runtime/{core → api/boot}/__init__.py +0 -0
- /nmdc_runtime/{core/db → api/core}/__init__.py +0 -0
- /nmdc_runtime/{domain → api/db}/__init__.py +0 -0
- /nmdc_runtime/{domain/users → api/endpoints}/__init__.py +0 -0
- /nmdc_runtime/{infrastructure → api/endpoints/lib}/__init__.py +0 -0
- /nmdc_runtime/{infrastructure/database → api/models}/__init__.py +0 -0
- /nmdc_runtime/{infrastructure/database/models → api/models/lib}/__init__.py +0 -0
- /nmdc_runtime/{site/drsobjects/__init__.py → api/models/minter.py} +0 -0
- {nmdc_runtime-2.9.0.dist-info → nmdc_runtime-2.11.0.dist-info}/entry_points.txt +0 -0
- {nmdc_runtime-2.9.0.dist-info → nmdc_runtime-2.11.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from fastapi import APIRouter, Depends, HTTPException
|
|
2
|
+
from pymongo.database import Database as MongoDatabase
|
|
3
|
+
from starlette import status
|
|
4
|
+
from toolz import concat
|
|
5
|
+
|
|
6
|
+
from nmdc_runtime.api.core.util import raise404_if_none
|
|
7
|
+
from nmdc_runtime.api.db.mongo import get_mongo_db
|
|
8
|
+
from nmdc_runtime.api.endpoints.util import _request_dagster_run
|
|
9
|
+
from nmdc_runtime.api.models.run import (
|
|
10
|
+
RunSummary,
|
|
11
|
+
RunEvent,
|
|
12
|
+
RunUserSpec,
|
|
13
|
+
)
|
|
14
|
+
from nmdc_runtime.api.models.user import User, get_current_active_user
|
|
15
|
+
from nmdc_runtime.api.models.util import ListResponse
|
|
16
|
+
|
|
17
|
+
router = APIRouter()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@router.post("/runs", response_model=RunSummary)
|
|
21
|
+
def request_run(
|
|
22
|
+
run_user_spec: RunUserSpec = Depends(),
|
|
23
|
+
mdb: MongoDatabase = Depends(get_mongo_db),
|
|
24
|
+
user: User = Depends(get_current_active_user),
|
|
25
|
+
):
|
|
26
|
+
requested = _request_dagster_run(
|
|
27
|
+
nmdc_workflow_id=run_user_spec.job_id,
|
|
28
|
+
nmdc_workflow_inputs=run_user_spec.inputs,
|
|
29
|
+
extra_run_config_data=run_user_spec.run_config,
|
|
30
|
+
mdb=mdb,
|
|
31
|
+
user=user,
|
|
32
|
+
)
|
|
33
|
+
if requested["type"] == "success":
|
|
34
|
+
return _get_run_summary(requested["detail"]["run_id"], mdb)
|
|
35
|
+
else:
|
|
36
|
+
raise HTTPException(
|
|
37
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
38
|
+
detail=(
|
|
39
|
+
f"Runtime failed to start {run_user_spec.job_id} job. "
|
|
40
|
+
f'Detail: {requested["detail"]}'
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _get_run_summary(run_id, mdb) -> RunSummary:
|
|
46
|
+
events_in_order = list(mdb.run_events.find({"run.id": run_id}, sort=[("time", 1)]))
|
|
47
|
+
raise404_if_none(events_in_order or None)
|
|
48
|
+
# TODO put relevant outputs in outputs! (for get_study_metadata job)
|
|
49
|
+
return RunSummary(
|
|
50
|
+
id=run_id,
|
|
51
|
+
status=events_in_order[-1]["type"],
|
|
52
|
+
started_at_time=events_in_order[0]["time"],
|
|
53
|
+
was_started_by=events_in_order[0]["producer"],
|
|
54
|
+
inputs=list(concat(e["inputs"] for e in events_in_order)),
|
|
55
|
+
outputs=list(concat(e["outputs"] for e in events_in_order)),
|
|
56
|
+
job=events_in_order[-1]["job"],
|
|
57
|
+
producer=events_in_order[-1]["producer"],
|
|
58
|
+
schemaURL=events_in_order[-1]["schemaURL"],
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@router.get(
|
|
63
|
+
"/runs/{run_id}", response_model=RunSummary, response_model_exclude_unset=True
|
|
64
|
+
)
|
|
65
|
+
def get_run_summary(
|
|
66
|
+
run_id: str,
|
|
67
|
+
mdb: MongoDatabase = Depends(get_mongo_db),
|
|
68
|
+
):
|
|
69
|
+
return _get_run_summary(run_id, mdb)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@router.get("/runs/{run_id}/events", response_model=ListResponse[RunEvent])
|
|
73
|
+
def list_events_for_run(
|
|
74
|
+
run_id: str,
|
|
75
|
+
mdb: MongoDatabase = Depends(get_mongo_db),
|
|
76
|
+
):
|
|
77
|
+
"""List events for run, in reverse chronological order."""
|
|
78
|
+
raise404_if_none(mdb.run_events.find_one({"run.id": run_id}))
|
|
79
|
+
return {
|
|
80
|
+
"resources": list(mdb.run_events.find({"run.id": run_id}, sort=[("time", -1)]))
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@router.post(
|
|
85
|
+
"/runs/{run_id}/events", response_model=RunEvent, response_model_exclude_unset=True
|
|
86
|
+
)
|
|
87
|
+
def post_run_event(
|
|
88
|
+
run_id: str,
|
|
89
|
+
run_event: RunEvent = Depends(),
|
|
90
|
+
mdb: MongoDatabase = Depends(get_mongo_db),
|
|
91
|
+
):
|
|
92
|
+
if run_id != run_event.run.id:
|
|
93
|
+
raise HTTPException(
|
|
94
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
|
95
|
+
detail=f"Supplied run_event.run.id does not match run_id given in request URL.",
|
|
96
|
+
)
|
|
97
|
+
mdb.run_events.insert_one(run_event.model_dump())
|
|
98
|
+
return _get_run_summary(run_event.run.id, mdb)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends
|
|
4
|
+
from pymongo.database import Database as MongoDatabase
|
|
5
|
+
|
|
6
|
+
from nmdc_runtime.api.db.mongo import get_mongo_db
|
|
7
|
+
from nmdc_runtime.api.endpoints.nmdcschema import strip_oid
|
|
8
|
+
from nmdc_runtime.api.endpoints.util import list_resources
|
|
9
|
+
from nmdc_runtime.api.models.nmdc_schema import (
|
|
10
|
+
DataObject,
|
|
11
|
+
DataObjectListRequest,
|
|
12
|
+
list_request_filter_to_mongo_filter,
|
|
13
|
+
)
|
|
14
|
+
from nmdc_runtime.api.models.util import ListResponse, ListRequest
|
|
15
|
+
|
|
16
|
+
router = APIRouter()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.get(
|
|
20
|
+
"/data_objects",
|
|
21
|
+
response_model=ListResponse[DataObject],
|
|
22
|
+
response_model_exclude_unset=True,
|
|
23
|
+
)
|
|
24
|
+
def data_objects(
|
|
25
|
+
req: DataObjectListRequest = Depends(),
|
|
26
|
+
mdb: MongoDatabase = Depends(get_mongo_db),
|
|
27
|
+
):
|
|
28
|
+
filter_ = list_request_filter_to_mongo_filter(req.model_dump(exclude_unset=True))
|
|
29
|
+
max_page_size = filter_.pop("max_page_size", None)
|
|
30
|
+
page_token = filter_.pop("page_token", None)
|
|
31
|
+
req = ListRequest(
|
|
32
|
+
filter=json.dumps(filter_),
|
|
33
|
+
max_page_size=max_page_size,
|
|
34
|
+
page_token=page_token,
|
|
35
|
+
)
|
|
36
|
+
rv = list_resources(req, mdb, "data_objects")
|
|
37
|
+
rv["resources"] = [strip_oid(d) for d in rv["resources"]]
|
|
38
|
+
return rv
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
from typing import List, Annotated
|
|
2
|
+
|
|
3
|
+
import botocore
|
|
4
|
+
import pymongo.database
|
|
5
|
+
from fastapi import APIRouter, Depends, status, HTTPException, Path, Query
|
|
6
|
+
from starlette.status import HTTP_403_FORBIDDEN
|
|
7
|
+
|
|
8
|
+
from nmdc_runtime.api.core.auth import (
|
|
9
|
+
ClientCredentials,
|
|
10
|
+
get_password_hash,
|
|
11
|
+
)
|
|
12
|
+
from nmdc_runtime.api.core.idgen import generate_one_id, local_part
|
|
13
|
+
from nmdc_runtime.api.core.util import (
|
|
14
|
+
raise404_if_none,
|
|
15
|
+
expiry_dt_from_now,
|
|
16
|
+
dotted_path_for,
|
|
17
|
+
generate_secret,
|
|
18
|
+
API_SITE_ID,
|
|
19
|
+
)
|
|
20
|
+
from nmdc_runtime.api.db.mongo import get_mongo_db
|
|
21
|
+
from nmdc_runtime.api.db.s3 import (
|
|
22
|
+
get_s3_client,
|
|
23
|
+
presigned_url_to_put,
|
|
24
|
+
presigned_url_to_get,
|
|
25
|
+
S3_ID_NS,
|
|
26
|
+
)
|
|
27
|
+
from nmdc_runtime.api.endpoints.util import exists, list_resources
|
|
28
|
+
from nmdc_runtime.api.models.object import (
|
|
29
|
+
AccessMethod,
|
|
30
|
+
AccessURL,
|
|
31
|
+
DrsObjectBase,
|
|
32
|
+
DrsObjectIn,
|
|
33
|
+
)
|
|
34
|
+
from nmdc_runtime.api.models.operation import Operation, ObjectPutMetadata
|
|
35
|
+
from nmdc_runtime.api.models.site import (
|
|
36
|
+
get_current_client_site,
|
|
37
|
+
Site,
|
|
38
|
+
SiteInDB,
|
|
39
|
+
)
|
|
40
|
+
from nmdc_runtime.api.models.user import get_current_active_user, User
|
|
41
|
+
from nmdc_runtime.api.models.util import ListResponse, ListRequest
|
|
42
|
+
from nmdc_runtime.minter.bootstrap import refresh_minter_requesters_from_sites
|
|
43
|
+
|
|
44
|
+
router = APIRouter()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@router.post("/sites", status_code=status.HTTP_201_CREATED, response_model=SiteInDB)
|
|
48
|
+
def create_site(
|
|
49
|
+
site: Site,
|
|
50
|
+
mdb: pymongo.database.Database = Depends(get_mongo_db),
|
|
51
|
+
user: User = Depends(get_current_active_user),
|
|
52
|
+
):
|
|
53
|
+
if exists(mdb.sites, {"id": site.id}):
|
|
54
|
+
raise HTTPException(
|
|
55
|
+
status_code=status.HTTP_409_CONFLICT,
|
|
56
|
+
detail=f"site with supplied id {site.id} already exists",
|
|
57
|
+
)
|
|
58
|
+
mdb.sites.insert_one(site.model_dump())
|
|
59
|
+
refresh_minter_requesters_from_sites()
|
|
60
|
+
rv = mdb.users.update_one(
|
|
61
|
+
{"username": user.username},
|
|
62
|
+
{"$addToSet": {"site_admin": site.id}},
|
|
63
|
+
)
|
|
64
|
+
if rv.modified_count != 1:
|
|
65
|
+
raise HTTPException(
|
|
66
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
67
|
+
detail=f"failed to register user {user.username} as site_admin for site {site.id}.",
|
|
68
|
+
)
|
|
69
|
+
return mdb.sites.find_one({"id": site.id})
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@router.get(
|
|
73
|
+
"/sites", response_model=ListResponse[Site], response_model_exclude_unset=True
|
|
74
|
+
)
|
|
75
|
+
def list_sites(
|
|
76
|
+
req: Annotated[ListRequest, Query()],
|
|
77
|
+
mdb: pymongo.database.Database = Depends(get_mongo_db),
|
|
78
|
+
):
|
|
79
|
+
return list_resources(req, mdb, "sites")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@router.get("/sites/{site_id}", response_model=Site, response_model_exclude_unset=True)
|
|
83
|
+
def get_site(
|
|
84
|
+
site_id: str,
|
|
85
|
+
mdb: pymongo.database.Database = Depends(get_mongo_db),
|
|
86
|
+
):
|
|
87
|
+
return raise404_if_none(mdb.sites.find_one({"id": site_id}))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def verify_client_site_pair(
|
|
91
|
+
site_id: str,
|
|
92
|
+
mdb: pymongo.database.Database = Depends(get_mongo_db),
|
|
93
|
+
client_site: Site = Depends(get_current_client_site),
|
|
94
|
+
):
|
|
95
|
+
site = raise404_if_none(
|
|
96
|
+
mdb.sites.find_one({"id": site_id}), detail=f"no site with ID '{site_id}'"
|
|
97
|
+
)
|
|
98
|
+
if site["id"] != client_site.id:
|
|
99
|
+
raise HTTPException(
|
|
100
|
+
status_code=HTTP_403_FORBIDDEN,
|
|
101
|
+
detail=f"client authorized for different site_id than {site_id}",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@router.post(
|
|
106
|
+
"/sites/{site_id}:putObject",
|
|
107
|
+
response_model=Operation[DrsObjectIn, ObjectPutMetadata],
|
|
108
|
+
dependencies=[Depends(verify_client_site_pair)],
|
|
109
|
+
)
|
|
110
|
+
def put_object_in_site(
|
|
111
|
+
site_id: str,
|
|
112
|
+
object_in: DrsObjectBase,
|
|
113
|
+
mdb: pymongo.database.Database = Depends(get_mongo_db),
|
|
114
|
+
s3client: botocore.client.BaseClient = Depends(get_s3_client),
|
|
115
|
+
):
|
|
116
|
+
if site_id != API_SITE_ID:
|
|
117
|
+
raise HTTPException(
|
|
118
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
119
|
+
detail=f"API-mediated object storage for site {site_id} is not enabled.",
|
|
120
|
+
)
|
|
121
|
+
expires_in = 300
|
|
122
|
+
object_id = generate_one_id(mdb, S3_ID_NS)
|
|
123
|
+
url = presigned_url_to_put(
|
|
124
|
+
f"{S3_ID_NS}/{object_id}",
|
|
125
|
+
client=s3client,
|
|
126
|
+
mime_type=object_in.mime_type,
|
|
127
|
+
expires_in=expires_in,
|
|
128
|
+
)
|
|
129
|
+
# XXX ensures defaults are set, e.g. done:false
|
|
130
|
+
op = Operation[DrsObjectIn, ObjectPutMetadata](
|
|
131
|
+
**{
|
|
132
|
+
"id": generate_one_id(mdb, "op"),
|
|
133
|
+
"expire_time": expiry_dt_from_now(days=30, seconds=expires_in),
|
|
134
|
+
"metadata": {
|
|
135
|
+
"object_id": object_id,
|
|
136
|
+
"site_id": site_id,
|
|
137
|
+
"url": url,
|
|
138
|
+
"expires_in_seconds": expires_in,
|
|
139
|
+
"model": dotted_path_for(ObjectPutMetadata),
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
mdb.operations.insert_one(op.model_dump())
|
|
144
|
+
return op
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@router.post(
|
|
148
|
+
"/sites/{site_id}:getObjectLink",
|
|
149
|
+
response_model=AccessURL,
|
|
150
|
+
dependencies=[Depends(verify_client_site_pair)],
|
|
151
|
+
)
|
|
152
|
+
def get_site_object_link(
|
|
153
|
+
site_id: str,
|
|
154
|
+
access_method: AccessMethod,
|
|
155
|
+
s3client: botocore.client.BaseClient = Depends(get_s3_client),
|
|
156
|
+
):
|
|
157
|
+
if site_id != API_SITE_ID:
|
|
158
|
+
raise HTTPException(
|
|
159
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
160
|
+
detail=f"API-mediated object storage for site {site_id} is not enabled.",
|
|
161
|
+
)
|
|
162
|
+
url = presigned_url_to_get(
|
|
163
|
+
f"{S3_ID_NS}/{access_method.access_id}",
|
|
164
|
+
client=s3client,
|
|
165
|
+
)
|
|
166
|
+
return {"url": url}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@router.post("/sites/{site_id}:generateCredentials", response_model=ClientCredentials)
|
|
170
|
+
def generate_credentials_for_site_client(
|
|
171
|
+
site_id: str = Path(
|
|
172
|
+
...,
|
|
173
|
+
description="The ID of the site.",
|
|
174
|
+
),
|
|
175
|
+
mdb: pymongo.database.Database = Depends(get_mongo_db),
|
|
176
|
+
user: User = Depends(get_current_active_user),
|
|
177
|
+
):
|
|
178
|
+
"""
|
|
179
|
+
Generate a client_id and client_secret for the given site.
|
|
180
|
+
|
|
181
|
+
You must be authenticated as a user who is registered as an admin of the given site.
|
|
182
|
+
"""
|
|
183
|
+
raise404_if_none(
|
|
184
|
+
mdb.sites.find_one({"id": site_id}), detail=f"no site with ID '{site_id}'"
|
|
185
|
+
)
|
|
186
|
+
site_admin = mdb.users.find_one({"username": user.username, "site_admin": site_id})
|
|
187
|
+
if not site_admin:
|
|
188
|
+
raise HTTPException(
|
|
189
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
190
|
+
detail="You're not registered as an admin for this site",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# XXX client_id must not contain a ':' because HTTPBasic auth splits on ':'.
|
|
194
|
+
client_id = local_part(generate_one_id(mdb, "site_clients"))
|
|
195
|
+
client_secret = generate_secret()
|
|
196
|
+
hashed_secret = get_password_hash(client_secret)
|
|
197
|
+
mdb.sites.update_one(
|
|
198
|
+
{"id": site_id},
|
|
199
|
+
{"$push": {"clients": {"id": client_id, "hashed_secret": hashed_secret}}},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
"client_id": client_id,
|
|
204
|
+
"client_secret": client_secret,
|
|
205
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
|
|
3
|
+
import pymongo
|
|
4
|
+
from fastapi import APIRouter, Depends
|
|
5
|
+
|
|
6
|
+
from nmdc_runtime.api.core.util import raise404_if_none
|
|
7
|
+
from nmdc_runtime.api.db.mongo import get_mongo_db
|
|
8
|
+
from nmdc_runtime.api.models.trigger import Trigger
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get("/triggers", response_model=List[Trigger])
|
|
14
|
+
def list_triggers(
|
|
15
|
+
mdb: pymongo.database.Database = Depends(get_mongo_db),
|
|
16
|
+
):
|
|
17
|
+
return list(mdb.triggers.find())
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@router.get("/triggers/{trigger_id}", response_model=Trigger)
|
|
21
|
+
def get_trigger(
|
|
22
|
+
trigger_id: str,
|
|
23
|
+
mdb: pymongo.database.Database = Depends(get_mongo_db),
|
|
24
|
+
):
|
|
25
|
+
return raise404_if_none(mdb.triggers.find_one({"id": trigger_id}))
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import pymongo.database
|
|
6
|
+
import requests
|
|
7
|
+
from fastapi import Depends, APIRouter, HTTPException, status, Cookie
|
|
8
|
+
from jose import jws, JWTError
|
|
9
|
+
from starlette.requests import Request
|
|
10
|
+
from starlette.responses import RedirectResponse, PlainTextResponse
|
|
11
|
+
|
|
12
|
+
from nmdc_runtime.api.core.auth import (
|
|
13
|
+
OAuth2PasswordOrClientCredentialsRequestForm,
|
|
14
|
+
Token,
|
|
15
|
+
ACCESS_TOKEN_EXPIRES,
|
|
16
|
+
create_access_token,
|
|
17
|
+
ORCID_NMDC_CLIENT_ID,
|
|
18
|
+
ORCID_JWK,
|
|
19
|
+
ORCID_JWS_VERITY_ALGORITHM,
|
|
20
|
+
credentials_exception,
|
|
21
|
+
ORCID_NMDC_CLIENT_SECRET,
|
|
22
|
+
ORCID_BASE_URL,
|
|
23
|
+
)
|
|
24
|
+
from nmdc_runtime.api.core.auth import get_password_hash
|
|
25
|
+
from nmdc_runtime.api.core.util import generate_secret
|
|
26
|
+
from nmdc_runtime.api.db.mongo import get_mongo_db
|
|
27
|
+
from nmdc_runtime.api.endpoints.util import BASE_URL_EXTERNAL
|
|
28
|
+
from nmdc_runtime.api.models.site import authenticate_site_client
|
|
29
|
+
from nmdc_runtime.api.models.user import UserInDB, UserIn, get_user
|
|
30
|
+
from nmdc_runtime.api.models.user import (
|
|
31
|
+
authenticate_user,
|
|
32
|
+
User,
|
|
33
|
+
get_current_active_user,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
router = APIRouter()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@router.get("/orcid_code", response_class=RedirectResponse, include_in_schema=False)
|
|
40
|
+
async def receive_orcid_code(request: Request, code: str, state: str | None = None):
|
|
41
|
+
rv = requests.post(
|
|
42
|
+
f"{ORCID_BASE_URL}/oauth/token",
|
|
43
|
+
data=(
|
|
44
|
+
f"client_id={ORCID_NMDC_CLIENT_ID}&client_secret={ORCID_NMDC_CLIENT_SECRET}&"
|
|
45
|
+
f"grant_type=authorization_code&code={code}&redirect_uri={BASE_URL_EXTERNAL}/orcid_code"
|
|
46
|
+
),
|
|
47
|
+
headers={
|
|
48
|
+
"Content-type": "application/x-www-form-urlencoded",
|
|
49
|
+
"Accept": "application/json",
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
token_response = rv.json()
|
|
53
|
+
response = RedirectResponse(state or request.url_for("custom_swagger_ui_html"))
|
|
54
|
+
for key in ["user_orcid", "user_name", "user_id_token"]:
|
|
55
|
+
response.set_cookie(
|
|
56
|
+
key=key,
|
|
57
|
+
value=token_response[key.replace("user_", "")],
|
|
58
|
+
max_age=2592000,
|
|
59
|
+
)
|
|
60
|
+
return response
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@router.get("/orcid_jwt")
|
|
64
|
+
async def get_orcid_jwt(user_id_token: Annotated[str | None, Cookie()] = None):
|
|
65
|
+
if user_id_token:
|
|
66
|
+
return PlainTextResponse(content=user_id_token)
|
|
67
|
+
else:
|
|
68
|
+
return PlainTextResponse(content="No ORCiD cookie found. Did you log in?")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@router.post("/token", response_model=Token)
|
|
72
|
+
async def login_for_access_token(
|
|
73
|
+
form_data: OAuth2PasswordOrClientCredentialsRequestForm = Depends(),
|
|
74
|
+
mdb: pymongo.database.Database = Depends(get_mongo_db),
|
|
75
|
+
):
|
|
76
|
+
if form_data.grant_type == "password":
|
|
77
|
+
user = authenticate_user(mdb, form_data.username, form_data.password)
|
|
78
|
+
if not user:
|
|
79
|
+
raise HTTPException(
|
|
80
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
81
|
+
detail="Incorrect username or password",
|
|
82
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
83
|
+
)
|
|
84
|
+
access_token_expires = timedelta(**ACCESS_TOKEN_EXPIRES.model_dump())
|
|
85
|
+
access_token = create_access_token(
|
|
86
|
+
data={"sub": f"user:{user.username}"}, expires_delta=access_token_expires
|
|
87
|
+
)
|
|
88
|
+
else: # form_data.grant_type == "client_credentials"
|
|
89
|
+
# If the HTTP request didn't include a Client Secret, we validate the Client ID as an ORCID JWT.
|
|
90
|
+
# We get a username from that ORCID JWT and fetch the corresponding user record from our database,
|
|
91
|
+
# creating that user record if it doesn't already exist.
|
|
92
|
+
if not form_data.client_secret:
|
|
93
|
+
try:
|
|
94
|
+
payload = jws.verify(
|
|
95
|
+
form_data.client_id,
|
|
96
|
+
ORCID_JWK,
|
|
97
|
+
algorithms=[ORCID_JWS_VERITY_ALGORITHM],
|
|
98
|
+
)
|
|
99
|
+
payload = json.loads(payload.decode())
|
|
100
|
+
issuer: str = payload.get("iss")
|
|
101
|
+
if issuer != ORCID_BASE_URL:
|
|
102
|
+
raise credentials_exception
|
|
103
|
+
subject: str = payload.get("sub")
|
|
104
|
+
user = get_user(mdb, subject)
|
|
105
|
+
if user is None:
|
|
106
|
+
mdb.users.insert_one(
|
|
107
|
+
UserInDB(
|
|
108
|
+
username=subject,
|
|
109
|
+
hashed_password=get_password_hash(generate_secret()),
|
|
110
|
+
).model_dump(exclude_unset=True)
|
|
111
|
+
)
|
|
112
|
+
user = get_user(mdb, subject)
|
|
113
|
+
assert user is not None, "failed to create orcid user"
|
|
114
|
+
access_token_expires = timedelta(**ACCESS_TOKEN_EXPIRES.model_dump())
|
|
115
|
+
access_token = create_access_token(
|
|
116
|
+
data={"sub": f"user:{user.username}"},
|
|
117
|
+
expires_delta=access_token_expires,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
except JWTError:
|
|
121
|
+
raise credentials_exception
|
|
122
|
+
else: # form_data.client_secret
|
|
123
|
+
site = authenticate_site_client(
|
|
124
|
+
mdb, form_data.client_id, form_data.client_secret
|
|
125
|
+
)
|
|
126
|
+
if not site:
|
|
127
|
+
raise HTTPException(
|
|
128
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
129
|
+
detail="Incorrect client_id or client_secret",
|
|
130
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
131
|
+
)
|
|
132
|
+
# TODO make below an absolute time
|
|
133
|
+
access_token_expires = timedelta(**ACCESS_TOKEN_EXPIRES.model_dump())
|
|
134
|
+
access_token = create_access_token(
|
|
135
|
+
data={"sub": f"client:{form_data.client_id}"},
|
|
136
|
+
expires_delta=access_token_expires,
|
|
137
|
+
)
|
|
138
|
+
return {
|
|
139
|
+
"access_token": access_token,
|
|
140
|
+
"token_type": "bearer",
|
|
141
|
+
"expires": ACCESS_TOKEN_EXPIRES.model_dump(),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@router.get(
|
|
146
|
+
"/users/me",
|
|
147
|
+
response_model=User,
|
|
148
|
+
response_model_exclude_unset=True,
|
|
149
|
+
name="Get User Information",
|
|
150
|
+
description="Get information about the logged-in user",
|
|
151
|
+
)
|
|
152
|
+
async def read_users_me(current_user: User = Depends(get_current_active_user)):
|
|
153
|
+
return current_user
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def check_can_create_user(requester: User):
|
|
157
|
+
if "nmdc-runtime-useradmin" not in requester.site_admin:
|
|
158
|
+
raise HTTPException(
|
|
159
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
160
|
+
detail="only admins for site nmdc-runtime-useradmin are allowed to create users.",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@router.post(
|
|
165
|
+
"/users",
|
|
166
|
+
status_code=status.HTTP_201_CREATED,
|
|
167
|
+
response_model=User,
|
|
168
|
+
name="Create User",
|
|
169
|
+
description="Create new user (Admin Only)",
|
|
170
|
+
)
|
|
171
|
+
def create_user(
|
|
172
|
+
user_in: UserIn,
|
|
173
|
+
requester: User = Depends(get_current_active_user),
|
|
174
|
+
mdb: pymongo.database.Database = Depends(get_mongo_db),
|
|
175
|
+
):
|
|
176
|
+
check_can_create_user(requester)
|
|
177
|
+
mdb.users.insert_one(
|
|
178
|
+
UserInDB(
|
|
179
|
+
**user_in.model_dump(),
|
|
180
|
+
hashed_password=get_password_hash(user_in.password),
|
|
181
|
+
).model_dump(exclude_unset=True)
|
|
182
|
+
)
|
|
183
|
+
return mdb.users.find_one({"username": user_in.username})
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@router.put(
|
|
187
|
+
"/users",
|
|
188
|
+
status_code=status.HTTP_200_OK,
|
|
189
|
+
response_model=User,
|
|
190
|
+
name="Update User",
|
|
191
|
+
description="Update information about the user having the specified username (Admin Only)",
|
|
192
|
+
)
|
|
193
|
+
def update_user(
|
|
194
|
+
user_in: UserIn,
|
|
195
|
+
requester: User = Depends(get_current_active_user),
|
|
196
|
+
mdb: pymongo.database.Database = Depends(get_mongo_db),
|
|
197
|
+
):
|
|
198
|
+
check_can_create_user(requester)
|
|
199
|
+
username = user_in.username
|
|
200
|
+
|
|
201
|
+
if user_in.password:
|
|
202
|
+
user_dict = UserInDB(
|
|
203
|
+
**user_in.model_dump(),
|
|
204
|
+
hashed_password=get_password_hash(
|
|
205
|
+
user_in.password
|
|
206
|
+
), # Store the password hash
|
|
207
|
+
).model_dump(exclude_unset=True)
|
|
208
|
+
else:
|
|
209
|
+
user_dict = UserIn(
|
|
210
|
+
**user_in.model_dump(),
|
|
211
|
+
).model_dump(exclude_unset=True)
|
|
212
|
+
|
|
213
|
+
mdb.users.update_one({"username": username}, {"$set": user_dict})
|
|
214
|
+
return mdb.users.find_one({"username": user_in.username})
|