Skip to content
Merged
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "register-your-data-api"
version = "0.3.1"
version = "0.3.2"
requires-python = ">= 3.12.11"
readme = "README.md"
authors = [{name="IATI Secretariat", email="support@iatistandard.org"}]
Expand Down
3 changes: 2 additions & 1 deletion src/register_your_data_api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ def get_suitecrm_audit_headers(
except ValueError as e:
trace_id = uuid.uuid4()
raise RYDUserException(
user,
user.user_id_crm,
user.client_id,
400,
f"trace_id: {trace_id} - details: unknown client ID",
f"trace_id: {trace_id} - details: {e.args[0]}",
Expand Down
13 changes: 7 additions & 6 deletions src/register_your_data_api/exception_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@
from fastapi.exceptions import RequestValidationError
from starlette.requests import Request

from .auth.models import UserAndCredentials
from .exceptions import RYDUserException # noqa: F401
from .util import Context # noqa: F401


def format_log_msg(
request: Request, user: UserAndCredentials, msg: str | None, include_client_id: bool = False
request: Request, user_id: str | None, client_id: str | None, msg: str | None, include_client_id: bool = False
) -> str | None:
if msg is None:
return None
client_prefix: str = f"client id: {user.client_id} - " if include_client_id else ""
return client_prefix + f"user id: {user.user_id_crm} - " f"{request.method} {request.url.path} - {msg}"

client_prefix: str = f"client id: {client_id} - " if include_client_id else ""

return client_prefix + f"user id: {user_id} - {request.method} {request.url.path} - {msg}"


async def ryd_user_exception_handler(request: Request, exc: RYDUserException) -> fastapi.responses.JSONResponse:
Expand All @@ -36,9 +37,9 @@ async def ryd_user_exception_handler(request: Request, exc: RYDUserException) ->

context: Context = request.app.state.context

app_log_msg = format_log_msg(request, exc.user, exc.app_msg)
app_log_msg = format_log_msg(request, exc.user_id, exc.client_id, exc.app_msg)

audit_log_msg = format_log_msg(request, exc.user, exc.audit_msg, include_client_id=True)
audit_log_msg = format_log_msg(request, exc.user_id, exc.client_id, exc.audit_msg, include_client_id=True)

if app_log_msg is not None:
context.app_logger.error(app_log_msg)
Expand Down
9 changes: 4 additions & 5 deletions src/register_your_data_api/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
from .auth.models import UserAndCredentials


class RYDException(Exception):
"""Base class for all app errors."""

Expand All @@ -12,13 +9,15 @@ class RYDUserException(RYDException):

def __init__(
self,
user: UserAndCredentials,
user_id: str | None,
client_id: str | None,
status_code: int,
app_msg: str | None,
audit_msg: str | None,
public_msg: str | None,
):
self.user: UserAndCredentials = user
self.user_id: str | None = user_id
self.client_id: str | None = client_id
self.status_code: int = status_code
self.app_msg: str | None = app_msg
self.audit_msg: str | None = audit_msg
Expand Down
49 changes: 26 additions & 23 deletions src/register_your_data_api/routers/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ def create_dataset(
# Check the user has permission to create datasets for the specified
# reporting org
assert_precondition_met(
context,
user,
user.user_id_crm,
user.client_id,
condition_func=lambda: user.validator.user_can_create_reporting_org_datasets(
uuid.UUID(dataset.owner_organisation_id)
),
Expand All @@ -69,8 +69,8 @@ def create_dataset(
# the case for superadmins, so permission to create datasets for a reporting
# org does not imply that the reporting org exists, so we check that here
assert_precondition_met(
context,
user,
user.user_id_crm,
user.client_id,
condition_func=lambda: check_crm_record_exists(crm, "Accounts", str(dataset.owner_organisation_id)),
status_code=fastapi.status.HTTP_404_NOT_FOUND,
audit_log_msg=(
Expand All @@ -82,8 +82,8 @@ def create_dataset(

# Check that the short name is unique
assert_precondition_met(
context,
user,
user.user_id_crm,
user.client_id,
condition_func=lambda: get_num_crm_records(crm, "IATI_Datasets", {"iati_short_name": dataset.short_name}) == 0,
status_code=fastapi.status.HTTP_409_CONFLICT,
audit_log_msg=(
Expand All @@ -104,7 +104,8 @@ def create_dataset(
context.audit_logger.info(
format_log_msg(
request,
user,
user.user_id_crm,
user.client_id,
f"trace id: {trace_id} - Created dataset with id: {suitecrm_dataset['id']}",
include_client_id=True,
)
Expand Down Expand Up @@ -137,8 +138,8 @@ def get_dataset_detail(

# Check that the dataset exists and is unique
assert_precondition_met(
context,
user,
user.user_id_crm,
user.client_id,
condition_func=lambda: len(datasets) == 1,
status_code=fastapi.status.HTTP_404_NOT_FOUND,
audit_log_msg=(
Expand Down Expand Up @@ -194,8 +195,8 @@ def update_dataset(
)

assert_precondition_met(
context,
user,
user.user_id_crm,
user.client_id,
condition_func=lambda: len(original_dataset_record_from_suitecrm["data"]) != 0,
status_code=fastapi.status.HTTP_404_NOT_FOUND,
audit_log_msg=(
Expand All @@ -210,8 +211,8 @@ def update_dataset(
owning_reporting_org = original_dataset_record_from_suitecrm["data"][0]["attributes"]["iati_dataset_owner_org_id"]

assert_precondition_met(
context,
user,
user.user_id_crm,
user.client_id,
condition_func=lambda: user.validator.user_can_update_reporting_org_datasets(uuid.UUID(owning_reporting_org)),
status_code=fastapi.status.HTTP_403_FORBIDDEN,
audit_log_msg=(
Expand All @@ -228,8 +229,8 @@ def update_dataset(
dataset_for_suitecrm = get_suitecrm_dict_from_dataset(dataset)

assert_precondition_met(
context,
user,
user.user_id_crm,
user.client_id,
condition_func=lambda: (
user.validator.user_can_update_reporting_org_dataset_visibility(owning_reporting_org)
or dataset.visibility is None
Expand All @@ -251,8 +252,8 @@ def update_dataset(

# Check that the short name is unique
assert_precondition_met(
context,
user,
user.user_id_crm,
user.client_id,
condition_func=lambda: (
original_dataset_record_from_suitecrm["data"][0]["attributes"]["iati_short_name"] == dataset.short_name
or (
Expand Down Expand Up @@ -280,7 +281,8 @@ def update_dataset(
context.audit_logger.info(
format_log_msg(
request,
user,
user.user_id_crm,
user.client_id,
f"trace id: {trace_id} - Created dataset with id: {suitecrm_dataset['id']}",
include_client_id=True,
)
Expand Down Expand Up @@ -317,8 +319,8 @@ def delete_dataset(
)

assert_precondition_met(
context,
user,
user.user_id_crm,
user.client_id,
condition_func=lambda: len(original_dataset_record_from_suitecrm["data"]) == 1,
status_code=fastapi.status.HTTP_404_NOT_FOUND,
public_msg=f"There is no dataset with ID {str(dataset_id)} in the Registry.",
Expand All @@ -327,8 +329,8 @@ def delete_dataset(
owning_reporting_org = original_dataset_record_from_suitecrm["data"][0]["attributes"]["iati_dataset_owner_org_id"]

assert_precondition_met(
context,
user,
user.user_id_crm,
user.client_id,
condition_func=lambda: user.validator.user_can_delete_reporting_org_datasets(uuid.UUID(owning_reporting_org)),
status_code=fastapi.status.HTTP_403_FORBIDDEN,
public_msg=(
Expand All @@ -347,7 +349,8 @@ def delete_dataset(
context.audit_logger.info(
format_log_msg(
request,
user,
user.user_id_crm,
user.client_id,
f"trace id: {trace_id} - Deleted dataset with id: {str(dataset_id)}",
include_client_id=True,
)
Expand Down
54 changes: 34 additions & 20 deletions src/register_your_data_api/routers/reporting_orgs.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ def get_reporting_org_detail(
context: Context = request.app.state.context

assert_precondition_met(
context,
user,
user.user_id_crm,
user.client_id,
condition_func=lambda: user.validator.user_can_read_reporting_org(org_id),
status_code=fastapi.status.HTTP_403_FORBIDDEN,
audit_log_msg=(f"Request to get reporting org details for org id: {org_id} by unauthorised user"),
Expand All @@ -102,8 +102,8 @@ def get_reporting_org_detail(
# a user could have a role for an org that doesn't exist or isn't discoverable, so we handle that here. We don't
# run this check first to avoid making unnecessary CRM calls in the majority of cases.
assert_precondition_met(
context,
user,
user.user_id_crm,
user.client_id,
condition_func=lambda: len(crm_reporting_orgs["data"]) > 0,
status_code=fastapi.status.HTTP_404_NOT_FOUND,
audit_log_msg=(f"Request to get reporting org details failed for org id: {org_id} as it doesn't exist"),
Expand Down Expand Up @@ -138,8 +138,8 @@ def create_reporting_org(
trace_id: uuid.UUID = uuid.uuid4()

assert_precondition_met(
context,
user,
user.user_id_crm,
user.client_id,
condition_func=lambda: user.validator.user_can_create_reporting_org(),
status_code=fastapi.status.HTTP_403_FORBIDDEN,
audit_log_msg=(f"Request to create reporting org by unauthorised user id: {user.user_id_crm}"),
Expand All @@ -155,8 +155,8 @@ def create_reporting_org(

# Check that the short name is unique
assert_precondition_met(
context,
user,
user.user_id_crm,
user.client_id,
condition_func=lambda: get_num_crm_records(
crm, "Accounts", {"iati_short_name": reporting_org.short_name, "iati_registry_discoverable": 1}
)
Expand Down Expand Up @@ -225,7 +225,8 @@ def create_reporting_org(
user_email = crm_user["data"][0]["attributes"]["email1"]
else:
raise RYDUserException(
user=user,
user_id=user.user_id_crm,
client_id=user.client_id,
status_code=400,
app_msg=(
f"trace id: {trace_id} - User account details for user id: {user.user_id_crm} "
Expand Down Expand Up @@ -260,7 +261,8 @@ def create_reporting_org(
context.audit_logger.info(
format_log_msg(
request,
user,
user.user_id_crm,
user.client_id,
f"trace id: {trace_id} - Created reporting org with id: {suitecrm_reporting_org['id']}",
include_client_id=True,
)
Expand Down Expand Up @@ -348,7 +350,13 @@ def update_reporting_org(
)

context.audit_logger.info(
format_log_msg(request, user, f"Updated reporting org with id: {str(org_id)}", include_client_id=True)
format_log_msg(
request,
user.user_id_crm,
user.client_id,
f"Updated reporting org with id: {str(org_id)}",
include_client_id=True,
)
)

return UserReportingOrgRelationSingleResponse(status="success", error=None, data=user_reporting_org_relation)
Expand All @@ -366,8 +374,8 @@ def delete_reporting_org(

# 1. Check that the user has permissions to delete (mark as not on RYD) the reporting org
assert_precondition_met(
context,
user,
user.user_id_crm,
user.client_id,
condition_func=lambda: user.validator.user_can_delete_reporting_org(org_id),
status_code=fastapi.status.HTTP_403_FORBIDDEN,
audit_log_msg=(
Expand Down Expand Up @@ -395,8 +403,8 @@ def delete_reporting_org(
)

assert_precondition_met(
context,
user,
user.user_id_crm,
user.client_id,
condition_func=lambda: len(crm_reporting_org["data"]) == 1,
status_code=fastapi.status.HTTP_404_NOT_FOUND,
public_msg=f"There is no organisation with ID {str(org_id)} in the Registry.",
Expand Down Expand Up @@ -447,7 +455,13 @@ def delete_reporting_org(
)

context.audit_logger.info(
format_log_msg(request, user, f"Deleted reporting org with id: {str(org_id)}", include_client_id=True)
format_log_msg(
request,
user.user_id_crm,
user.client_id,
f"Deleted reporting org with id: {str(org_id)}",
include_client_id=True,
)
)

return fastapi.responses.JSONResponse({"status": "success", "data": None, "error": None})
Expand Down Expand Up @@ -545,8 +559,8 @@ def get_reporting_org_datasets(

# 1. Check that the user has permissions to read datasets for the reporting org
assert_precondition_met(
context,
user,
user.user_id_crm,
user.client_id,
condition_func=lambda: user.validator.user_can_read_reporting_org_datasets(org_id),
status_code=fastapi.status.HTTP_403_FORBIDDEN,
audit_log_msg=(
Expand All @@ -563,8 +577,8 @@ def get_reporting_org_datasets(

# 2. Check that the Reporting Org exists in the CRM
assert_precondition_met(
context,
user,
user.user_id_crm,
user.client_id,
condition_func=lambda: check_crm_record_exists(crm, "Accounts", str(org_id)),
status_code=fastapi.status.HTTP_404_NOT_FOUND,
public_msg=f"There is no organisation with ID {str(org_id)} in the Registry.",
Expand Down
Loading
Loading