Merge remote-tracking branch 'origin/main' into feature/use_tasks_as_logs

This commit is contained in:
jasquat 2023-03-17 12:40:26 -04:00
commit 3461056beb
10 changed files with 165 additions and 52 deletions

View File

@ -156,7 +156,7 @@ jobs:
- name: Upload coverage data
# pin to upload coverage from only one matrix entry, otherwise coverage gets confused later
if: always() && matrix.session == 'tests' && matrix.python == '3.11' && matrix.os == 'ubuntu-latest' && matrix.database == 'mysql'
uses: "actions/upload-artifact@v3.0.0"
uses: "actions/upload-artifact@v3"
# this action doesn't seem to respect working-directory so include working-directory value in path
with:
name: coverage-data
@ -164,14 +164,14 @@ jobs:
- name: Upload documentation
if: matrix.session == 'docs-build'
uses: actions/upload-artifact@v3.0.0
uses: actions/upload-artifact@v3
with:
name: docs
path: docs/_build
- name: Upload logs
if: failure() && matrix.session == 'tests'
uses: "actions/upload-artifact@v3.0.0"
uses: "actions/upload-artifact@v3"
with:
name: logs-${{matrix.python}}-${{matrix.os}}-${{matrix.database}}
path: "./log/*.log"

View File

@ -108,21 +108,21 @@ jobs:
run: ./bin/get_logs_from_docker_compose >./log/docker_compose.log
- name: Upload logs
if: failure()
uses: "actions/upload-artifact@v3.0.0"
uses: "actions/upload-artifact@v3"
with:
name: spiffworkflow-backend-logs
path: "./spiffworkflow-backend/log/*.log"
# https://github.com/cypress-io/github-action#artifacts
- name: upload_screenshots
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: failure()
with:
name: cypress-screenshots
path: ./spiffworkflow-frontend/cypress/screenshots
# Test run video was always captured, so this action uses "always()" condition
- name: upload_videos
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: failure()
with:
name: cypress-videos

View File

