-
Notifications
You must be signed in to change notification settings - Fork 464
[VIES Integration] Per-environment daily request rate-limit #11734
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+306
−0
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
bbaed87
[VIES Integration] Disallow running codeunit 248 in background and AP…
dcenic e7fe86c
[VIES Integration] Disallow running codeunit 248 in background and AP…
dcenic ed031ec
[VIES Integration] Per-tenant daily request rate-limit
dcenic 8b3f2bb
[VIES Integration] Per-tenant daily request rate-limit
dcenic 00b8aa7
[VIES Integration] Per-tenant daily request rate-limit
dcenic d906747
[VIES Integration] Per-tenant daily request rate-limit
dcenic ade6298
[VIES Integration] Move VIES quota to a dedicated codeunit on the sta…
dcenic 4349fed
[VIES Integration] Reword VIES quota audit message to reflect limit r…
dcenic 1c024c5
[VIES Integration] Make quota codeunit internal, fix comments, set te…
dcenic b11a805
[VIES Integration] Rename quota telemetry label to Txt and reset quot…
dcenic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
147 changes: 147 additions & 0 deletions
147
src/Layers/W1/BaseApp/Finance/VAT/Registration/VATLookupQuotaMgt.Codeunit.al
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| // ------------------------------------------------------------------------------------------------ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. See License.txt in the project root for license information. | ||
| // ------------------------------------------------------------------------------------------------ | ||
| namespace Microsoft.Finance.VAT.Registration; | ||
|
|
||
| using System.Environment; | ||
| using System.Telemetry; | ||
|
|
||
| /// <summary> | ||
| /// Enforces the per-environment daily EU VIES lookup quota, run as a dedicated codeunit on the standard | ||
| /// VIES request path. It increments and commits the daily counter before the outbound request so the count | ||
| /// stays durable. That commit also commits the caller's ambient transaction - the same boundary at which | ||
| /// codeunit 248 already commits around the outbound call - so it is not an isolated transaction. | ||
| /// </summary> | ||
| codeunit 247 "VAT Lookup Quota Mgt." | ||
|
dcenic marked this conversation as resolved.
dcenic marked this conversation as resolved.
|
||
| { | ||
| Access = Internal; | ||
| Permissions = TableData "VAT Reg. No. Lookup Quota" = rimd; | ||
|
|
||
| trigger OnRun() | ||
|
dcenic marked this conversation as resolved.
|
||
| begin | ||
| RegisterAndCheckQuota(); | ||
| end; | ||
|
|
||
| var | ||
| DailyQuotaExceededErr: Label 'VAT registration number validation against the EU VIES service has reached the daily limit for this environment. Try again tomorrow, and avoid verifying VAT registration numbers in bulk.'; | ||
| DailyQuotaReachedTxt: Label 'The daily EU VAT reg. no. validation limit was reached for this environment.', Locked = true; | ||
| SecurityAuditDailyQuotaExceededTxt: Label 'The EU VAT Registration No. validation service (VIES) daily lookup limit was reached for this environment; further lookups are blocked for the rest of the day.', Locked = true; | ||
| EUVATRegNoValidationServiceTok: Label 'EUVATRegNoValidationServiceTelemetryCategoryTok', Locked = true; | ||
| QuotaTestOverride: Boolean; | ||
| QuotaTestMaxDailyCallCount: Integer; | ||
|
|
||
| local procedure RegisterAndCheckQuota() | ||
| var | ||
| VATRegNoLookupQuota: Record "VAT Reg. No. Lookup Quota"; | ||
| EnvironmentInformation: Codeunit "Environment Information"; | ||
| AuditLog: Codeunit "Audit Log"; | ||
| begin | ||
| // The unauthenticated EU VIES service deny-lists the shared outbound IP address of a cloud app service | ||
| // when it receives high-volume validation, which then affects every co-located environment on that address. | ||
| // Cap the number of VIES lookups per environment per day so a single environment cannot flood VIES - from | ||
| // any session type (interactive, background or API) and from either the Base Application or a per-tenant | ||
| // extension that reuses this codeunit - and get the shared address deny-listed. The count is kept in a | ||
| // single row shared by all companies in the database (DataPerCompany = false) that is locked for the brief | ||
| // read-modify-write, so concurrent sessions increment it atomically without lost updates. Enforced online | ||
| // (SaaS) only; on-prem environments own their own outbound address and only affect themselves. | ||
| if not EnvironmentInformation.IsSaaS() then | ||
| exit; | ||
|
|
||
| GetQuotaUnderLock(VATRegNoLookupQuota); | ||
|
|
||
| // Reset the counter at the start of a new (UTC) day. | ||
| if VATRegNoLookupQuota."Window Date" <> Today() then begin | ||
| VATRegNoLookupQuota."Window Date" := Today(); | ||
| VATRegNoLookupQuota."Daily Call Count" := 0; | ||
| end; | ||
|
|
||
| // Block once the daily limit is reached. Blocked calls are not counted (they never reach the service). | ||
| if VATRegNoLookupQuota."Daily Call Count" >= GetMaxDailyCallCount() then | ||
| Error(DailyQuotaExceededErr); | ||
|
|
||
| VATRegNoLookupQuota."Daily Call Count" += 1; | ||
|
|
||
| // On the call that reaches the limit, record it once - after this, lookups are blocked for the rest of the day. | ||
| if VATRegNoLookupQuota."Daily Call Count" = GetMaxDailyCallCount() then begin | ||
| // 4, 0 = AuditMessageOperation / AuditMessageOperationResult (standard security-audit codes; also routes the entry to Purview). | ||
| AuditLog.LogAuditMessage(SecurityAuditDailyQuotaExceededTxt, SecurityOperationResult::Failure, AuditCategory::Authorization, 4, 0); | ||
| Session.LogMessage('0000VL7', DailyQuotaReachedTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::All, 'Category', EUVATRegNoValidationServiceTok); | ||
| end; | ||
|
|
||
| // Persist and commit the count before the outbound request so the increment stays durable even if the | ||
| // subsequent VIES call fails, and the row lock is released before the potentially slow VIES call. This | ||
| // commit also commits the caller's ambient transaction - the same boundary codeunit 248 commits at around | ||
| // the outbound call - so it is not isolated from caller state. | ||
| VATRegNoLookupQuota.Modify(); | ||
| Commit(); | ||
|
dcenic marked this conversation as resolved.
|
||
| end; | ||
|
|
||
| local procedure GetQuotaUnderLock(var VATRegNoLookupQuota: Record "VAT Reg. No. Lookup Quota") | ||
| begin | ||
|
dcenic marked this conversation as resolved.
|
||
| VATRegNoLookupQuota.LockTable(); | ||
| if VATRegNoLookupQuota.Get() then | ||
| exit; | ||
| // Create the single row on first use. Do not rely on install/upgrade triggers - they are not guaranteed | ||
| // to have run for every environment. | ||
| VATRegNoLookupQuota.Init(); | ||
| VATRegNoLookupQuota."Primary Key" := ''; | ||
| VATRegNoLookupQuota.Insert(); | ||
| end; | ||
|
|
||
| local procedure GetMaxDailyCallCount(): Integer | ||
| begin | ||
| if QuotaTestOverride then | ||
| exit(QuotaTestMaxDailyCallCount); | ||
| // Legitimate use is < ~200 lookups per environment per day (99th percentile). 2000 leaves generous headroom | ||
| // while staying roughly 10x below the daily volume at which VIES deny-lists a shared outbound address. | ||
| exit(2000); | ||
| end; | ||
|
|
||
| // The following members exist only so the automated tests can exercise the daily-quota decision logic | ||
| // without calling the external VIES service. They are internal, so the Base Application test libraries can | ||
| // reach them but per-tenant extensions cannot influence or bypass the quota. | ||
| internal procedure SetVIESCallQuotaLimitForTest(MaxDailyCallCount: Integer) | ||
| begin | ||
| QuotaTestOverride := true; | ||
| QuotaTestMaxDailyCallCount := MaxDailyCallCount; | ||
| end; | ||
|
|
||
| internal procedure InvokeVIESCallQuotaForTest() | ||
| begin | ||
| RegisterAndCheckQuota(); | ||
| end; | ||
|
|
||
| internal procedure SeedVIESCallQuotaForTest(WindowDate: Date; CallCount: Integer) | ||
| var | ||
| VATRegNoLookupQuota: Record "VAT Reg. No. Lookup Quota"; | ||
| begin | ||
| if not VATRegNoLookupQuota.Get() then begin | ||
| VATRegNoLookupQuota.Init(); | ||
| VATRegNoLookupQuota."Primary Key" := ''; | ||
| VATRegNoLookupQuota.Insert(); | ||
| end; | ||
| VATRegNoLookupQuota."Window Date" := WindowDate; | ||
| VATRegNoLookupQuota."Daily Call Count" := CallCount; | ||
| VATRegNoLookupQuota.Modify(); | ||
| end; | ||
|
|
||
| internal procedure GetVIESCallCountForTest(): Integer | ||
| var | ||
| VATRegNoLookupQuota: Record "VAT Reg. No. Lookup Quota"; | ||
| begin | ||
| if not VATRegNoLookupQuota.Get() then | ||
| exit(0); | ||
| if VATRegNoLookupQuota."Window Date" <> Today() then | ||
| exit(0); | ||
| exit(VATRegNoLookupQuota."Daily Call Count"); | ||
| end; | ||
|
|
||
| internal procedure ClearVIESCallQuotaForTest() | ||
| var | ||
| VATRegNoLookupQuota: Record "VAT Reg. No. Lookup Quota"; | ||
| begin | ||
| if VATRegNoLookupQuota.Get() then | ||
| VATRegNoLookupQuota.Delete(); | ||
| end; | ||
| } | ||
50 changes: 50 additions & 0 deletions
50
src/Layers/W1/BaseApp/Finance/VAT/Registration/VATRegNoLookupQuota.Table.al
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| // ------------------------------------------------------------------------------------------------ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. See License.txt in the project root for license information. | ||
| // ------------------------------------------------------------------------------------------------ | ||
| namespace Microsoft.Finance.VAT.Registration; | ||
|
|
||
| /// <summary> | ||
| /// Per-environment counter that tracks the number of EU VIES VAT registration number lookups performed per day. | ||
| /// Used to cap the daily lookup volume per environment (all companies in the database share one counter) so a | ||
| /// single environment cannot flood the shared, unauthenticated VIES service and get the shared outbound IP address | ||
| /// deny-listed. Holds a single row that is locked for the brief read-modify-write, so concurrent sessions | ||
| /// increment it atomically. | ||
| /// </summary> | ||
| table 243 "VAT Reg. No. Lookup Quota" | ||
| { | ||
| Access = Internal; | ||
| DataPerCompany = false; | ||
| DataClassification = SystemMetadata; | ||
| ReplicateData = false; | ||
| InherentEntitlements = RIMDX; | ||
| InherentPermissions = RIMDX; | ||
|
|
||
| fields | ||
| { | ||
| field(1; "Primary Key"; Code[10]) | ||
| { | ||
| Caption = 'Primary Key'; | ||
| DataClassification = SystemMetadata; | ||
| } | ||
| field(2; "Window Date"; Date) | ||
| { | ||
| Caption = 'Window Date'; | ||
| DataClassification = SystemMetadata; | ||
| } | ||
| field(3; "Daily Call Count"; Integer) | ||
| { | ||
| Caption = 'Daily Call Count'; | ||
| DataClassification = SystemMetadata; | ||
| MinValue = 0; | ||
| } | ||
| } | ||
|
|
||
| keys | ||
| { | ||
| key(PK; "Primary Key") | ||
| { | ||
| Clustered = true; | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.