TMGMT LATW Cloud connects the Translation Management Tool (TMGMT) to LATW Cloud, a paid AI translation service. It translates content entities (with embedded entities such as Paragraphs), configuration (with tmgmt_config) and interface strings.
The module is a connector. The AI model, prompt, glossary, budgets and target languages are set in the LATW Cloud panel, and translations are ordered there. LATW Cloud then calls a small signed API on the site to read the source of the items chosen for translation and to write the translations back:
- Content and configuration translations are saved through TMGMT jobs (one job per item and language, accepted automatically). Content Moderation states are respected.
- Interface strings are saved as customized locale translations, so
.poimports do not overwrite them. - Aborting or deleting one of these jobs in TMGMT cancels the item in LATW Cloud.
How it differs from similar projects
TMGMT DeepL Integration and AI Translation Management are TMGMT translators: a job is requested in the TMGMT checkout and sent to a machine translation API with the settings of the translator. With this module, translations are configured and ordered in the LATW Cloud panel. Prompt, glossary, context and budgets are kept in one place, and the whole site can be translated in bulk. TMGMT is used to save the translations, and Drupal stays the source of truth for content and translation status.
Notes for reviewers
- Nothing is sent to LATW Cloud before an administrator clicks "Connect with LATW" at
/admin/tmgmt/latw(permission administer tmgmt; the admin routes are POST-only and CSRF-protected). Pairing uses PKCE. - LATW Cloud calls
POST /latw/v1/cloud/{endpoint}. A custom access check (SignatureAccessCheck) lets in only requests signed with HMAC-SHA256 with the site's secret, with a timestamp window and a single-use nonce. - The secret is not stored in configuration. It is kept in the State API, encrypted with AES-256-GCM using a key derived from the site's hash salt.
- Outgoing requests use Drupal's
http_clientwith timeouts and redirects disabled. - GitLab CI is enabled and green on 1.0.x: pipelines.
Comments
Comment #2
vinodkannan18 commentedReviewed 1.0.x at 8111510. I read the signing and crypto path and the endpoint surface, ran phpcs (Drupal, DrupalPractice), checked licensing and third-party assets, and checked the Drupal 10/11 compatibility claims against the code.
What is already right
Worth stating specifically, because this is a better-prepared application than most:
hash_equals()for the comparison,random_bytes()for nonces, PKCE S256 for pairing.setWithExpireIfNotExists()is genuinely atomic, so two concurrent requests carrying one nonce cannot both win.NONCE_TTLof 600 against aWINDOWof plus/minus 300 is the correct relationship: the nonce outlives every moment its signature is still valid.#[LegacyHook]wrappers,#[LegacyRequirementsHook]with aclass_exists(RequirementSeverity::class)guard so 11.2 does not double-report, and an explicit service registration for the hook class because Drupal 10 does not register them itself.LICENSE.txtcommitted, GPL-2.0-or-later in composer.json, no bundled libraries, stock gitlab_templates CI,config/schemapresent, and phpcs clean across all PHP.1. Raw exception messages are returned on a path reachable without authentication
In
src/EventSubscriber/CloudApiExceptionSubscriber.php:CloudException::toBody()emits'message' => $this->getMessage(), so that string reaches the caller in the JSON body. Thedefaultarm of theHttpExceptionInterfacematch just above does the same.The subscriber decides whether to handle a request on the path prefix alone, before and independently of
SignatureAccessCheck, so the caller at that point has not necessarily authenticated.Connection::get()necessarily runs ahead of any signature check, because it has to in order to find the key, and it touches State and thenSettings::getHashSalt(). ADatabaseExceptionWrapperthere carries the failing SQL and the table prefix in its message; a missing hash salt raises aRuntimeExceptionnaming settings.php.The detail is already logged on the line above, including file and line, so returning it as well adds disclosure without adding diagnostic value. A fixed string for
internal_error, and for thedefaultarm, would close it.This is the only thing I would hold the application on.
2.
$is_syncingis accepted and then ignoredtmgmt_latw_uninstall(bool $is_syncing)makes an unconditional outbound HTTP call to LATW Cloud. The parameter exists so a module can skip side effects when the uninstall is replaying configuration rather than expressing an intent, and here the side effect is a network call to a third party, which also means an uninstall during a config import blocks on an external timeout.Not necessarily wrong, since you may well want the panel told either way. But it reads as an oversight rather than a decision, so: was it one? If the call is wanted during sync, a short comment saying so would settle it for the next reader.
3. Worth one sentence in the README
Scope is bundles with Content Translation enabled, which is a real and defensible boundary. Within it,
ContentSource::countByType()filters on the default-translation column and not on published status, so unpublished and in-moderation sources are listed and translatable. That matches how TMGMT itself behaves and I am not asking for a change. But an administrator switching this on is sending unpublished content to a third party, and the README is the right place to say so plainly.Two things that are not problems, recorded so the next reviewer does not chase them:
js/connection.js(nulltoNULL, spacing around-). These are false positives from PHP-oriented sniffs reading JavaScript: core's ownmisc/drupal.jstrips 28 of the same andmisc/ajax.jstrips 106. Drupal.org lints JavaScript with ESLint, and lowercasenull,trueandfalseare correct in JS. No change wanted.src/Hook/TmgmtLatwRequirements.phpandsrc/Install/Requirements/TmgmtLatwRequirements.phplook like a duplicate pair but are the correct Drupal 11.2 split, the runtime hook versusInstallRequirementsInterface. Correct as written.Setting to Needs work for the first point only.
Comment #3
vishal.kadamComment #4
szymon.zawadzki commentedThanks for the review!
1. CloudApiExceptionSubscriber now returns fixed messages for internal_error and for the default arm. For consistency, SiteApi does the same where an unexpected \Throwable used to end up in a translation_failed answer. The detail is only logged. Messages of the module's own CloudExceptions are unchanged.
2. $is_syncing was not intentional. During a config import, uninstall no longer calls LATW Cloud. It logs a notice to disconnect the site in the panel instead.
3. The README now says that unpublished and in-moderation content can be sent to LATW Cloud.
Setting back to Needs review.
Comment #5
avpadernoThank you for applying!
Before giving links helpful to understand how the review process works, what to expect from a review, and what to do to avoid a review takes more time than needed, I would like to thank all the reviewers for the work they do.
These applications are volunters-driven, which also means it is not possible to predict when an application will be marked fixed and the applicant will get the permission to opt projects into security advisory policy. While we aim to make an application as quick as possible, it is also important for us that more people review the project used for an application. In this way, we make sure applications do not miss some important points that should be instead reported.
Applications are not meant to be complete debugging sessions that eliminate every existing bug, though. I apologize if sometimes applications seem to go into too-detailed reviews.
Please read Review process for security advisory coverage: What to expect for more details and Security advisory coverage application checklist to understand what reviewers look for. Tips for ensuring a smooth review gives some hints for a smoother review.
The important notes are the following.
Keep in mind that once the project is opted into security advisory coverage, only Security Team members may change coverage.
To the reviewers
Please read How to review security advisory coverage applications, Application workflow, What to cover in an application review, and Tools to use for reviews.
The important notes are the following.
For new reviewers, I would also suggest to first read In which way the issue queue for coverage applications is different from other project queues.
Comment #6
vishal.kadamFILE: tmgmt_latw.libraries.yml
version: VERSIONVERSION is only used by Drupal core modules. Contributed modules should use a literal string that does not change with the Drupal core version a site is using.
Comment #7
szymon.zawadzki commentedThanks! tmgmt_latw.libraries.yml now uses a literal version (1.0.0) instead of VERSION.
Setting back to Needs review.
Comment #8
dietric@gmail.comI read the 1.0.x branch at commit 3f1e219 and checked it against the review in #2 and the follow-up in #6. I did not run the tests or the pipeline.
The blocking item from #2 is resolved. CloudApiExceptionSubscriber::onException() now sends only CloudException messages and a fixed string for any other throwable, and the detail goes to the log. The same pattern is used in SiteApi when saving translations. SignatureAccessCheck runs before any endpoint, verifies the HMAC with hash_equals(), rejects timestamps outside the 300 second window, and only stores the nonce after the signature has passed, so an unauthenticated caller cannot fill the nonce store. The $is_syncing handling in tmgmt_latw_uninstall() and the literal 1.0.0 library version are also in place.
A few small points, none blocking.
SignatureAccessCheck::NONCE_TTL is 600 and the validity window is 300 seconds either side of now, so a request stamped 300 seconds ahead can be replayed until exactly 600 seconds after first use. The docblock says the nonce is kept "a little longer" than a signature is valid, but it is equal at the boundary. A small margin (for example Signer::WINDOW * 2 + 60) would match the comment.
The nonce is consumed inside an access checker. That works here because the check runs once per request, but access checks are not meant to have side effects, so a short comment saying so would help the next maintainer.
unknown_key and invalid_signature are different answers, so an unauthenticated caller can learn whether a key id matches. The key id is not a secret, so I would not change it, but it may be worth a line in the README.
tmgmt_latw_uninstall() calls LATW Cloud with a 30 second timeout when the module is uninstalled from the UI or Drush. If the service is slow, the uninstall waits. It is caught and logged, so it is safe, only slow.
I see no blocking concerns in the code I read.
Comment #9
szymon.zawadzki commentedThanks for the review!
1. NONCE_TTL is now Signer::WINDOW * 2 + 60, so a nonce outlives every signature it was sent with, and a unit test checks that.
2. SignatureAccessCheck now has a comment explaining why it consumes the nonce, and why that is safe: the check runs once per request, and only after the signature has passed.
3. The README now says that unknown_key and invalid_signature are different answers, so LATW Cloud can tell that a site was disconnected, and that the key id is not a secret.
4. I kept the uninstall call as it is. It is skipped during a config import, and a failure is caught and logged. Telling LATW Cloud on a manual uninstall is worth the wait.
Leaving it at Needs review.