@ -396,7 +396,7 @@
"otpPolicyLookAheadWindow" : 1,
"otpPolicyPeriod" : 30,
"otpPolicyCodeReusable" : false,
"otpSupportedApplications" : [ "totpAppGoogleName", "totpAppFreeOTPName" ],
"otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppGoogleName" ],
"webAuthnPolicyRpEntityName" : "keycloak",
"webAuthnPolicySignatureAlgorithms" : [ "ES256" ],
"webAuthnPolicyRpId" : "",
@ -991,6 +991,29 @@
"realmRoles" : [ "default-roles-spiffworkflow" ],
"notBefore" : 0,
"groups" : [ ]
}, {
"id" : "7b86b997-de98-478c-8550-cfca65e40c33",
"createdTimestamp" : 1679060366901,
"username" : "core18.contributor",
"enabled" : true,
"totp" : false,
"emailVerified" : false,
"email" : "core18.contributor@status.im",
"attributes" : {
"spiffworkflow-employeeid" : [ "233" ]
},
"credentials" : [ {
"id" : "55ca2bd7-6f60-4f04-be21-df6300ca9442",
"type" : "password",
"createdDate" : 1679060366954,
"secretData" : "{\"value\":\"hC/O8LJ8/y/nXLmRFgRazOX9PXMHkowYH1iHUB4Iw9jzc8IMMv8dFrxu7XBklfyz7CPc1bmgl0k29jygRZYHlg==\",\"salt\":\"4R17tmLrHWyFAMvrfLMETQ==\",\"additionalParameters\":{}}",
"credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
} ],
"disableableCredentialTypes" : [ ],
"requiredActions" : [ ],
"realmRoles" : [ "default-roles-spiffworkflow" ],
"notBefore" : 0,
"groups" : [ ]
}, {
"id" : "3b81b45e-759b-4d7a-aa90-adf7b447208c",
"createdTimestamp" : 1676302140358,
@ -1505,6 +1528,29 @@
"realmRoles" : [ "default-roles-spiffworkflow" ],
"notBefore" : 0,
"groups" : [ ]
}, {
"id" : "d123d384-66a4-4db5-9dbb-d73c12047001",
"createdTimestamp" : 1678997616280,
"username" : "finance.project-lead",
"enabled" : true,
"totp" : false,
"emailVerified" : false,
"email" : "finance.project-lead@status.im",
"attributes" : {
"spiffworkflow-employeeid" : [ "128" ]
},
"credentials" : [ {
"id" : "b680f5c5-c2de-4255-9d23-7e18cff3ac4e",
"type" : "password",
"createdDate" : 1678997616336,
"secretData" : "{\"value\":\"4kasmb11Sv62rInh8eFUhS3rGYNymzsvxzfsEIWGYhnlisYuo1iTS2opv/kET/NyJlsYrfwc7yrIqSHvkUHkkA==\",\"salt\":\"q/ees3a4K+3K11olnfPzCQ==\",\"additionalParameters\":{}}",
"credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
} ],
"disableableCredentialTypes" : [ ],
"requiredActions" : [ ],
"realmRoles" : [ "default-roles-spiffworkflow" ],
"notBefore" : 0,
"groups" : [ ]
}, {
"id" : "f6d2488a-446c-493b-bbe8-210ede6f3e42",
"createdTimestamp" : 1674148694899,
@ -4578,7 +4624,7 @@
"subType" : "authenticated",
"subComponents" : { },
"config" : {
"allowed-protocol-mapper-types" : [ "oidc-usermodel-property-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-address-mapper" ]
"allowed-protocol-mapper-types" : [ "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "oidc-address-mapper", "saml-user-attribute-mapper", "oidc-full-name-mapper", "oidc-usermodel-property-mapper", "saml-role-list-mapper", "saml-user-property-mapper" ]
}
}, {
"id" : "d68e938d-dde6-47d9-bdc8-8e8523eb08cd",
@ -4596,7 +4642,7 @@
"subType" : "anonymous",
"subComponents" : { },
"config" : {
"allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-usermodel-attribute-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "saml-user-property-mapper" ]
"allowed-protocol-mapper-types" : [ "saml-user-property-mapper", "saml-user-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "oidc-usermodel-attribute-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper" ]
}
}, {
"id" : "3854361d-3fe5-47fb-9417-a99592e3dc5c",
@ -4686,7 +4732,7 @@
"internationalizationEnabled" : false,
"supportedLocales" : [ ],
"authenticationFlows" : [ {
"id" : "04b09640-f53c-4c1b-b2b1-8cac25afc2bb",
"id" : "38a6b336-b026-46be-a8be-e8ff7b9da407",
"alias" : "Account verification options",
"description" : "Method with which to verity the existing account",
"providerId" : "basic-flow",
@ -4708,7 +4754,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "e7c246f4-71c3-4a48-9037-72438bdcfcbb",
"id" : "eb9fe753-cd35-4e65-bb34-e83ba7059566",
"alias" : "Authentication Options",
"description" : "Authentication options.",
"providerId" : "basic-flow",
@ -4737,7 +4783,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "6e9d415e-98f7-4459-b10b-45b08302c681",
"id" : "aa9c74f7-0426-4440-907f-4aa0f999eb1e",
"alias" : "Browser - Conditional OTP",
"description" : "Flow to determine if the OTP is required for the authentication",
"providerId" : "basic-flow",
@ -4759,7 +4805,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "c86b0fad-f7dd-4c58-974e-25eb83c1dacf",
"id" : "eb2a0849-c316-46bc-8b06-fd0cc50e3f32",
"alias" : "Direct Grant - Conditional OTP",
"description" : "Flow to determine if the OTP is required for the authentication",
"providerId" : "basic-flow",
@ -4781,7 +4827,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "cb7f4c87-a8fa-445a-a8d4-53869cdfed12",
"id" : "8f064003-823b-4be1-aa66-7324bf38c741",
"alias" : "First broker login - Conditional OTP",
"description" : "Flow to determine if the OTP is required for the authentication",
"providerId" : "basic-flow",
@ -4803,7 +4849,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "8fa87954-bc65-4f1e-bc55-f5bb49f59fbb",
"id" : "eef22678-b09c-4ca8-bdcf-90ea44ff0120",
"alias" : "Handle Existing Account",
"description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider",
"providerId" : "basic-flow",
@ -4825,7 +4871,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "e617d826-c654-4c35-96ad-8381bd1e2298",
"id" : "4367f263-ef2c-426e-b5cd-49fff868ea1a",
"alias" : "Reset - Conditional OTP",
"description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
"providerId" : "basic-flow",
@ -4847,7 +4893,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "2e4a46ae-2813-4b71-9386-c08b2f063fa6",
"id" : "b2e9c608-1779-4c03-b32a-03c77450abae",
"alias" : "User creation or linking",
"description" : "Flow for the existing/non-existing user alternatives",
"providerId" : "basic-flow",
@ -4870,7 +4916,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "8fa69de0-13cf-4252-899b-c59a30ebd132",
"id" : "a8c79324-1881-4bb0-a8a2-83dfd54cacd1",
"alias" : "Verify Existing Account by Re-authentication",
"description" : "Reauthentication of existing account",
"providerId" : "basic-flow",
@ -4892,7 +4938,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "204d20f6-d9a7-49ff-a7a3-45386fb884f4",
"id" : "d1aa83c6-da36-4cb6-b6ed-f6ec556df614",
"alias" : "browser",
"description" : "browser based authentication",
"providerId" : "basic-flow",
@ -4928,7 +4974,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "3c0c2987-65db-4920-ae44-34aba220c3fb",
"id" : "2afecfef-4bfb-4842-b338-7ed032a618d2",
"alias" : "clients",
"description" : "Base authentication for clients",
"providerId" : "client-flow",
@ -4964,7 +5010,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "68a92113-be75-4e63-a322-8076d6c67650",
"id" : "34dc1854-4969-4065-90e6-fef38b0dea98",
"alias" : "direct grant",
"description" : "OpenID Connect Resource Owner Grant",
"providerId" : "basic-flow",
@ -4993,7 +5039,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "a630d78f-4fe1-4350-a19d-d091d1af514d",
"id" : "40557323-dbbc-48ee-9ed1-748b11c9628d",
"alias" : "docker auth",
"description" : "Used by Docker clients to authenticate against the IDP",
"providerId" : "basic-flow",
@ -5008,7 +5054,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "f73b4437-8e82-4788-be69-e437b09b500c",
"id" : "d18b5c50-39fa-4b11-a7d2-0e6768e275c1",
"alias" : "first broker login",
"description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"providerId" : "basic-flow",
@ -5031,7 +5077,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "b7c8cc6d-bc1f-446e-b263-72214b2f5c56",
"id" : "976be80d-a88b-412c-8ad2-9ebe427793d4",
"alias" : "forms",
"description" : "Username, password, otp and other auth forms.",
"providerId" : "basic-flow",
@ -5053,7 +5099,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "a3bdf79f-8c7d-4bff-807d-76fa61093446",
"id" : "83b3a411-ff7c-4cba-845a-9554c536d6b1",
"alias" : "http challenge",
"description" : "An authentication flow based on challenge-response HTTP Authentication Schemes",
"providerId" : "basic-flow",
@ -5075,7 +5121,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "ada41b4e-5a12-496d-aa1e-d31cf8c08226",
"id" : "1cb835a6-b38c-4f29-a6d8-d04d0a84d05e",
"alias" : "registration",
"description" : "registration flow",
"providerId" : "basic-flow",
@ -5091,7 +5137,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "1c858bcd-2031-4056-bbf0-1fbaecdd7068",
"id" : "7ec06c82-6802-4ff4-a3ab-9b6a0b8dbc4b",
"alias" : "registration form",
"description" : "registration form",
"providerId" : "form-flow",
@ -5127,7 +5173,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "ff91e251-d85e-450b-bff7-d45be26777d5",
"id" : "f3bc2f7b-2074-4d93-9578-3abf648a6681",
"alias" : "reset credentials",
"description" : "Reset credentials for a user if they forgot their password or something",
"providerId" : "basic-flow",
@ -5163,7 +5209,7 @@
"userSetupAllowed" : false
} ]
}, {
"id" : "7b0680a2-99b9-454c-b145-f286e9d60c58",
"id" : "e62e031b-9922-4682-b867-bc5c3a4a7e99",
"alias" : "saml ecp",
"description" : "SAML ECP Profile Authentication Flow",
"providerId" : "basic-flow",
@ -5179,13 +5225,13 @@
} ]
} ],
"authenticatorConfig" : [ {
"id" : "aa1e4f55-3e7f-445a-a432-7a972776d719",
"id" : "c449f0aa-5f3c-4107-9f04-3222fa93a486",
"alias" : "create unique user config",
"config" : {
"require.password.update.after.registration" : "false"
}
}, {
"id" : "fd69765e-309b-4c5d-bdd5-51343427cd27",
"id" : "f7a6ed54-0ab8-4f29-9877-960bd65bf394",
"alias" : "review profile config",
"config" : {
"update.profile.on.first.login" : "missing"

View File

@ -21,6 +21,7 @@ core14.contributor@status.im,229
core15.contributor@status.im,230
core16.contributor@status.im,231
core17.contributor@status.im,232
core18.contributor@status.im,233
core2.contributor@status.im,156
core3.contributor@status.im,157
core4.contributor@status.im,158
@ -42,6 +43,7 @@ desktop3.sme@status.im,196
desktop4.sme@status.im,197
desktop5.sme@status.im,198
fin@status.im,118
finance.project-lead@status.im,128
finance_user1@status.im
fluffy.project-lead@status.im,162
harmeet@status.im,109

View File

@ -129,5 +129,13 @@ SPIFFWORKFLOW_BACKEND_ENGINE_STEP_DEFAULT_STRATEGY_WEB = environ.get(
"SPIFFWORKFLOW_BACKEND_ENGINE_STEP_DEFAULT_STRATEGY_WEB", default="greedy"
)
SPIFFWORKFLOW_BACKEND_USER_INPUT_REQUIRED_LOCK_RETRY_TIMES = int(
environ.get("SPIFFWORKFLOW_BACKEND_USER_INPUT_REQUIRED_LOCK_RETRY_TIMES", default="3")
)
SPIFFWORKFLOW_BACKEND_USER_INPUT_REQUIRED_LOCK_RETRY_INTERVAL_IN_SECONDS = int(
environ.get("SPIFFWORKFLOW_BACKEND_USER_INPUT_REQUIRED_LOCK_RETRY_INTERVAL_IN_SECONDS", default="1")
)
# this is only used in CI. use SPIFFWORKFLOW_BACKEND_DATABASE_URI instead for real configuration
SPIFFWORKFLOW_BACKEND_DATABASE_PASSWORD = environ.get("SPIFFWORKFLOW_BACKEND_DATABASE_PASSWORD", default=None)

View File

@ -173,7 +173,17 @@ def process_instance_terminate(
"""Process_instance_run."""
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
processor = ProcessInstanceProcessor(process_instance)
processor.terminate()
try:
processor.lock_process_instance("Web")
processor.terminate()
except (ProcessInstanceIsNotEnqueuedError, ProcessInstanceIsAlreadyLockedError) as e:
ErrorHandlingService().handle_error(processor, e)
raise e
finally:
if ProcessInstanceLockService.has_lock(process_instance.id):
processor.unlock_process_instance("Web")
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
@ -183,7 +193,18 @@ def process_instance_suspend(
) -> flask.wrappers.Response:
"""Process_instance_suspend."""
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
ProcessInstanceProcessor.suspend(process_instance)
processor = ProcessInstanceProcessor(process_instance)
try:
processor.lock_process_instance("Web")
processor.suspend()
except (ProcessInstanceIsNotEnqueuedError, ProcessInstanceIsAlreadyLockedError) as e:
ErrorHandlingService().handle_error(processor, e)
raise e
finally:
if ProcessInstanceLockService.has_lock(process_instance.id):
processor.unlock_process_instance("Web")
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")
@ -193,7 +214,18 @@ def process_instance_resume(
) -> flask.wrappers.Response:
"""Process_instance_resume."""
process_instance = _find_process_instance_by_id_or_raise(process_instance_id)
ProcessInstanceProcessor.resume(process_instance)
processor = ProcessInstanceProcessor(process_instance)
try:
processor.lock_process_instance("Web")
processor.resume()
except (ProcessInstanceIsNotEnqueuedError, ProcessInstanceIsAlreadyLockedError) as e:
ErrorHandlingService().handle_error(processor, e)
raise e
finally:
if ProcessInstanceLockService.has_lock(process_instance.id):
processor.unlock_process_instance("Web")
return Response(json.dumps({"ok": True}), status=200, mimetype="application/json")

View File

@ -378,8 +378,13 @@ def task_submit_shared(
only_tasks_that_can_be_completed=True,
)
retry_times = current_app.config["SPIFFWORKFLOW_BACKEND_USER_INPUT_REQUIRED_LOCK_RETRY_TIMES"]
retry_interval_in_seconds = current_app.config[
"SPIFFWORKFLOW_BACKEND_USER_INPUT_REQUIRED_LOCK_RETRY_INTERVAL_IN_SECONDS"
]
with sentry_sdk.start_span(op="task", description="complete_form_task"):
processor.lock_process_instance("Web")
processor.lock_process_instance("Web", retry_times, retry_interval_in_seconds)
ProcessInstanceService.complete_form_task(
processor=processor,
spiff_task=spiff_task,

View File

@ -91,9 +91,8 @@ from spiffworkflow_backend.services.file_system_service import FileSystemService
from spiffworkflow_backend.services.process_instance_lock_service import (
ProcessInstanceLockService,
)
from spiffworkflow_backend.services.process_instance_queue_service import (
ProcessInstanceQueueService,
)
from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceIsAlreadyLockedError
from spiffworkflow_backend.services.process_instance_queue_service import ProcessInstanceQueueService
from spiffworkflow_backend.services.process_model_service import ProcessModelService
from spiffworkflow_backend.services.service_task_service import ServiceTaskDelegate
from spiffworkflow_backend.services.spec_file_service import SpecFileService
@ -1276,7 +1275,7 @@ class ProcessInstanceProcessor:
self.add_step()
self.save()
# Saving the workflow seems to reset the status
self.suspend(self.process_instance_model)
self.suspend()
def reset_process(self, spiff_step: int) -> None:
"""Reset a process to an earlier state."""
@ -1319,7 +1318,7 @@ class ProcessInstanceProcessor:
db.session.delete(row)
self.save()
self.suspend(self.process_instance_model)
self.suspend()
@staticmethod
def get_parser() -> MyCustomParser:
@ -1485,8 +1484,23 @@ class ProcessInstanceProcessor:
return the_status
# TODO: replace with implicit/more granular locking in workflow execution service
def lock_process_instance(self, lock_prefix: str) -> None:
ProcessInstanceQueueService.dequeue(self.process_instance_model)
# TODO: remove the retry logic once all user_input_required's don't need to be locked to check timers
def lock_process_instance(
self, lock_prefix: str, retry_count: int = 0, retry_interval_in_seconds: int = 0
) -> None:
try:
ProcessInstanceQueueService.dequeue(self.process_instance_model)
except ProcessInstanceIsAlreadyLockedError as e:
if retry_count > 0:
current_app.logger.info(
f"process_instance_id {self.process_instance_model.id} is locked. "
f"will retry {retry_count} times with delay of {retry_interval_in_seconds}."
)
if retry_interval_in_seconds > 0:
time.sleep(retry_interval_in_seconds)
self.lock_process_instance(lock_prefix, retry_count - 1, retry_interval_in_seconds)
else:
raise e
# TODO: replace with implicit/more granular locking in workflow execution service
def unlock_process_instance(self, lock_prefix: str) -> None:
@ -1923,16 +1937,14 @@ class ProcessInstanceProcessor:
db.session.add(self.process_instance_model)
db.session.commit()
@classmethod
def suspend(cls, process_instance: ProcessInstanceModel) -> None:
def suspend(self) -> None:
"""Suspend."""
process_instance.status = ProcessInstanceStatus.suspended.value
db.session.add(process_instance)
self.process_instance_model.status = ProcessInstanceStatus.suspended.value
db.session.add(self.process_instance_model)
db.session.commit()
@classmethod
def resume(cls, process_instance: ProcessInstanceModel) -> None:
def resume(self) -> None:
"""Resume."""
process_instance.status = ProcessInstanceStatus.waiting.value
db.session.add(process_instance)
self.process_instance_model.status = ProcessInstanceStatus.waiting.value
db.session.add(self.process_instance_model)
db.session.commit()

View File

@ -25,6 +25,7 @@ from spiffworkflow_backend.models.process_instance_file_data import (
from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.task import Task
from spiffworkflow_backend.models.user import UserModel
from spiffworkflow_backend.services.assertion_service import safe_assertion
from spiffworkflow_backend.services.authorization_service import AuthorizationService
from spiffworkflow_backend.services.git_service import GitCommandError
from spiffworkflow_backend.services.git_service import GitService
@ -95,6 +96,13 @@ class ProcessInstanceService:
)
process_instance_lock_prefix = "Background"
for process_instance in records:
with safe_assertion(process_instance.status == status_value) as false_assumption:
if false_assumption:
raise AssertionError(
f"Queue assumed process instance {process_instance.id} has status of {status_value} "
f"when it really is {process_instance.status}"
)
locked = False
processor = None
try:

View File

@ -1419,7 +1419,7 @@ class TestProcessApi(BaseTest):
)
processor.save()
processor.suspend(process_instance)
processor.suspend()
payload["description"] = "Message To Suspended"
response = client.post(
f"/v1.0/messages/{message_model_identifier}",
@ -1431,7 +1431,7 @@ class TestProcessApi(BaseTest):
assert response.json
assert response.json["error_code"] == "message_not_accepted"
processor.resume(process_instance)
processor.resume()
payload["description"] = "Message To Resumed"
response = client.post(
f"/v1.0/messages/{message_model_identifier}",