|
8 | 8 |
|
9 | 9 | SECURITY: Test endpoint only. Restrict access in production. |
10 | 10 | """ |
11 | | -from fastapi import APIRouter |
| 11 | + |
12 | 12 | from typing import Dict, Any |
13 | 13 | from datetime import datetime |
| 14 | +from pathlib import Path |
| 15 | +from fastapi import APIRouter |
| 16 | +from pydantic import BaseModel, field_validator |
| 17 | + |
14 | 18 |
|
15 | 19 |
|
16 | 20 | # Import RDA/GDEX libraries |
17 | 21 | try: |
18 | 22 | from rda_python_common.pg_dbi import PgDBI |
| 23 | + from rda_python_common.pg_log import PgLOG |
19 | 24 | RDA_AVAILABLE = True |
20 | 25 | except ImportError as e: |
21 | 26 | raise RuntimeError( |
|
26 | 31 |
|
27 | 32 | router = APIRouter(prefix="/dscheck", tags=["dscheck_testing"]) |
28 | 33 |
|
| 34 | + |
| 35 | +class MetadataRequest(BaseModel): |
| 36 | + """Request model for metadata modification.""" |
| 37 | + data_file_relpath: Path = Path('Web-services/test.nc') |
| 38 | + attr_name: str |
| 39 | + attr_value: str |
| 40 | + |
| 41 | + @field_validator('data_file_relpath') |
| 42 | + @classmethod |
| 43 | + def validate_relative_path(cls, v: Path) -> Path: |
| 44 | + """Ensure path is relative and doesn't escape base directory.""" |
| 45 | + if v.is_absolute(): |
| 46 | + raise ValueError(f"Path must be relative, got absolute path: {v}") |
| 47 | + |
| 48 | + # Prevent path traversal attacks (../) |
| 49 | + if ".." in str(v): |
| 50 | + raise ValueError(f"Path traversal not allowed: {v}") |
| 51 | + |
| 52 | + return v |
| 53 | + |
| 54 | + |
29 | 55 | @router.get("/getinfo1") |
30 | 56 | async def get_dscheck_info() -> Dict[str, Any]: |
31 | 57 | """ |
32 | 58 | Retrieve dscheck information for specialist 'chiaweih'. |
33 | 59 |
|
34 | | - **SECURITY**: This is a test endpoint only. Hardcoded values, no parameters. |
| 60 | + This is a test endpoint with hardcoded query parameters. |
| 61 | +
|
| 62 | + Returns |
| 63 | + ------- |
| 64 | + dict |
| 65 | + Response dictionary containing: |
35 | 66 |
|
36 | | - Query: |
37 | | - PgDBI.pgget("dscheck", "*", "specialist = 'chiaweih'", logact|PgLOG.EXITLG) |
| 67 | + - success (bool): Whether the query succeeded |
| 68 | + - message (str): Human-readable status message |
| 69 | + - timestamp (str): ISO format timestamp |
| 70 | + - specialist (str): The specialist name queried |
| 71 | + - record (dict): The first dscheck record matching the query, or None |
| 72 | + - error (str, optional): Error message if query failed |
38 | 73 |
|
39 | | - Returns: |
40 | | - Single dscheck record with all fields or error message |
| 74 | + Notes |
| 75 | + ----- |
| 76 | + **SECURITY**: This is a test endpoint only. Uses hardcoded query: |
| 77 | + - Table: dscheck |
| 78 | + - Condition: specialist = 'chiaweih' |
| 79 | + - Fields: * (all) |
41 | 80 |
|
42 | | - Example: |
43 | | - GET /dscheck/getinfo1 (full path after router prefix) |
| 81 | + Examples |
| 82 | + -------- |
| 83 | + >>> curl https://api_url/dscheck/getinfo1 |
| 84 | + {"success": true, "message": "Successfully retrieved dscheck record...", ...} |
44 | 85 | """ |
45 | 86 | try: |
46 | 87 | # Hardcoded query as specified |
@@ -88,6 +129,133 @@ async def get_dscheck_info() -> Dict[str, Any]: |
88 | 129 | } |
89 | 130 |
|
90 | 131 |
|
| 132 | + |
| 133 | +@router.post("/metadata") |
| 134 | +async def submit_metadata_modify( |
| 135 | + request: MetadataRequest |
| 136 | +) -> Dict[str, Any]: |
| 137 | + """ |
| 138 | + Submit metadata modification for dscheck records. |
| 139 | +
|
| 140 | + Parameters |
| 141 | + ---------- |
| 142 | + request : MetadataRequest |
| 143 | + Request object containing metadata modification parameters. |
| 144 | +
|
| 145 | + Attributes: |
| 146 | + data_file_relpath : Path |
| 147 | + Relative path to the data file. Must be relative, not absolute. |
| 148 | + Default: 'Web-services/test.nc' |
| 149 | + attr_name : str |
| 150 | + Name of the attribute to modify. |
| 151 | + Default: 'gdex_dsid' |
| 152 | + attr_value : str |
| 153 | + Value of the attribute to set. |
| 154 | + Default: 'd99ext9' |
| 155 | +
|
| 156 | + Returns |
| 157 | + ------- |
| 158 | + dict |
| 159 | + Response dictionary containing: |
| 160 | +
|
| 161 | + - success (bool): Whether the operation succeeded |
| 162 | + - message (str): Human-readable status message |
| 163 | + - timestamp (str): ISO format timestamp |
| 164 | + - record (dict): The modified dscheck record with cindex |
| 165 | + - error (str, optional): Error message if operation failed |
| 166 | +
|
| 167 | + Raises |
| 168 | + ------ |
| 169 | + ValueError |
| 170 | + If data_file_relpath is an absolute path instead of relative. |
| 171 | + Exception |
| 172 | + Database operation failures are caught and returned in response. |
| 173 | +
|
| 174 | + Examples |
| 175 | + -------- |
| 176 | + >>> curl -X POST https://api_url/dscheck/metadata \\ |
| 177 | + ... -H "Content-Type: application/json" \\ |
| 178 | + ... -d '{ |
| 179 | + ... "data_file_relpath": "exchange_subfolder/test.nc", |
| 180 | + ... "attr_name": "gdex_dsid", |
| 181 | + ... "attr_value": "d99ext9" |
| 182 | + ... }' |
| 183 | + {"success": true, "message": "Successfully added dscheck record...", ...} |
| 184 | + """ |
| 185 | + # Initialize dictionary with None values for all expected fields (can extend as needed) |
| 186 | + dict_dscheck_post = { |
| 187 | + 'command': None, |
| 188 | + 'specialist': None, |
| 189 | + 'argv': None, |
| 190 | + 'workdir': None, |
| 191 | + } |
| 192 | + |
| 193 | + # SECURITY: Define data file path with validation |
| 194 | + base_dir = Path('/gdex/data/exchange/').resolve() |
| 195 | + data_file_path = (base_dir / request.data_file_relpath).resolve() |
| 196 | + |
| 197 | + # Verify final resolved path is within base directory (prevent path traversal) |
| 198 | + try: |
| 199 | + data_file_path.relative_to(base_dir) |
| 200 | + except ValueError: |
| 201 | + return { |
| 202 | + "success": False, |
| 203 | + "message": "Access denied: Path escapes base directory", |
| 204 | + "timestamp": datetime.now().isoformat(), |
| 205 | + "record": dict_dscheck_post, |
| 206 | + "error": "Path traversal attempt blocked" |
| 207 | + } |
| 208 | + |
| 209 | + try: |
| 210 | + # Hardcoded shell script for now as specified |
| 211 | + command = 'add_global_attr_av.sh' |
| 212 | + specialist = "chiaweih" |
| 213 | + |
| 214 | + # Use relative paths with pathlib.Path |
| 215 | + workdir_path = Path('/glade/u/home/chiaweih/data_curation_script/') |
| 216 | + argv = f"{data_file_path} {request.attr_name} {request.attr_value}" |
| 217 | + workdir = str(workdir_path) |
| 218 | + |
| 219 | + # Update dictionary with hardcoded values |
| 220 | + dict_dscheck_post['command'] = command |
| 221 | + dict_dscheck_post['specialist'] = specialist |
| 222 | + dict_dscheck_post['argv'] = argv |
| 223 | + dict_dscheck_post['workdir'] = workdir |
| 224 | + |
| 225 | + # Create PgDBI instance and query single record using pgget |
| 226 | + db = PgDBI() |
| 227 | + cindex = db.pgadd("dscheck", dict_dscheck_post, PgLOG.EXITLG|PgLOG.AUTOID|PgLOG.DODFLT) |
| 228 | + |
| 229 | + # check output status and return appropriate response |
| 230 | + if cindex > 0: |
| 231 | + dict_dscheck_post['cindex'] = cindex |
| 232 | + return { |
| 233 | + "success": True, |
| 234 | + "message": f"Successfully added dscheck record with cindex '{cindex}'", |
| 235 | + "timestamp": datetime.now().isoformat(), |
| 236 | + "record": dict_dscheck_post |
| 237 | + } |
| 238 | + else: |
| 239 | + log = PgLOG() |
| 240 | + log.pglog("Fail to add dscheck record for '{}'".format(dict_dscheck_post['command']), logact=PgLOG.RETMSG) |
| 241 | + return { |
| 242 | + "success": False, |
| 243 | + "message": "Failed to add dscheck information, no cindex returned", |
| 244 | + "timestamp": datetime.now().isoformat(), |
| 245 | + "record": dict_dscheck_post, |
| 246 | + "error": "Failed to add dscheck record" |
| 247 | + } |
| 248 | + |
| 249 | + except Exception as e: |
| 250 | + error_msg = str(e) |
| 251 | + return { |
| 252 | + "success": False, |
| 253 | + "message": "Failed to add dscheck information, during exception", |
| 254 | + "timestamp": datetime.now().isoformat(), |
| 255 | + "record": dict_dscheck_post, |
| 256 | + "error": error_msg |
| 257 | + } |
| 258 | + |
91 | 259 | @router.get("/health") |
92 | 260 | async def health_check() -> Dict[str, Any]: |
93 | 261 | """Simple health check for the test API.""" |
|
0 commit comments