Skip to content

[VIES Integration] Per-environment daily request rate-limit - #11734

Merged
dcenic merged 10 commits into
mainfrom
bugs/651007VIESCodeunit248BGAPINo
Sep 24, 2026
Merged

dcenic merged 10 commits into
mainfrom
bugs/651007VIESCodeunit248BGAPINo

Conversation

@dcenic

@dcenic dcenic commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

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

  • At the start of 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.
  • The counter lives in a Base Application table (243 "VAT Reg. No. Lookup Quota", DataPerCompany = false) holding a single environment-wide row. Because the read/modify/write executes in Base App code under a table lock, every caller shares one counter — including per-tenant extensions that call codeunit 248 (e.g. a custom "Verify All" action) and job-queue / API callers. Extensions cannot read or reset it.
  • The cap is 2000 lookups/day — roughly 10x the 99th-percentile legitimate daily usage, and ~10x below the volume at which VIES deny-lists a shared IP.
  • Because the counter is gated in OnRun, it covers all VIES code paths (interactive, background, API, direct CODEUNIT.Run(248), and codeunit 249 field validation, which funnels into 248).
  • Online (SaaS) only. On-premises tenants own their own outbound IP and only affect themselves, so the limit does not apply there.

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#651007

How I validated this

  • I read the full diff and it contains only changes I intended.
  • I added or updated tests for the new behavior.

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

  • Online only — on-premises behavior is unchanged.
  • The pre-existing background/API guard in codeunit 249 field validation is intentionally left in place — it is a complementary reliability control, not a duplicate of this quota.

@dcenic
dcenic requested a review from a team as a code owner September 22, 2026 12:28
@dcenic
dcenic enabled auto-merge September 22, 2026 12:28
@github-actions github-actions Bot added the Team: Finance GitHub request for Finance area label Sep 22, 2026
@github-actions github-actions Bot added this to the Version 30.0 milestone Sep 22, 2026
mynjj
Joshua (mynjj) previously approved these changes Sep 22, 2026
@alexei-dobriansky

Copy link
Copy Markdown
Contributor

Good Sense Reviewer - Round 1

Recommendation: Accept

What this PR does

This change blocks direct execution of the VIES lookup codeunit from SaaS API, SOAP, OData, and background sessions, while leaving interactive validation and on-prem use unchanged. The guard runs before the service call and before any VAT registration log setup, so automated callers cannot reach the external VIES endpoint through codeunit 248.

The placement looks correct for the reported problem. The existing codeunit 249 path already exits for API and non-interactive sessions before it runs codeunit 248, and this change closes the direct-codeunit path that could bypass that protection.

Problem-solution fit

Fit: Strong

The bug describes repeated non-interactive VIES calls from SaaS causing shared outbound access to be blocked. The change targets that exact path by rejecting non-interactive SaaS execution at the start of codeunit 248, without changing interactive validation.

Suggestions

None.

Risk assessment and necessity

Risk: The main regression surface is extensions or jobs that directly run codeunit 248 in SaaS; that is the intended behavior for this fix. Interactive UI validation remains allowed, the existing codeunit 249 caller path is already non-interactive-safe, and on-prem environments are not restricted.

Necessity: The change is needed because blocking only the higher-level caller still leaves a direct automated path to the shared VIES service. The scope is narrow and matches the incident pattern.


[AI-PR-REVIEW] version=1 promptVersion=4 system=github pr=11734 round=1 by=alexei-dobriansky at=2026-09-22T13:04:30Z lastSha=e7fe86c100f6c4d4e022d4ebf49776af7dbe560c reviewKey=f0968037118174ea2e4c46ca523f39d42101d4e8a641400959194261a536cca7 suggestions=none

