mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-06 17:36:53 +01:00
Correct security and privacy knowledge guidance (#92)
* Correct security and privacy guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9c2eebc4-dcd5-4b85-8113-90772d818900 * Address security privacy review findings Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9c2eebc4-dcd5-4b85-8113-90772d818900 --------- Co-authored-by: Jesper Schulz-Wedde <jesper.schulzwedde@microsoft.com>
This commit is contained in:
parent
bfda67a95a
commit
aca3986fd0
34 changed files with 192 additions and 153 deletions
|
|
@ -2,10 +2,16 @@ codeunit 50207 "Privacy Sample StrSubstNo Bad"
|
|||
{
|
||||
procedure ReportFailure(var Customer: Record Customer)
|
||||
var
|
||||
ErrorMsg: Text;
|
||||
CustomerInvalidErr: Label 'Customer %1 has invalid data.', Comment = '%1 = Customer No.';
|
||||
begin
|
||||
ErrorMsg := StrSubstNo('Customer %1 (%2) at %3 has invalid data',
|
||||
Customer.Name, Customer."E-Mail", Customer.Address);
|
||||
Error(ErrorMsg);
|
||||
Error(StrSubstNo(CustomerInvalidErr, Customer."No."));
|
||||
end;
|
||||
|
||||
procedure ReportCombinedFailure()
|
||||
var
|
||||
HeaderErr: Label 'Customer validation failed. ';
|
||||
DetailErr: Label 'Correct the customer card and try again.';
|
||||
begin
|
||||
Error(HeaderErr + DetailErr);
|
||||
end;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
bc-version: [20..]
|
||||
domain: privacy
|
||||
keywords: [strsubstno, error, telemetry, pii, prebuild, text-variable]
|
||||
technologies: [al]
|
||||
|
|
@ -7,20 +7,20 @@ countries: [w1]
|
|||
application-area: [all]
|
||||
---
|
||||
|
||||
# Do not pre-build an error string with `StrSubstNo` before calling `Error()`
|
||||
# Pass a Label directly as the first Error argument
|
||||
|
||||
## Description
|
||||
|
||||
`StrSubstNo` returns a plain `Text` value with the substitutions already performed. When that result is then passed to `Error()`, the platform sees a single plain-text parameter with no field references left to inspect, so it cannot apply `DataClassification` to anything inside it. Whatever PII the `StrSubstNo` call interpolated — customer name, e-mail, address, error text — is logged verbatim to telemetry. This is the canonical way to accidentally leak customer data through error telemetry, and it is the only `Error()` shape that needs to be flagged.
|
||||
Error method trace telemetry includes the AL error string only when the first `Error` argument is a `Label` or `TextConst`. Wrapping a label in `StrSubstNo`, or concatenating labels or text, produces a dynamic `Text` first argument. In that case the actual string is not emitted as the telemetry message; the platform emits its generic guidance instead. CodeCop AA0231 flags both shapes because the label identity and data-classification context are lost.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Call `Error()` directly with the format string and the substitution parameters. The platform classifies each parameter individually and handles telemetry correctly even when the parameters are PII fields (see `error-direct-substitution-safe-for-telemetry.md`). If the message text needs to be a `Label`, pass the `Label` and the parameters to `Error()` — do not pre-render via `StrSubstNo`.
|
||||
Declare the complete message as a `Label` or `TextConst` and pass it directly to `Error`, followed by substitution values. The client receives the formatted message while telemetry can retain the static message template without using the dynamic values as its message. See `error-direct-substitution-safe-for-telemetry.md`.
|
||||
|
||||
See sample: `avoid-strsubstno-prebuild-before-error.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Assigning `StrSubstNo('Customer %1 (%2) ...', Customer.Name, Customer."E-Mail")` to a `Text` variable and then calling `Error(ErrorMsg)`. The platform has nothing to classify by the time `Error` runs — the PII is baked into the string and goes straight to telemetry. Detection signal for a reviewer: any `Text` variable assigned from `StrSubstNo` and later passed as the *only* parameter to `Error()`.
|
||||
`Error(StrSubstNo(CustomerInvalidErr, Customer."No."))` and `Error(HeaderErr + DetailErr)` both make the first argument dynamic. They reduce error telemetry quality; they do not cause that composed string to be logged verbatim as the telemetry message.
|
||||
|
||||
See sample: `avoid-strsubstno-prebuild-before-error.bad.al`.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
bc-version: [20..]
|
||||
domain: privacy
|
||||
keywords: [error, strsubstno, direct-substitution, telemetry, classification, label]
|
||||
technologies: [al]
|
||||
|
|
@ -7,18 +7,18 @@ countries: [w1]
|
|||
application-area: [all]
|
||||
---
|
||||
|
||||
# `Error()` with direct substitution parameters is always safe for telemetry
|
||||
# Use a Label or TextConst for the Error telemetry message
|
||||
|
||||
## Description
|
||||
|
||||
When `Error()` is called with a format string and direct substitution parameters (`%1`, `%2`, …), the BC platform intercepts the call, inspects each parameter individually, and applies the `DataClassification` of the source field — stripping or masking sensitive data before writing the message to telemetry. This is true regardless of whether a parameter is a record field reference, a local variable, a function return value, or any other expression. Patterns such as `Error('Invalid email: %1', Customer."E-Mail")` are therefore safe even when the parameter is PII: the platform sees `Customer."E-Mail"` as a `CustomerContent` field reference and handles it correctly.
|
||||
For Error method trace telemetry, the platform includes the AL error string only when `Error` receives a `Label` or `TextConst` as its first argument. Substitution values format the client message, but the static label supplies the telemetry message and preserves its classification context. A string literal, local `Text`, `StrSubstNo` result, or concatenation is not equivalent: telemetry substitutes generic guidance instead of that dynamic string.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Pass values to `Error()` as direct substitution parameters — either inline or via a `Label` with `Comment = '%1 = …'` placeholders. Let the platform do the per-parameter classification. This works equally well for record fields, local text variables, and document IDs.
|
||||
Define the complete error template as a `Label` with placeholder comments, pass the label directly as the first argument, and pass values separately. Independently review whether those values are appropriate to show to the current user.
|
||||
|
||||
See sample: `error-direct-substitution-safe-for-telemetry.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Treating any `Error()` call that mentions PII as a leak. A review skill that flags `Error('Invalid email: %1', EmailAddress)` is wrong; the platform handles that pattern correctly. The only `Error()` shape that genuinely leaks PII to telemetry is the pre-built `StrSubstNo` form covered in `avoid-strsubstno-prebuild-before-error.md`.
|
||||
Assuming that any direct format string is telemetry-safe, or that a `StrSubstNo`/concatenated first argument is logged verbatim. The required telemetry shape is specifically a directly supplied `Label` or `TextConst`; see `avoid-strsubstno-prebuild-before-error.md`.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
bc-version: [20..]
|
||||
domain: privacy
|
||||
keywords: [error, message, confirm, notification, telemetry, logging, ui-dialog]
|
||||
technologies: [al]
|
||||
|
|
@ -7,16 +7,16 @@ countries: [w1]
|
|||
application-area: [all]
|
||||
---
|
||||
|
||||
# Only `Error()` is logged to telemetry — `Message`, `Confirm`, `Notification` are not
|
||||
# Error dialogs emit Error method trace telemetry
|
||||
|
||||
## Description
|
||||
|
||||
The privacy concern with dialog APIs is not what the signed-in user sees on the screen — it is what the platform writes to telemetry. The BC platform automatically captures `Error()` invocations in the telemetry stream; it does not capture `Message()`, `Confirm()` or `Notification` calls. That asymmetry is the reason privacy review focuses on `Error()` text and ignores the other dialog APIs: a `Message` that shows a customer's email to the signed-in user reveals nothing they were not already entitled to see, while an `Error` carrying the same email leaks it to a separate, longer-lived telemetry destination.
|
||||
When `Error` displays a dialog, Business Central emits the RT0030 Error method trace telemetry signal. `Message`, `Confirm`, and `Notification` do not emit that Error method trace signal. For RT0030, the actual AL error string is included only when the first `Error` argument is a `Label` or `TextConst`; other first-argument types produce generic guidance instead of the dynamic string.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Treat `Error()` as a telemetry surface, not just a UI surface — review the message text and parameters with the same scrutiny you apply to `Session.LogMessage`. Treat `Message()`, `Confirm()`, and `Notification` as pure UI: showing business data the user is permissioned for is normal functionality.
|
||||
Use a `Label` or `TextConst` as the direct first argument to `Error` so telemetry contains a stable, classified message. Review user-facing substitution values for UI appropriateness. Do not treat `Message`, `Confirm`, or `Notification` content as though it were automatically copied into RT0030.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Flagging `Message`/`Confirm`/`Notification` calls for "showing PII" — they are not logged to telemetry, and the user already has permission to the underlying data. The inverse anti-pattern is treating `Error()` as harmless because the user sees only a dialog: the message is also written verbatim to telemetry.
|
||||
Claiming that every rendered `Error` string is written verbatim to telemetry, or that `Message`, `Confirm`, and `Notification` automatically feed the Error method trace. Both overstate the platform behavior.
|
||||
|
|
|
|||
|
|
@ -2,12 +2,18 @@ codeunit 50209 "Privacy Sample GetLastError Bad"
|
|||
{
|
||||
procedure AddAttachment()
|
||||
var
|
||||
ErrorMsg: Text;
|
||||
AttachmentFailedErr: Label 'Attachment failed: %1', Comment = '%1 = underlying error';
|
||||
begin
|
||||
if not TryAddAttachment() then begin
|
||||
ErrorMsg := StrSubstNo('Attachment failed: %1', GetLastErrorText(true));
|
||||
Error(ErrorMsg);
|
||||
end;
|
||||
if not TryAddAttachment() then
|
||||
Error(StrSubstNo(AttachmentFailedErr, GetLastErrorText()));
|
||||
end;
|
||||
|
||||
procedure AddAttachmentWithConcatenation()
|
||||
var
|
||||
AttachmentFailedErr: Label 'Attachment failed: ';
|
||||
begin
|
||||
if not TryAddAttachment() then
|
||||
Error(AttachmentFailedErr + GetLastErrorText());
|
||||
end;
|
||||
|
||||
[TryFunction]
|
||||
|
|
|
|||
|
|
@ -2,15 +2,15 @@ codeunit 50208 "Privacy Sample GetLastError Good"
|
|||
{
|
||||
procedure AddAttachmentSafely()
|
||||
var
|
||||
AttachmentFailedErr: Label 'Failed to add email attachment. Please try again.';
|
||||
AttachmentFailedErr: Label 'Failed to add the attachment: %1', Comment = '%1 = underlying error shown to the user';
|
||||
begin
|
||||
if not TryAddAttachment() then
|
||||
Error(AttachmentFailedErr);
|
||||
Error(AttachmentFailedErr, GetLastErrorText());
|
||||
end;
|
||||
|
||||
[TryFunction]
|
||||
local procedure TryAddAttachment()
|
||||
begin
|
||||
// ... attachment logic that may fail with a customer-data-bearing error ...
|
||||
// Attachment logic that can fail with a customer-data-bearing error.
|
||||
end;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
bc-version: [20..]
|
||||
domain: privacy
|
||||
keywords: [getlasterrortext, error, strsubstno, telemetry, customer-data, attachment]
|
||||
technologies: [al]
|
||||
|
|
@ -11,16 +11,16 @@ application-area: [all]
|
|||
|
||||
## Description
|
||||
|
||||
`GetLastErrorText()` returns the text of the last error that occurred in the context where it is called. That text routinely contains customer content — field values that triggered the validation, record keys, customer names, file names from upload failures, and similar fragments lifted from the failing operation. Re-emitting it through `StrSubstNo` into `Error()` bakes that customer data into a single plain-text parameter that the platform can no longer classify, so it is logged verbatim to telemetry (the same problem as any other `StrSubstNo`-pre-built error — see `avoid-strsubstno-prebuild-before-error.md`).
|
||||
Parameterless `GetLastErrorText()` can contain customer content such as field values, record keys, and file names. The Boolean overload names its parameter `ExcludeCustomerContent`; passing `true` requests scrubbed text and is not the customer-content scenario covered here. When unsanitized error text is passed as a substitution value to an `Error` whose first argument is a `Label` or `TextConst`, the label supplies the Error method trace telemetry message.
|
||||
|
||||
## Best Practice
|
||||
|
||||
When the goal is to surface a recoverable failure to the user, raise a generic message that does not embed `GetLastErrorText()` content, and log technical detail separately via `Session.LogMessage` with the correct `DataClassification`. If you must propagate the inner error verbatim, re-raise it as a direct parameter of `Error()` (e.g., `Error('%1', GetLastErrorText())`) rather than concatenating with `StrSubstNo` so the platform can apply its own handling.
|
||||
Use a generic label when the user does not need the underlying detail. If showing unsanitized detail is appropriate, put `%1` in a label and pass parameterless `GetLastErrorText()` as a separate argument. This preserves a useful static telemetry message while keeping the dynamic value out of the telemetry message field.
|
||||
|
||||
See sample: `getlasterrortext-customer-content-in-errors.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`ErrorMsg := StrSubstNo('Attachment failed: %1', GetLastErrorText(true)); Error(ErrorMsg);` — the inner error text may carry filenames or record values, and `StrSubstNo` strips the platform's ability to filter them before they hit telemetry.
|
||||
`Error(StrSubstNo(AttachmentFailedErr, GetLastErrorText()))` or `Error(AttachmentPrefixErr + GetLastErrorText())`. Both lose the static first argument and trigger AA0231; neither causes the composed text to be logged verbatim as the Error telemetry message.
|
||||
|
||||
See sample: `getlasterrortext-customer-content-in-errors.bad.al`.
|
||||
|
|
|
|||
|
|
@ -4,10 +4,14 @@ codeunit 50217 "Privacy Sample Consent Bad"
|
|||
var
|
||||
HttpClient: HttpClient;
|
||||
Content: HttpContent;
|
||||
Payload: JsonObject;
|
||||
PayloadText: Text;
|
||||
Response: HttpResponseMessage;
|
||||
begin
|
||||
Content.WriteFrom(StrSubstNo('{"email":"%1","name":"%2"}',
|
||||
Customer."E-Mail", Customer.Name));
|
||||
Payload.Add('email', Customer."E-Mail");
|
||||
Payload.Add('name', Customer.Name);
|
||||
Payload.WriteTo(PayloadText);
|
||||
Content.WriteFrom(PayloadText);
|
||||
HttpClient.Post('https://api.externalservice.com/sync', Content, Response);
|
||||
end;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,35 @@
|
|||
codeunit 50216 "Privacy Sample Consent Good"
|
||||
{
|
||||
var
|
||||
ExternalSyncNoticeIdLbl: Label 'CONTOSO-EXTERNAL-SYNC', Locked = true;
|
||||
ExternalSyncNameLbl: Label 'Contoso External Sync', Locked = true;
|
||||
PrivacyTermsUrlLbl: Label 'https://contoso.example/privacy', Locked = true;
|
||||
|
||||
internal procedure RegisterPrivacyNotice()
|
||||
var
|
||||
PrivacyNotice: Codeunit "Privacy Notice";
|
||||
begin
|
||||
PrivacyNotice.CreatePrivacyNotice(
|
||||
ExternalSyncNoticeIdLbl, ExternalSyncNameLbl, PrivacyTermsUrlLbl);
|
||||
end;
|
||||
|
||||
procedure SendDataToExternalService(Customer: Record Customer)
|
||||
var
|
||||
PrivacyNotice: Codeunit "Privacy Notice";
|
||||
PrivacyNoticeRegistrations: Codeunit "Privacy Notice Registrations";
|
||||
HttpClient: HttpClient;
|
||||
Content: HttpContent;
|
||||
Payload: JsonObject;
|
||||
PayloadText: Text;
|
||||
Response: HttpResponseMessage;
|
||||
PrivacyConsentRequiredErr: Label 'Privacy notice consent is required for this integration.';
|
||||
begin
|
||||
if PrivacyNotice.GetPrivacyNoticeApprovalState(
|
||||
PrivacyNoticeRegistrations.GetExchangePrivacyNoticeId())
|
||||
<> "Privacy Notice Approval State"::Agreed
|
||||
then
|
||||
if not PrivacyNotice.ConfirmPrivacyNoticeApproval(ExternalSyncNoticeIdLbl) then
|
||||
Error(PrivacyConsentRequiredErr);
|
||||
|
||||
Content.WriteFrom(StrSubstNo('{"email":"%1","name":"%2"}',
|
||||
Customer."E-Mail", Customer.Name));
|
||||
Payload.Add('email', Customer."E-Mail");
|
||||
Payload.Add('name', Customer.Name);
|
||||
Payload.WriteTo(PayloadText);
|
||||
Content.WriteFrom(PayloadText);
|
||||
HttpClient.Post('https://api.externalservice.com/sync', Content, Response);
|
||||
end;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: privacy
|
||||
keywords: [privacy-notice, consent, http-client, outgoing-request, external-service, getprivacynoticeapprovalstate]
|
||||
keywords: [privacy-notice, consent, http-client, outgoing-request, external-service, confirmprivacynoticeapproval]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Outgoing requests to external services require a Privacy Notice consent check
|
||||
# Check the custom Privacy Notice before external data transfer
|
||||
|
||||
## Description
|
||||
|
||||
Business Central ships a built-in Privacy Notice framework that the admin uses to grant or withhold per-integration consent for sending data to external services. The relevant API surface is `Codeunit "Privacy Notice"` (consent checks via `GetPrivacyNoticeApprovalState()`), `Codeunit "Privacy Notice Registrations"` (well-known notice IDs for integrations such as Exchange, OneDrive, Teams), and the `Enum "Privacy Notice Approval State"` with values `Agreed`, `Disagreed`, and `Not Set`. The admin UI is the **Privacy Notices Status** page. The compliance concern in code review is therefore not that personal data is included in an outgoing HTTP body — that is normal business functionality — but that the code path issuing the request contains no `PrivacyNotice.GetPrivacyNoticeApprovalState(...)` check.
|
||||
Business Central's `Codeunit "Privacy Notice"` creates notices and records per-integration approval. A custom integration needs its own stable notice ID; it must not borrow the Exchange or another built-in service's consent. `ConfirmPrivacyNoticeApproval` shows the notice when needed and returns whether the request is approved. `GetPrivacyNoticeApprovalState` checks an existing notice without showing UI.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Before issuing an outgoing HTTP request to an external service, verify `PrivacyNotice.GetPrivacyNoticeApprovalState(<integration id>) = "Privacy Notice Approval State"::Agreed`. The check does not have to live next to the `HttpClient.Post` call — it can sit anywhere upstream in the same code path (for example in the page's `OnOpenPage`, in a wizard step, or in a setup action) as long as no execution path reaches the request without passing through it.
|
||||
Register the custom notice with `CreatePrivacyNotice` during setup or through `OnRegisterPrivacyNotices`. Before sending data, call `ConfirmPrivacyNoticeApproval(<custom id>)` outside a write transaction, or check `GetPrivacyNoticeApprovalState(<custom id>)` when the flow must not show UI. No path should issue the request without approval.
|
||||
|
||||
See sample: `privacy-notice-consent-for-external-data-transfer.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
A `procedure SendDataToExternalService(...)` that posts customer data to an external endpoint with no `PrivacyNotice.GetPrivacyNoticeApprovalState` anywhere upstream. The same anti-pattern applies in reverse: removing an existing privacy-notice check from code that still issues the external call.
|
||||
A custom integration that posts data without checking its own notice, or that gates the call with a built-in ID such as the Exchange privacy notice ID. Consent for one service does not authorize another.
|
||||
|
||||
See sample: `privacy-notice-consent-for-external-data-transfer.bad.al`.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
codeunit 50218 "Privacy Sample Register Integration"
|
||||
{
|
||||
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Privacy Notice Registrations", 'OnRegisterPrivacyNotices', '', false, false)]
|
||||
local procedure OnRegisterPrivacyNotices(var TempPrivacyNotice: Record "Privacy Notice" temporary)
|
||||
var
|
||||
PrivacyNotice: Codeunit "Privacy Notice";
|
||||
ExternalSyncNoticeIdLbl: Label 'CONTOSO-EXTERNAL-SYNC', Locked = true;
|
||||
ExternalSyncNameLbl: Label 'Contoso External Sync', Locked = true;
|
||||
PrivacyTermsUrlLbl: Label 'https://contoso.example/privacy', Locked = true;
|
||||
|
||||
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Privacy Notice", 'OnRegisterPrivacyNotices', '', false, false)]
|
||||
local procedure OnRegisterPrivacyNotices(var TempPrivacyNotice: Record "Privacy Notice" temporary)
|
||||
begin
|
||||
PrivacyNotice.CreatePrivacyNoticeForIntegration(
|
||||
'My External Sync', 'External Customer Sync Service');
|
||||
TempPrivacyNotice.Init();
|
||||
TempPrivacyNotice.ID := ExternalSyncNoticeIdLbl;
|
||||
TempPrivacyNotice."Integration Service Name" := ExternalSyncNameLbl;
|
||||
TempPrivacyNotice.Link := PrivacyTermsUrlLbl;
|
||||
if not TempPrivacyNotice.Insert() then;
|
||||
end;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: privacy
|
||||
keywords: [privacy-notice-registrations, integration, register, exchange, onedrive, teams, notice-id]
|
||||
keywords: [privacy-notice, integration, register, onregisterprivacynotices, notice-id]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Register every new external integration with `Privacy Notice Registrations`
|
||||
# Register custom integrations with Codeunit Privacy Notice
|
||||
|
||||
## Description
|
||||
|
||||
`Codeunit "Privacy Notice Registrations"` is the registry of integrations whose consent state the platform tracks. Built-in integrations such as Exchange, OneDrive and Teams already have notice IDs exposed via accessor methods on this codeunit (`GetExchangePrivacyNoticeId`, etc.); a new integration introduced by an extension must add itself to the registry so that the admin can grant or withhold consent on the **Privacy Notices Status** page. Without registration, there is nothing for `Codeunit "Privacy Notice"` to return an approval state for — the call cannot meaningfully gate the outbound request.
|
||||
The current extension point is `Codeunit "Privacy Notice"`. Extensions can subscribe to its `OnRegisterPrivacyNotices` event and add a dedicated notice ID, integration name, and link to the temporary `Privacy Notice` record. For explicit creation outside the default-registration flow, the same codeunit exposes `CreatePrivacyNotice`. `Codeunit "Privacy Notice Registrations"` contains IDs for built-in integrations and is not the registration API for a custom service.
|
||||
|
||||
## Best Practice
|
||||
|
||||
When introducing a new outbound integration: pick a stable notice ID, register it via `Privacy Notice Registrations`, and then gate every outbound call with `PrivacyNotice.GetPrivacyNoticeApprovalState(<that id>)` as described in `privacy-notice-consent-for-external-data-transfer.md`.
|
||||
Choose a stable ID owned by the extension. Register it through `OnRegisterPrivacyNotices`, or call `PrivacyNotice.CreatePrivacyNotice` during an intentional setup or upgrade path. Use that same ID for consent checks described in `privacy-notice-consent-for-external-data-transfer.md`.
|
||||
|
||||
See sample: `register-integration-in-privacy-notice-registrations.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Shipping a new outbound integration without registering it. Even if the code calls `GetPrivacyNoticeApprovalState`, the admin has no surface to express consent — the integration is effectively unmanaged from a privacy-notice standpoint.
|
||||
Reusing the Exchange or another built-in notice ID for a custom integration, subscribing to `Privacy Notice Registrations`, or calling the nonexistent `CreatePrivacyNoticeForIntegration` method. These shapes attach consent to the wrong service or do not compile.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,19 @@
|
|||
table 50202 "System Configuration Log"
|
||||
{
|
||||
DataClassification = SystemMetadata;
|
||||
|
||||
fields
|
||||
{
|
||||
field(1; "Entry No."; Integer) { }
|
||||
field(2; "Changed By"; Code[50]) { }
|
||||
field(3; "Change Description"; Text[250]) { }
|
||||
field(1; "Entry No."; Integer)
|
||||
{
|
||||
DataClassification = SystemMetadata;
|
||||
}
|
||||
field(2; "Changed By"; Code[50])
|
||||
{
|
||||
DataClassification = EndUserIdentifiableInformation;
|
||||
}
|
||||
field(3; "Change Description"; Text[250])
|
||||
{
|
||||
DataClassification = CustomerContent;
|
||||
}
|
||||
}
|
||||
|
||||
keys
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: privacy
|
||||
keywords: [data-classification, table-level, inheritance, override, cascading]
|
||||
keywords: [data-classification, table-level, normal-field, appsourcecop, as0016]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# Table-level DataClassification cascades to every field unless overridden
|
||||
# Set DataClassification on every Normal table field
|
||||
|
||||
## Description
|
||||
|
||||
`DataClassification` may be set at the table level. When it is, every field in the table inherits that classification and individual fields do not need their own `DataClassification` property. The cascade is the platform's intended way of classifying tables whose fields are homogeneous — for example, a system configuration log whose every column is `SystemMetadata`. A field only needs its own classification when its content genuinely differs from the table's default and the inherited value would be wrong.
|
||||
AppSourceCop AS0016 requires every field whose `FieldClass` is `Normal` to declare `DataClassification` and use a value other than `ToBeClassified`. A table-level `DataClassification` property does not satisfy that field-level requirement. FlowFields and FlowFilters are handled separately by the platform and are covered by `flowfield-flowfilter-classification-systemmetadata.md`.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Set `DataClassification` once at the table level whenever every field in the table shares the same classification. Omit field-level `DataClassification` properties in that case. Override only on the specific fields whose data class differs from the table's — for example, a `SystemMetadata` audit table that nonetheless captures a `CustomerContent` value somewhere.
|
||||
Classify each Normal field according to the data it stores, even when every field in the table has the same classification. Repeat the property explicitly so AS0016 can verify every field.
|
||||
|
||||
See sample: `table-level-data-classification-cascades.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Flagging individual fields for "missing `DataClassification`" when the table declares one — the inheritance is the correct, intentional pattern. The mirror anti-pattern is repeating the same `DataClassification` on every field of a table that already declares it at the table level; the property is redundant and adds nothing the platform did not already know.
|
||||
Relying on `DataClassification` at table scope and leaving Normal fields unclassified. The table property does not cascade in the way AS0016 requires, so the fields still fail AppSourceCop validation.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue