Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions api/app/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@
from urllib.parse import parse_qs, quote, urlencode, urlparse, urlunparse

from app import EPSG, HOSTNAME, TOP_VALUE
from asyncpg.exceptions import UniqueViolationError
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to import 'UniqueViolationError' if we are not using it

from asyncpg.types import Range
from dateutil import parser
from fastapi import status
from fastapi.responses import JSONResponse

_USERNAME_RE = re.compile(r"^[a-zA-Z0-9_]{3,63}$")

Expand Down Expand Up @@ -269,6 +272,29 @@ def validate_required_keys(payload, required_keys):
raise Exception(f"Missing required fields: {', '.join(missing)}")


def handle_duplicate_error(e=None):
message = "Entity already exists."

if e and hasattr(e, "detail") and e.detail:
# Example detail: "Key (name)=(duplicate test) already exists."
# We can extract the column name and the value from here
import re
match = re.search(r"Key \((.*?)\)=\((.*?)\) already exists", e.detail)
if match:
column = match.group(1)
value = match.group(2)
message = f"An entity with {column} '{value}' already exists."
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im a bit unsure about parsing e.detail and returning the extracted column/value to the client. even if we no longer expose the constraint name, we are still deriving the response from internal database error details. would it be safer to return a fixed application-level message like with this name already exists. and keep the detailed exception only for logs?


return JSONResponse(
status_code=status.HTTP_409_CONFLICT,
content={
"code": 409,
"type": "error",
"message": message,
},
)


def validate_epsg(key):
crs = key.get("crs")
if crs is not None:
Expand Down
10 changes: 8 additions & 2 deletions api/app/v1/endpoints/create/feature_of_interest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@

from app import AUTHORIZATION, POSTGRES_PORT_WRITE, VERSIONING
from app.db.asyncpg_db import get_pool, get_pool_w
from app.utils.utils import validate_payload_keys, validate_required_keys
from app.utils.utils import (
handle_duplicate_error,
validate_payload_keys,
validate_required_keys,
)
from app.v1.endpoints.functions import set_role
from asyncpg.exceptions import InsufficientPrivilegeError
from asyncpg.exceptions import InsufficientPrivilegeError, UniqueViolationError
from fastapi import APIRouter, Body, Depends, Header, Request, status
from fastapi.responses import JSONResponse, Response

Expand Down Expand Up @@ -100,6 +104,8 @@ async def create_feature_of_interest(
status_code=status.HTTP_201_CREATED,
headers={"location": header},
)
except UniqueViolationError as e:
return handle_duplicate_error(e)
except InsufficientPrivilegeError:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand Down
17 changes: 13 additions & 4 deletions api/app/v1/endpoints/create/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@

from app import AUTHORIZATION, POSTGRES_PORT_WRITE, VERSIONING
from app.db.asyncpg_db import get_pool, get_pool_w
from app.utils.utils import validate_payload_keys, validate_required_keys
from app.utils.utils import (
handle_duplicate_error,
validate_payload_keys,
validate_required_keys,
)
from app.v1.endpoints.functions import set_role
from asyncpg.exceptions import InsufficientPrivilegeError
from asyncpg.exceptions import InsufficientPrivilegeError, UniqueViolationError
from fastapi import APIRouter, Body, Depends, Header, Request, status
from fastapi.responses import JSONResponse, Response

Expand Down Expand Up @@ -81,6 +85,7 @@ async def create_location(

async with pool.acquire() as connection:
async with connection.transaction():

if current_user is not None:
await set_role(connection, current_user)

Expand All @@ -100,6 +105,8 @@ async def create_location(
status_code=status.HTTP_201_CREATED,
headers={"location": header},
)
except UniqueViolationError as e:
return handle_duplicate_error(e)
except InsufficientPrivilegeError:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand All @@ -115,7 +122,7 @@ async def create_location(
content={
"code": 400,
"type": "error",
"message": str(e),
"message": f"[{type(e).__name__}] {str(e)}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think we should include type(e).name or raw str(e) in the client response here. That still exposes internal implementation details and can leak database/backend information. For this case, it would be safer to return a sanitized conflict message and keep the original exception only in server-side logs

},
)

Expand Down Expand Up @@ -172,6 +179,8 @@ async def create_location_for_thing(
status_code=status.HTTP_201_CREATED,
headers={"location": header},
)
except UniqueViolationError as e:
return handle_duplicate_error(e)
except InsufficientPrivilegeError:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand All @@ -187,6 +196,6 @@ async def create_location_for_thing(
content={
"code": 400,
"type": "error",
"message": str(e),
"message": f"[{type(e).__name__}] {str(e)}",
},
)
10 changes: 8 additions & 2 deletions api/app/v1/endpoints/create/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@
# limitations under the License.
from app import AUTHORIZATION, POSTGRES_PORT_WRITE, VERSIONING
from app.db.asyncpg_db import get_pool, get_pool_w
from app.utils.utils import validate_payload_keys, validate_required_keys
from app.utils.utils import (
handle_duplicate_error,
validate_payload_keys,
validate_required_keys,
)
from app.v1.endpoints.functions import set_role
from asyncpg.exceptions import InsufficientPrivilegeError
from asyncpg.exceptions import InsufficientPrivilegeError, UniqueViolationError
from fastapi import APIRouter, Body, Depends, Header, Request, status
from fastapi.responses import JSONResponse, Response

Expand Down Expand Up @@ -90,6 +94,8 @@ async def create_network(
status_code=status.HTTP_201_CREATED,
headers={"location": header},
)
except UniqueViolationError as e:
return handle_duplicate_error(e)
except InsufficientPrivilegeError as e:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand Down
10 changes: 8 additions & 2 deletions api/app/v1/endpoints/create/observed_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@

from app import AUTHORIZATION, POSTGRES_PORT_WRITE, VERSIONING
from app.db.asyncpg_db import get_pool, get_pool_w
from app.utils.utils import validate_payload_keys, validate_required_keys
from app.utils.utils import (
handle_duplicate_error,
validate_payload_keys,
validate_required_keys,
)
from app.v1.endpoints.functions import set_role
from asyncpg.exceptions import InsufficientPrivilegeError
from asyncpg.exceptions import InsufficientPrivilegeError, UniqueViolationError
from fastapi import APIRouter, Body, Depends, Header, Request, status
from fastapi.responses import JSONResponse, Response

Expand Down Expand Up @@ -98,6 +102,8 @@ async def create_observed_property(
status_code=status.HTTP_201_CREATED,
headers={"location": header},
)
except UniqueViolationError as e:
return handle_duplicate_error(e)
except InsufficientPrivilegeError:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand Down
10 changes: 8 additions & 2 deletions api/app/v1/endpoints/create/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@

from app import AUTHORIZATION, POSTGRES_PORT_WRITE, VERSIONING
from app.db.asyncpg_db import get_pool, get_pool_w
from app.utils.utils import validate_payload_keys, validate_required_keys
from app.utils.utils import (
handle_duplicate_error,
validate_payload_keys,
validate_required_keys,
)
from app.v1.endpoints.functions import set_role
from asyncpg.exceptions import InsufficientPrivilegeError
from asyncpg.exceptions import InsufficientPrivilegeError, UniqueViolationError
from fastapi import APIRouter, Body, Depends, Header, Request, status
from fastapi.responses import JSONResponse, Response

Expand Down Expand Up @@ -100,6 +104,8 @@ async def create_sensor(
status_code=status.HTTP_201_CREATED,
headers={"location": header},
)
except UniqueViolationError as e:
return handle_duplicate_error(e)
except InsufficientPrivilegeError:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand Down
12 changes: 10 additions & 2 deletions api/app/v1/endpoints/create/thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@

from app import AUTHORIZATION, POSTGRES_PORT_WRITE, VERSIONING
from app.db.asyncpg_db import get_pool, get_pool_w
from app.utils.utils import validate_payload_keys, validate_required_keys
from app.utils.utils import (
handle_duplicate_error,
validate_payload_keys,
validate_required_keys,
)
from app.v1.endpoints.functions import set_role
from asyncpg.exceptions import InsufficientPrivilegeError
from asyncpg.exceptions import InsufficientPrivilegeError, UniqueViolationError
from fastapi import APIRouter, Body, Depends, Header, Request, status
from fastapi.responses import JSONResponse, Response

Expand Down Expand Up @@ -98,6 +102,8 @@ async def create_thing(
status_code=status.HTTP_201_CREATED,
headers={"location": header},
)
except UniqueViolationError as e:
return handle_duplicate_error(e)
except InsufficientPrivilegeError as e:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand Down Expand Up @@ -171,6 +177,8 @@ async def create_thing_for_location(
status_code=status.HTTP_201_CREATED,
headers={"location": header},
)
except UniqueViolationError as e:
return handle_duplicate_error(e)
except InsufficientPrivilegeError:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand Down
6 changes: 4 additions & 2 deletions api/app/v1/endpoints/update/feature_of_interest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@

from app import AUTHORIZATION, POSTGRES_PORT_WRITE, VERSIONING
from app.db.asyncpg_db import get_pool, get_pool_w
from app.utils.utils import validate_payload_keys
from app.utils.utils import handle_duplicate_error, validate_payload_keys
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you import from asyncpg.exceptions import InsufficientPrivilegeError, UniqueViolationError as it was used but never import.

from app.v1.endpoints.functions import (
get_datastreams_from_foi,
set_role,
update_datastream_observedArea,
)
from asyncpg.exceptions import InsufficientPrivilegeError
from asyncpg.exceptions import InsufficientPrivilegeError, UniqueViolationError
from fastapi import APIRouter, Body, Depends, Header, status
from fastapi.responses import JSONResponse, Response

Expand Down Expand Up @@ -133,6 +133,8 @@ async def update_feature_of_interest(
await connection.execute("RESET ROLE;")

return Response(status_code=status.HTTP_200_OK)
except UniqueViolationError as e:
return handle_duplicate_error(e)
except InsufficientPrivilegeError:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand Down
6 changes: 4 additions & 2 deletions api/app/v1/endpoints/update/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

from app import AUTHORIZATION, POSTGRES_PORT_WRITE, VERSIONING
from app.db.asyncpg_db import get_pool, get_pool_w
from app.utils.utils import validate_payload_keys
from app.utils.utils import handle_duplicate_error, validate_payload_keys
from app.v1.endpoints.functions import set_role
from asyncpg.exceptions import InsufficientPrivilegeError
from asyncpg.exceptions import InsufficientPrivilegeError, UniqueViolationError
from fastapi import APIRouter, Body, Depends, Header, status
from fastapi.responses import JSONResponse, Response

Expand Down Expand Up @@ -115,6 +115,8 @@ async def update_location(
await connection.execute("RESET ROLE;")

return Response(status_code=status.HTTP_200_OK)
except UniqueViolationError as e:
return handle_duplicate_error(e)
except InsufficientPrivilegeError:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand Down
6 changes: 4 additions & 2 deletions api/app/v1/endpoints/update/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
# limitations under the License.
from app import AUTHORIZATION, POSTGRES_PORT_WRITE, VERSIONING
from app.db.asyncpg_db import get_pool, get_pool_w
from app.utils.utils import validate_payload_keys
from app.utils.utils import handle_duplicate_error, validate_payload_keys
from app.v1.endpoints.functions import set_role
from asyncpg.exceptions import InsufficientPrivilegeError
from asyncpg.exceptions import InsufficientPrivilegeError, UniqueViolationError
from fastapi import APIRouter, Body, Depends, Header, status
from fastapi.responses import JSONResponse, Response

Expand Down Expand Up @@ -101,6 +101,8 @@ async def update_network(
await connection.execute("RESET ROLE;")

return Response(status_code=status.HTTP_200_OK)
except UniqueViolationError as e:
return handle_duplicate_error(e)
except InsufficientPrivilegeError:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand Down
6 changes: 4 additions & 2 deletions api/app/v1/endpoints/update/observed_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

from app import AUTHORIZATION, POSTGRES_PORT_WRITE, VERSIONING
from app.db.asyncpg_db import get_pool, get_pool_w
from app.utils.utils import validate_payload_keys
from app.utils.utils import handle_duplicate_error, validate_payload_keys
from app.v1.endpoints.functions import set_role
from asyncpg.exceptions import InsufficientPrivilegeError
from asyncpg.exceptions import InsufficientPrivilegeError, UniqueViolationError
from fastapi import APIRouter, Body, Depends, Header, status
from fastapi.responses import JSONResponse, Response

Expand Down Expand Up @@ -117,6 +117,8 @@ async def update_observed_property(
await connection.execute("RESET ROLE;")

return Response(status_code=status.HTTP_200_OK)
except UniqueViolationError as e:
return handle_duplicate_error(e)
except InsufficientPrivilegeError:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand Down
6 changes: 4 additions & 2 deletions api/app/v1/endpoints/update/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

from app import AUTHORIZATION, POSTGRES_PORT_WRITE, VERSIONING
from app.db.asyncpg_db import get_pool, get_pool_w
from app.utils.utils import validate_payload_keys
from app.utils.utils import handle_duplicate_error, validate_payload_keys
from app.v1.endpoints.functions import set_role
from asyncpg.exceptions import InsufficientPrivilegeError
from asyncpg.exceptions import InsufficientPrivilegeError, UniqueViolationError
from fastapi import APIRouter, Body, Depends, Header, status
from fastapi.responses import JSONResponse, Response

Expand Down Expand Up @@ -110,6 +110,8 @@ async def update_sensor(
await connection.execute("RESET ROLE;")

return Response(status_code=status.HTTP_200_OK)
except UniqueViolationError as e:
return handle_duplicate_error(e)
except InsufficientPrivilegeError:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand Down
6 changes: 4 additions & 2 deletions api/app/v1/endpoints/update/thing.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@

from app import AUTHORIZATION, POSTGRES_PORT_WRITE, VERSIONING
from app.db.asyncpg_db import get_pool, get_pool_w
from app.utils.utils import validate_payload_keys
from app.utils.utils import handle_duplicate_error, validate_payload_keys
from app.v1.endpoints.functions import set_role
from asyncpg.exceptions import InsufficientPrivilegeError
from asyncpg.exceptions import InsufficientPrivilegeError, UniqueViolationError
from fastapi import APIRouter, Body, Depends, Header, status
from fastapi.responses import JSONResponse, Response

Expand Down Expand Up @@ -107,6 +107,8 @@ async def update_thing(
await connection.execute("RESET ROLE;")

return Response(status_code=status.HTTP_200_OK)
except UniqueViolationError as e:
return handle_duplicate_error(e)
except InsufficientPrivilegeError:
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand Down