@dcenic
dcenic disabled auto-merge September 22, 2026 15:54
@dcenic
dcenic requested a review from a team as a code owner September 22, 2026 20:27
@dcenic dcenic changed the title [VIES Integration] Disallowing the use of codeunit 248 in background and API sessions [VIES Integration] Per-tenant daily request rate-limit Sep 22, 2026
@alexei-dobriansky

Copy link
Copy Markdown
Contributor

Good Sense Reviewer - Round 2

Recommendation: Request Changes

What this PR does

The new round replaces the automated-session block with a SaaS-only daily quota for VIES lookups. The quota is stored in module-scoped isolated storage, emits telemetry in log-only mode, and has tests for blocking, log-only behavior, day rollover, and on-prem skip behavior.

The quota design fits the shared-egress problem, but the implementation now runs before the normal validation and web-service path. It writes the quota and commits immediately, so later errors can leave unrelated caller work committed even though the validation failed.

Status of previous suggestions

None - the previous round had no suggestions.

New observations (commits since round 1)

S1 (🔴 High): Avoid committing caller work before VIES fails
The quota code calls Commit() before the VIES request and before later paths that can still raise an error. This can commit unrelated caller changes even though VAT validation fails. Move the durable quota update to a design that does not commit the caller transaction.

S2 (🔴 High): Make the quota increment atomic
The quota reads the count, increments it in memory, and writes it back without a lock or atomic update. Parallel API or job-queue calls can overwrite each other and let a noisy tenant send far more requests than the daily limit. Store the counter behind a locked row or another atomic mechanism before relying on it for enforcement.

S3 (🟠 Moderate): Use a stable telemetry event id
The quota-reached telemetry uses an empty event id. The rollout depends on this signal, so give it a stable id like the nearby VIES telemetry. This will make the log-only phase easier to query and monitor reliably.

Risk assessment and necessity

Risk: High until the transaction issue is fixed. Codeunit 248 can be called from validation, APIs, background sessions, and extensions, and the new Commit() can make caller data durable before a later validation or service error. The non-atomic counter also weakens the protection when calls arrive concurrently.

Necessity: The shared VIES outbound address needs protection from high-volume tenant traffic, so the quota is a valid direction. The implementation should be adjusted before merge because the current transaction and concurrency behavior can create new reliability and data-integrity risks.


[AI-PR-REVIEW] version=1 promptVersion=4 system=github pr=11734 round=2 by=alexei-dobriansky at=2026-09-23T01:18:52Z lastSha=ed031ec6af0d45722671a585571bcf12b44df2a4 reviewKey=1f4105343f59c5ec5a8fc671917eb09a06abec59635377e5c637dfc747f3cb16 suggestions=S1@3b80b372:new,S2@11b70480:new,S3@beb6e241:new parentRound=1

@dcenic

This comment was marked as duplicate.

@alexei-dobriansky

Copy link
Copy Markdown
Contributor

Good Sense Reviewer - Round 3

Recommendation: Request Changes

What this PR does

This round assigns a stable telemetry id to the daily VIES quota signal. The daily counter, SaaS-only behavior, log-only rollout, and tests are otherwise unchanged.

The transaction concern from the previous round is resolved: documented Codeunit.Run transaction rules require a clean boundary, and the known callers do not write before invoking codeunit 248. The concurrency concern remains because parallel sessions can repeatedly overwrite each other's increments.

Status of previous suggestions
ID Title Status Author response
S1 Avoid committing caller work before VIES fails Disputed Codeunit.Run requires a clean transaction, and the known callers do not write before invoking codeunit 248. This resolves the concern.
S2 Make the quota increment atomic Disputed The expected abuse is sequential, and the limit has headroom for some concurrency undercount.
S3 Use a stable telemetry event id Addressed The telemetry call now uses 0000VL7.
New observations (commits since round 2)

None - the new commit only addresses S3.

Risk assessment and necessity

Risk: S2 remains a concurrency blocker. With several parallel sessions, each loop can read the same count and overwrite the same next value, so actual VIES traffic can approach the deny-list volume while the stored counter is still below 2000. The current tests cover sequential calls only, and the full pull-request build is still running.

Necessity: Protecting the shared VIES endpoint is important, and a tenant-wide quota is a reasonable direction. The counter must reliably limit parallel callers before enforcement is enabled; the telemetry-id correction is complete.


[AI-PR-REVIEW] version=1 promptVersion=4 system=github pr=11734 round=3 by=alexei-dobriansky at=2026-09-23T07:14:44Z lastSha=8b3f2bb64e52ff061cc1aebe4a581ae99ef9da21 reviewKey=b79531a20878007407bd0ffcc59007f1d649b96d83f2791397705905bc5c3dc0 suggestions=S1@3b80b372:disputed,S2@11b70480:disputed,S3@beb6e241:addressed parentRound=2

@dcenic

dcenic commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Good Sense Reviewer - Round 3

Recommendation: Request Changes

What this PR does
This round assigns a stable telemetry id to the daily VIES quota signal. The daily counter, SaaS-only behavior, log-only rollout, and tests are otherwise unchanged.

The transaction concern from the previous round is resolved: documented Codeunit.Run transaction rules require a clean boundary, and the known callers do not write before invoking codeunit 248. The concurrency concern remains because parallel sessions can repeatedly overwrite each other's increments.

Status of previous suggestions
ID Title Status Author response
S1 Avoid committing caller work before VIES fails Disputed Codeunit.Run requires a clean transaction, and the known callers do not write before invoking codeunit 248. This resolves the concern.
S2 Make the quota increment atomic Disputed The expected abuse is sequential, and the limit has headroom for some concurrency undercount.
S3 Use a stable telemetry event id Addressed The telemetry call now uses 0000VL7.
New observations (commits since round 2)
None - the new commit only addresses S3.

Risk assessment and necessity
Risk: S2 remains a concurrency blocker. With several parallel sessions, each loop can read the same count and overwrite the same next value, so actual VIES traffic can approach the deny-list volume while the stored counter is still below 2000. The current tests cover sequential calls only, and the full pull-request build is still running.

Necessity: Protecting the shared VIES endpoint is important, and a tenant-wide quota is a reasonable direction. The counter must reliably limit parallel callers before enforcement is enabled; the telemetry-id correction is complete.

[AI-PR-REVIEW] version=1 promptVersion=4 system=github pr=11734 round=3 by=alexei-dobriansky at=2026-09-23T07:14:44Z lastSha=8b3f2bb64e52ff061cc1aebe4a581ae99ef9da21 reviewKey=b79531a20878007407bd0ffcc59007f1d649b96d83f2791397705905bc5c3dc0 suggestions=S1@3b80b372:disputed,S2@11b70480:disputed,S3@beb6e241:addressed parentRound=2

Thanks for the round-3 review. Responses to S1–S3:

S1 — Avoid committing caller work before VIES fails. After checking the transaction semantics, I'd keep the commit rather than restructure — it's placed at the very top of OnRun, before any other work, and that is safe here:

Codeunit.Run is an isolated, implicitly-committed transaction scope. Per the Codeunit.Run docs: "If you're already in a transaction you must commit first before calling Codeunit.Run." So codeunit 248 begins at a clean commit boundary — there is no uncommitted caller work in scope that an early commit could inadvertently persist.
The two real callers confirm this: codeunit 249 performs no database writes before CODEUNIT.Run(248, …) (it only reads the service config and inits the log record in memory), and the "Verify VAT Registration No." action invokes 248 via RunObject, i.e. a fresh action scope.
The quota update is the first thing in OnRun (before InitVATRegistrationLog and any log writes), so at the commit the only change 248 has made is the counter increment — the commit persists exactly the intended counter and nothing else.
Codeunit 248 already commits later in the same invocation (the existing end-of-flow Commit()), so this flow is already a commit boundary. The early commit only moves the counter's durability ahead of the SOAP call, which is required so an attempt is still counted when the VIES call subsequently fails — exactly the deny-list case, where a resilient bulk job that swallows errors would otherwise keep hammering VIES while the counter never advances.
S2 — Make the quota increment atomic. ✅ Addressed. The counter is now a dedicated table (table 243 "VAT Reg. No. Lookup Quota", Access = Internal, DataPerCompany = false so it is one counter per environment, shared across all companies in the database) instead of Isolated Storage. Codeunit 248 does a locked read-modify-write:

LockTable() → get-or-insert the single row → reset on a new UTC day → check the cap → increment → Modify() → Commit(), all before the SOAP call. The row lock serializes concurrent sessions, so parallel API/job-queue callers can no longer overwrite each other's increment — the count is exact, not best-effort.
The lock is released by the commit before the outbound VIES call, so it is never held across the (potentially slow) callout.
The single row is created lazily on first use; a concurrent creator simply loses the insert race and reads the committed row.
The table is Access = Internal with InherentEntitlements/InherentPermissions, so extensions cannot read, reset, or bypass the counter.
Enforcement is now on (lookups beyond the daily limit are blocked, not just logged), which this atomic counter makes safe under concurrency.

S3 — Stable telemetry event id. Done — the quota-reached signal uses the registered id 0000VL7, consistent with the nearby VIES telemetry.

@dcenic dcenic changed the title [VIES Integration] Per-tenant daily request rate-limit [VIES Integration] Per-environment daily request rate-limit Sep 23, 2026
@dcenic
dcenic enabled auto-merge September 23, 2026 12:48
@alexei-dobriansky

Copy link
Copy Markdown
Contributor

Good Sense Reviewer - Round 4

Recommendation: Accept with Suggestions

What this PR does

This round replaces the previous quota storage with a SaaS-only, table-backed daily quota that is enforced before the VIES call. The single non-company row is locked for the read-modify-write and committed before the outbound request, so the earlier steady-state concurrency concern is addressed.

The change fits the shared-egress problem and now blocks excess calls instead of only logging them. Two non-blocking issues remain: locally invalid blank requests can consume quota, and the audit entry describes the last allowed lookup as blocked.

Status of previous suggestions
ID Title Status Author response
S1 Avoid committing caller work before VIES fails Disputed The codeunit starts at a clean transaction boundary, and the known callers do not write before invoking it.
S2 Make the quota increment atomic Addressed The counter now uses a locked single-row table with a committed read-modify-write before the SOAP request.
S3 Use a stable telemetry event id Addressed The quota telemetry now uses 0000VL7.
New observations (commits since round 3)

S4 (🟠 Moderate): Do not count locally invalid requests
The quota is registered before the existing blank VAT number check runs. A blank request never calls VIES, but it still consumes one daily quota entry and can block valid validations for the rest of the day. Move the quota registration after the blank VAT check and keep it before the outbound request.

S5 (🟠 Moderate): Do not audit an allowed lookup as blocked
The audit entry is written on the last allowed lookup, before the next request is blocked. The message says a lookup was blocked, which makes the security audit record misleading. Move the audit to the blocked branch or change the message and result to say the limit was reached.

Risk assessment and necessity

Risk: the main regression surface is SaaS VAT validation across all companies in one environment. The locked table removes the previous lost-update risk, but invalid local calls can still burn the shared quota and the audit trail can describe an allowed lookup as a blocked one.

Necessity: the change is necessary because uncontrolled VIES traffic from one environment can break validation for co-located environments on the same outbound address. A per-environment SaaS quota is the right scope, and the remaining findings are fixable without changing that design.


[AI-PR-REVIEW] version=1 promptVersion=4 system=github pr=11734 round=4 by=alexei-dobriansky at=2026-09-23T13:05:24Z lastSha=d90674766fade42cf08d39050a2b46a5d8018cf6 reviewKey=529108d986106e7dbfe16df4ff018339e2f3c97222a58003007d4405b1e8f030 suggestions=S1@3b80b372:disputed,S2@11b70480:addressed,S3@beb6e241:addressed,S4@5385caf4:new,S5@ed6451b1:new parentRound=3

…ndard request path

Address review feedback on the per-environment daily VIES quota:
- S2: charge the quota only on the standard VIES request path, after the blank-number
  check and the OnRun IsHandled event, so handled/replaced or invalid lookups that never
  contact VIES no longer consume quota.
- S1: extract the quota read-modify-write and its Commit into a dedicated codeunit 247
  "VAT Lookup Quota Mgt." invoked via a codeunit run, so the counter commit is its own
  unit of work placed immediately before the outbound request.

Note: codeunit 248 already commits around the outbound call in its normal flow, so this
does not introduce a new ambient-commit point; it makes the quota increment durable and
confines it to real VIES sends.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dcenic

dcenic commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

alexei-dobriansky Pushed a change addressing the review.

S2 - done. The quota is now charged only on the standard VIES request path: RegisterAndCheckVIESCallQuota was removed from OnRun, and the quota is invoked inside SendRequestToVatRegistrationService, after the blank-number check and only on the not-IsHandled path. Handled/replaced lookups and blank-number errors no longer consume daily quota.

S1 - done (dedicated codeunit run). The quota read-modify-write and its Commit moved into a new dedicated codeunit 247 "VAT Lookup Quota Mgt.", invoked via a codeunit run immediately before the outbound request, so the counter commit is its own unit of work and the increment stays durable.

One honest note on transaction semantics: a same-session codeunit run + Commit still commits the ambient transaction (AL has no partial commit; only a separate session would fully isolate, which is incompatible with synchronous blocking). In practice codeunit 248 already commits around the outbound call in its normal flow (the existing Commit in LookupVatRegistrationFromWebService), so this does not add a new ambient-commit point - it confines the quota commit to real VIES sends and makes it durable.

Comment thread src/Layers/W1/Tests/VAT/ERMVATVIESLookupUT.Codeunit.al
dcenic and others added 2 commits September 23, 2026 20:51
…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>
@dcenic

dcenic commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the AL review agent findings - addressed three, respectfully declining the interface one.

Commit() commits caller's ambient transaction (Medium) - comments fixed. You're right that a same-session codeunit run does not create an isolated transaction. This was a deliberate design choice (dedicated run + Commit) but the comments overclaimed isolation. Reworded the doc/inline comments in codeunit 247 and codeunit 248 to state plainly that the Commit also commits the caller's ambient transaction - the same boundary codeunit 248 already commits at around the outbound call (existing Commit in LookupVatRegistrationFromWebService). No new ambient-commit point is introduced. (commit 1c024c5)

Access = Internal on codeunit 247 (Medium) - done. Set Access = Internal; it is a Base Application implementation detail. Tests reach it via internalsVisibleTo (Tests-VAT). (commit 1c024c5)

Test TransactionModel (High) - done. The three quota tests now declare [TransactionModel(TransactionModel::AutoCommit)] since they commit the counter. (commit 1c024c5)

Extract an interface for the quota (High) - declining. For an internal, single-purpose quota counter in the Base Application, an interface + dependency injection is more indirection than this warrants. The logic is already unit-tested directly via internal test seams on codeunit 247 (SetVIESCallQuotaLimitForTest / InvokeVIESCallQuotaForTest / SeedVIESCallQuotaForTest / GetVIESCallCountForTest / ClearVIESCallQuotaForTest) with SaaS simulated via the Environment Info test library - no interface indirection is needed to test the quota decision, and this codeunit is not an extension point. Happy to revisit if a concrete substitution scenario comes up.

Also, for completeness: the earlier S4 (charge only requests that reach VIES) was already handled when the quota moved onto the standard request path after the blank-number check (commit ade6298), and S5 (audit wording) was reworded to say the daily limit was reached rather than "blocked" (commit 4349fed).

Comment thread src/Layers/W1/BaseApp/Finance/VAT/Registration/VATLookupQuotaMgt.Codeunit.al Outdated
Comment thread src/Layers/W1/Tests/VAT/ERMVATVIESLookupUT.Codeunit.al
@alexei-dobriansky

Copy link
Copy Markdown
Contributor

Good Sense Reviewer - Round 5

Recommendation: Accept with Suggestions

What this PR does

This round moves quota enforcement to the standard VIES request path, after local validation and before the outbound call. It also extracts the locked counter into an internal codeunit and updates the audit wording and tests.

Blank and subscriber-handled requests no longer consume quota, so the main correctness issue from the previous round is fixed. The audit text is clearer, but the last allowed lookup is still recorded with a failure result.

Status of previous suggestions
ID Title Status Author response
S1 Avoid committing caller work before VIES fails Disputed The same-session commit behavior is now documented, and the existing call path already commits around the outbound request.
S2 Make the quota increment atomic Addressed The counter uses a locked shared row and commits the atomic read-modify-write.
S3 Use a stable telemetry event id Addressed The telemetry event uses 0000VL7.
S4 Do not count locally invalid requests Addressed Quota enforcement now runs after the blank-number check and only on the standard request path.
S5 Do not audit an allowed lookup as blocked Not addressed The message was corrected, but the allowed lookup is still logged with SecurityOperationResult::Failure.
New observations (commits since round 4)

None.

Risk assessment and necessity

Risk: the quota now covers only requests that can reach VIES, and the locked environment-wide counter handles concurrent sessions. The remaining issue affects audit accuracy rather than quota enforcement.

Necessity: uncontrolled traffic can deny-list a shared outbound address and disrupt other environments. The SaaS-only environment quota remains a targeted and necessary safeguard.


[AI-PR-REVIEW] version=1 promptVersion=4 system=github pr=11734 round=5 by=alexei-dobriansky at=2026-09-23T19:21:11Z lastSha=1c024c557daff7ebc8210603320abecdd621b1e6 reviewKey=304b967ba875ce3997f454bfb070545a4f10e4a2ec0b818fbfab6e339af29018 suggestions=S1@3b80b372:disputed,S2@11b70480:addressed,S3@beb6e241:addressed,S4@5385caf4:addressed,S5@ed6451b1:notaddressed parentRound=4

…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>
@dcenic

dcenic commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Good Sense Reviewer - Round 5

Recommendation: Accept with Suggestions

What this PR does
This round moves quota enforcement to the standard VIES request path, after local validation and before the outbound call. It also extracts the locked counter into an internal codeunit and updates the audit wording and tests.

Blank and subscriber-handled requests no longer consume quota, so the main correctness issue from the previous round is fixed. The audit text is clearer, but the last allowed lookup is still recorded with a failure result.

Status of previous suggestions
ID Title Status Author response
S1 Avoid committing caller work before VIES fails Disputed The same-session commit behavior is now documented, and the existing call path already commits around the outbound request.
S2 Make the quota increment atomic Addressed The counter uses a locked shared row and commits the atomic read-modify-write.
S3 Use a stable telemetry event id Addressed The telemetry event uses 0000VL7.
S4 Do not count locally invalid requests Addressed Quota enforcement now runs after the blank-number check and only on the standard request path.
S5 Do not audit an allowed lookup as blocked Not addressed The message was corrected, but the allowed lookup is still logged with SecurityOperationResult::Failure.
New observations (commits since round 4)
None.

Risk assessment and necessity
Risk: the quota now covers only requests that can reach VIES, and the locked environment-wide counter handles concurrent sessions. The remaining issue affects audit accuracy rather than quota enforcement.

Necessity: uncontrolled traffic can deny-list a shared outbound address and disrupt other environments. The SaaS-only environment quota remains a targeted and necessary safeguard.

[AI-PR-REVIEW] version=1 promptVersion=4 system=github pr=11734 round=5 by=alexei-dobriansky at=2026-09-23T19:21:11Z lastSha=1c024c557daff7ebc8210603320abecdd621b1e6 reviewKey=304b967ba875ce3997f454bfb070545a4f10e4a2ec0b818fbfab6e339af29018 suggestions=S1@3b80b372:disputed,S2@11b70480:addressed,S3@beb6e241:addressed,S4@5385caf4:addressed,S5@ed6451b1:notaddressed parentRound=4

For S5, we do't care if the actual VIES call is successful or not. We want to log an audit failure if an environment reaches over 2000 calls per day. Whether they exceed it with 2001 calls or just make it with 2000 calls, it makes no big difference one call more or less. They have hit the ceiling and we want to raise a security audit failure.

@alexei-dobriansky

Copy link
Copy Markdown
Contributor

Good Sense Reviewer - Round 6

Recommendation: Accept with Suggestions

What this PR does

This round renames the quota telemetry label variable to follow the locked-text convention and resets committed quota test state in Initialize(). The test cleanup is a good change because the quota tests use AutoCommit and can otherwise leak the quota row or SaaS test flag into later tests.

The net change still enforces a SaaS-only daily VIES lookup quota with a locked shared counter before the outbound call. The latest commit does not change runtime behavior, and the remaining audit issue is still limited to audit accuracy.

Status of previous suggestions
ID Title Status Author response
S1 Avoid committing caller work before VIES fails Disputed The same-session commit behavior is documented, and the existing call path already commits around the outbound request.
S2 Make the quota increment atomic Addressed The counter uses a locked shared row and commits the atomic read-modify-write.
S3 Use a stable telemetry event id Addressed The telemetry event uses 0000VL7, and this round only renames the label variable.
S4 Do not count locally invalid requests Addressed Quota enforcement runs after the blank-number check and only on the standard request path.
S5 Do not audit an allowed lookup as blocked Not addressed The allowed lookup that reaches the limit is still logged with SecurityOperationResult::Failure.
New observations (commits since round 5)

None - the latest changes only rename a label variable and reset test state before each test.

S5 (🟠 Moderate): Do not audit an allowed lookup as blocked
The lookup that reaches the daily limit is still allowed to call VIES, but it is logged with SecurityOperationResult::Failure. Please either log the security audit on the first blocked lookup, or use a result that does not record the allowed lookup as a failed authorization.

Risk assessment and necessity

Risk: the latest runtime change is only a label variable rename, so quota behavior is unchanged. The locked environment-wide counter still handles concurrent sessions; the remaining risk is that audit data can show an allowed lookup as an authorization failure.

Necessity: uncontrolled VIES traffic can deny-list a shared outbound address and break validation for other environments. The SaaS-only quota remains a targeted safeguard, and the added test cleanup is needed to keep committed quota state from leaking between tests.


[AI-PR-REVIEW] version=1 promptVersion=4 system=github pr=11734 round=6 by=alexei-dobriansky at=2026-09-24T01:05:07Z lastSha=b11a8059bf9a5ffc84b42673a9e26f65c28378c5 reviewKey=41f5771a598c34dc4f248678f3087aca8ca769483951fb7534eadedd8c1909ef suggestions=S1@3b80b372:disputed,S2@11b70480:addressed,S3@beb6e241:addressed,S4@5385caf4:addressed,S5@ed6451b1:notaddressed parentRound=5

@dcenic
dcenic added this pull request to the merge queue Sep 24, 2026
Merged via the queue into main with commit e854bf0 Sep 24, 2026
172 checks passed
@dcenic
dcenic deleted the bugs/651007VIESCodeunit248BGAPINo branch September 24, 2026 07:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Team: Finance GitHub request for Finance area

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants