[29.x][VIES Integration] Per-environment daily request rate-limit - #11736
Conversation
Pull request was closed
Replace the background/API session block for codeunit 248 with a per-environment daily VIES lookup quota (table 243 "VAT Reg. No. Lookup Quota"), enforced on SaaS only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add Tests-VAT to BaseApp internalsVisibleTo so codeunit 134193 can reach the internal VIES quota test helpers on codeunit 248 (fixes AL0161 in the backport build). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Good Sense Reviewer - Round 1Recommendation: Request ChangesWhat this PR doesThis change adds a SaaS-only daily quota for EU VIES VAT registration number validation. The quota is shared across companies in the environment, stored in a new internal table, and checked before codeunit 248 sends the external SOAP request. The quota direction matches the shared-egress problem, and the tests cover the limit, day rollover, and on-premises skip behavior. The current implementation still has unsafe boundaries: it commits inside the validation codeunit before later failures can occur, and it consumes quota before the code knows that a VIES request will actually be sent. Problem-solution fitFit: Partial The change targets the reported flood pattern with a per-environment daily cap, which is the right kind of control. The implementation is broader than the actual VIES-call path and can affect validation failures or handled calls that do not contact VIES. SuggestionsS1 (🔴 High): Avoid committing before VIES can fail S2 (🟠 Moderate): Count only requests that can reach VIES S3 (🟠 Moderate): Keep quota test hooks out of production API Risk assessment and necessityRisk: The change is in a shared validation codeunit used by field validation, page actions, and direct codeunit calls. The daily counter is SaaS-only and the table lock limits races, but the early commit and broad entry-point placement can change validation and integration behavior outside real VIES calls. Necessity: A per-environment quota is necessary because one environment can otherwise flood the unauthenticated VIES service and affect other environments that share the same outbound address. The scope is appropriate, but the transaction and placement issues should be fixed before merge.
|
|
Thanks alexei-dobriansky — investigated. Note this branch is a line-for-line backport of #11734, so the substantive design points here really apply to the main PR; the fixes will land on #11734 and be mirrored to the backports. S1 (Commit before the lookup can fail) — agree, will address on #11734. The S2 (charge only requests that can reach VIES) — agree. Confirmed the quota is registered in S3 (test hooks on the production codeunit) — by design. The So: S1 and S2 are fair and will be fixed on the main PR (#11734) and ported here; treating them as design refinements rather than backport-specific defects. |
…ndard request path Backport of the review fixes: charge the per-environment daily VIES quota only on the standard request path (after the blank-number check and OnRun IsHandled event), and move the quota read-modify-write and its Commit into dedicated codeunit 247 "VAT Lookup Quota Mgt.". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
alexei-dobriansky Pushed a change addressing the review. S2 - done. The quota is now charged only on the standard VIES request path: S1 - done (dedicated codeunit run). The quota read-modify-write and its One honest note on transaction semantics: a same-session codeunit run + S3 - the quota test seams moved off the shared codeunit 248 onto the dedicated codeunit 247 and remain |
…eached, not blocked The audit entry fires on the last allowed lookup (the one that reaches the daily limit), so the message should describe the limit being reached rather than the lookup being blocked. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…st transaction model Address AL review agent feedback: - Mark codeunit 247 "VAT Lookup Quota Mgt." as Access = Internal (implementation detail). - Correct the doc/inline comments so they no longer imply the dedicated codeunit run isolates the transaction: the Commit also commits the caller's ambient transaction (the same boundary codeunit 248 already commits at around the outbound call). - Mark the quota tests with TransactionModel::AutoCommit since they commit the counter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Heads-up on the review state: the Good Sense Reviewer - Round 1 (Request Changes) above was raised against the first version of this work (blocking codeunit 248 in background/API sessions), before the per-environment daily VIES rate-limit was ported here. It no longer reflects the current code on this branch. The current implementation matches main #11734: a SaaS-only, table-backed daily quota (table 243 "VAT Reg. No. Lookup Quota") charged on the standard VIES request path via a dedicated codeunit 247 "VAT Lookup Quota Mgt.". The design review (S1-S5) and the AL review agent findings were addressed on #11734 and mirrored here:
|
Good Sense Reviewer - Round 2Recommendation: Request ChangesWhat this PR doesThe latest changes move quota charging to the standard VIES request path, after the blank-number and handled-request checks. They also move quota management into an internal codeunit and update the tests and audit wording. The placement issue is fixed. However, running the quota logic in a dedicated codeunit does not create a separate transaction: its Status of previous suggestions
New observations (commits since round 1)None - the new commits only address or respond to previous suggestions. Risk assessment and necessityRisk: The quota placement is now limited to real standard-path attempts, and the table lock protects concurrent increments. The remaining early Necessity: The per-environment quota remains necessary to prevent one environment from exhausting a shared VIES egress address. The scope is appropriate, but the transaction boundary must be corrected before merge.
|
…a state per test - Rename DailyQuotaReachedMsg -> DailyQuotaReachedTxt (telemetry/locked string convention). - Reset the VIES quota row and SaaS testability flag in the test Initialize() so an AutoCommit quota test that fails mid-way cannot leak committed state into later tests under a non-isolated test runner. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Good Sense Reviewer - Round 3Recommendation: Request ChangesWhat this PR doesThe latest commit renames a telemetry label and resets quota-related test state during test initialization. It does not change the production quota flow. The main quota design still matches the reported shared-egress problem, and the test cleanup is useful. The remaining blocker is unchanged: the quota code still commits before request preparation and the external VIES call can fail. Status of previous suggestions
New observations (commits since round 2)None - the new commit only renames telemetry text and resets test state. Risk assessment and necessityRisk: The latest commit lowers test leakage risk, but it does not reduce the production transaction risk. The early Necessity: The quota is still needed to stop one environment from exhausting the shared VIES capacity. The scope is right, but the transaction boundary must be corrected before merge.
|
What & why
Codeunit 248 ("VAT Lookup Ext. Data Hndl") calls the EU VIES service over SOAP to validate VAT registration numbers. VIES is unauthenticated and rate-limits by source IP. In Business Central online, many tenants share the same outbound egress IP per app service, so when a single noisy tenant floods VIES, VIES deny-lists the shared IP and every co-located tenant starts seeing "VIES service unavailable" errors.
We have a concrete example: one tenant scheduled a job that called codeunit 248 ~1.66 million times in 8 days, getting the whole app service deny-listed by VIES.
This PR adds a per-tenant (per-environment) daily request rate-limit to codeunit 248 so no single tenant can flood VIES and deny-list the shared outbound IP.
How it works
OnRun(before the SOAP request), the codeunit reads a per-day counter, resets it when the UTC day changes, increments it, then persists and commits it before the outbound call.OnRun, it covers all VIES code paths (interactive, background, API, directCODEUNIT.Run(248), and codeunit 249 field validation, which funnels into 248).Enforcement
When a tenant reaches the daily cap, further lookups are blocked (
Error(DailyQuotaExceededErr)) for the rest of the UTC day; the call that hits the limit also emits a security-audit entry and telemetry, and blocked lookups are not counted. The counter is stored in a dedicated table (243 "VAT Reg. No. Lookup Quota", Access = Internal), so the cap value or enforcement behavior can be adjusted later as a pure code change that can be serviced into release branches.Why this replaces the earlier approach
This PR previously blocked codeunit 248 in background/API sessions. That was incomplete: a foreground "Verify All" over a large customer list (or a PTE action) still reaches VIES, and it would break legitimate low-volume automated callers. A per-environment daily quota is benign to legitimate users (well under the cap) while still stopping the bulk-flooding pattern from every session type.
Linked work
Fixes AB#651033
How I validated this
What I tested and the outcome
New unit tests in
ERM VAT VIES Lookup UT(codeunit 134193) exercise the quota decision logic directly (the check runs before the SOAP call, so no service/mock is needed). They use the Environment Info test library to simulate SaaS and internal test-only seams on codeunit 248 to drive the counter:DailyVIESCallQuotaBlocksWhenLimitReached— lookups beyond the daily limit are blocked and blocked lookups are not counted.DailyVIESCallQuotaResetsOnNewDay— after the day rolls over the counter resets, the customer gets a fresh full daily allowance, and the cap re-applies within the same day.DailyVIESCallQuotaSkippedOnPrem— the quota does not apply on-premises (nothing counted or blocked).Risk & compatibility