mirror of
https://github.com/microsoft/BCQuality.git
synced 2026-08-06 17:36:53 +01:00
Drop the two new KB articles from this PR
The Execution-discipline change + suggested-code propagation in the skills is the structural fix. The two knowledge articles (case-must-handle-unknown-enum-values, instream-length-unreliable-for-bc-streams) were T4 follow-ups derived from a single parity case study; they need broader review before landing as canonical BCQuality knowledge and are out of scope for this PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
31b9949235
commit
539be9d735
6 changed files with 0 additions and 202 deletions
|
|
@ -1,29 +0,0 @@
|
|||
codeunit 50272 "Sample InStream Length Bad"
|
||||
{
|
||||
var
|
||||
MaxSimpleUploadSize: Integer;
|
||||
|
||||
procedure Upload(var Stream: InStream; FileName: Text)
|
||||
var
|
||||
SimpleResp: HttpResponseMessage;
|
||||
ChunkedResp: HttpResponseMessage;
|
||||
begin
|
||||
MaxSimpleUploadSize := 4 * 1024 * 1024;
|
||||
// Wrong: Stream.Length is 0 or unreliable for streams from HTTP
|
||||
// responses, some file APIs, and caller-supplied streams.
|
||||
if Stream.Length <= MaxSimpleUploadSize then
|
||||
UploadSimple(Stream, FileName, SimpleResp)
|
||||
else
|
||||
UploadChunked(Stream, FileName, ChunkedResp);
|
||||
end;
|
||||
|
||||
local procedure UploadSimple(var Stream: InStream; FileName: Text; var Response: HttpResponseMessage)
|
||||
begin
|
||||
// ... PUT to /items/{id}/content endpoint
|
||||
end;
|
||||
|
||||
local procedure UploadChunked(var Stream: InStream; FileName: Text; var Response: HttpResponseMessage)
|
||||
begin
|
||||
// ... POST to /items/{id}/createUploadSession endpoint, then PUT in chunks
|
||||
end;
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
codeunit 50273 "Sample InStream Length Good"
|
||||
{
|
||||
var
|
||||
MaxSimpleUploadSize: Integer;
|
||||
|
||||
procedure Upload(var Stream: InStream; FileName: Text)
|
||||
var
|
||||
TempBlob: Codeunit "Temp Blob";
|
||||
SizedStream: InStream;
|
||||
BufferLength: Integer;
|
||||
SimpleResp: HttpResponseMessage;
|
||||
ChunkedResp: HttpResponseMessage;
|
||||
begin
|
||||
MaxSimpleUploadSize := 4 * 1024 * 1024;
|
||||
// Materialise once into a Temp Blob; its length is reliable.
|
||||
CopyStream(TempBlob.CreateOutStream(), Stream);
|
||||
BufferLength := TempBlob.Length();
|
||||
TempBlob.CreateInStream(SizedStream);
|
||||
|
||||
if (BufferLength > 0) and (BufferLength <= MaxSimpleUploadSize) then
|
||||
UploadSimple(SizedStream, FileName, SimpleResp)
|
||||
else
|
||||
UploadChunked(SizedStream, FileName, ChunkedResp);
|
||||
end;
|
||||
|
||||
local procedure UploadSimple(var Stream: InStream; FileName: Text; var Response: HttpResponseMessage)
|
||||
begin
|
||||
// ... PUT to /items/{id}/content endpoint
|
||||
end;
|
||||
|
||||
local procedure UploadChunked(var Stream: InStream; FileName: Text; var Response: HttpResponseMessage)
|
||||
begin
|
||||
// ... POST to /items/{id}/createUploadSession endpoint, then PUT in chunks
|
||||
end;
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: performance
|
||||
keywords: [instream, outstream, length, stream, upload, blob, http, chunk]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# `InStream.Length` is unreliable for branching on payload size
|
||||
|
||||
## Description
|
||||
|
||||
`InStream` exposes a `Length` property that returns the total byte count of the underlying buffer when the runtime can determine it. The catch is that "when the runtime can determine it" depends on **how the stream was obtained**:
|
||||
|
||||
- Streams produced from a `Blob` or a `Temp Blob` field by `CreateInStream` — `Length` is reliable; the blob is fully materialised.
|
||||
- Streams produced from a Media or MediaSet field — same: backed by a known-size payload.
|
||||
- Streams produced from `HttpResponseMessage.Content.ReadAs` and from many `File.*` APIs — `Length` may return `0` or a partial value, because the underlying transport is consumed incrementally and the total length is not known until the stream is exhausted.
|
||||
- Streams from `Stream` parameters supplied by callers — depends entirely on what the caller passed in.
|
||||
|
||||
Branching upload behaviour on `Stream.Length` is the common failure mode. The pattern is:
|
||||
|
||||
```al
|
||||
if Stream.Length <= MaxSimpleUploadSize then
|
||||
UploadSimple(Stream)
|
||||
else
|
||||
UploadChunked(Stream);
|
||||
```
|
||||
|
||||
For a Microsoft Graph drive upload, `MaxSimpleUploadSize` is 4 MB. If `Stream.Length` returns `0` (because the stream came from an HTTP response or a freshly written outstream that the runtime cannot size cheaply), the code takes the simple-upload path with a 10 MB file behind it, the API returns `413 Payload Too Large`, and the upload fails. The error surfaces to the user as a generic HTTP failure with no obvious connection to the buggy size check.
|
||||
|
||||
The same trap applies to any code that "skips the work if the stream is empty": `if Stream.Length = 0 then exit;` silently drops payloads when the stream came from a source that does not pre-compute length.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Decide which behaviour you actually need.
|
||||
|
||||
- **Always-chunked** is the safe default when the stream's origin is not under your control. Chunked uploads work for any payload size; the per-chunk overhead is small for small payloads.
|
||||
- When a size threshold is genuinely required (for example, choosing between two endpoints with different cost profiles), copy the stream into a known-size buffer first — typically a `Temp Blob` — and read length from the blob, which IS reliable. The cost of one round-trip through a blob is acceptable for the upload-routing decision.
|
||||
- When the threshold is informational (logging, telemetry), guard against `0`: `if (Stream.Length > 0) and (Stream.Length <= Threshold)` so an unknown size routes to the safe path, not the optimistic one.
|
||||
|
||||
See sample: `instream-length-unreliable-for-bc-streams.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
Branching upload size, validation, or buffer allocation directly on `Stream.Length` when the stream's origin is anything other than a freshly-materialised blob. Detection signal: `Stream.Length` (or `.Length` on a variable typed `InStream` or `OutStream`) appearing as the left or right side of `<=`, `<`, `>=`, `>`, or `=` against a size-like constant or `Label`, with no prior copy through a `Blob`. The narrower signal — branching simple-vs-chunked upload on `Length` for Graph or REST endpoints with a documented size cap — is the high-confidence case.
|
||||
|
||||
See sample: `instream-length-unreliable-for-bc-streams.bad.al`.
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
codeunit 50270 "Sample Case No Else Bad"
|
||||
{
|
||||
procedure InitializeGraphClient(var SharePointAccount: Record "Ext. SharePoint Account"; var GraphAuthInterface: Interface "Graph Auth Interface")
|
||||
var
|
||||
GraphAuthClientCredentials: Codeunit "Graph Auth Client Credentials";
|
||||
GraphAuthCertificate: Codeunit "Graph Auth Certificate";
|
||||
begin
|
||||
case SharePointAccount."Authentication Type" of
|
||||
SharePointAccount."Authentication Type"::"Client Secret":
|
||||
GraphAuthInterface := GraphAuthClientCredentials;
|
||||
SharePointAccount."Authentication Type"::Certificate:
|
||||
GraphAuthInterface := GraphAuthCertificate;
|
||||
end;
|
||||
end;
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
codeunit 50271 "Sample Case With Else Good"
|
||||
{
|
||||
var
|
||||
UnsupportedAuthTypeErr: Label 'Authentication type %1 is not supported.', Comment = '%1 = Authentication Type value';
|
||||
|
||||
procedure InitializeGraphClient(var SharePointAccount: Record "Ext. SharePoint Account"; var GraphAuthInterface: Interface "Graph Auth Interface")
|
||||
var
|
||||
GraphAuthClientCredentials: Codeunit "Graph Auth Client Credentials";
|
||||
GraphAuthCertificate: Codeunit "Graph Auth Certificate";
|
||||
begin
|
||||
case SharePointAccount."Authentication Type" of
|
||||
SharePointAccount."Authentication Type"::"Client Secret":
|
||||
GraphAuthInterface := GraphAuthClientCredentials;
|
||||
SharePointAccount."Authentication Type"::Certificate:
|
||||
GraphAuthInterface := GraphAuthCertificate;
|
||||
else
|
||||
Error(UnsupportedAuthTypeErr, SharePointAccount."Authentication Type");
|
||||
end;
|
||||
end;
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
---
|
||||
bc-version: [all]
|
||||
domain: security
|
||||
keywords: [case, else, enum, fallthrough, authentication, authorization, default, switch]
|
||||
technologies: [al]
|
||||
countries: [w1]
|
||||
application-area: [all]
|
||||
---
|
||||
|
||||
# `case` over an enum must handle unknown values via `else`
|
||||
|
||||
## Description
|
||||
|
||||
A `case` statement that branches on an enum value and lists only the values the author knows about silently falls through when the runtime value is one the code does not name. For business-logic enums, falling through usually means "do nothing"; for security-relevant enums — authentication type, authorization mode, identity provider, encryption strategy, permission scope — falling through means **the code path that was supposed to set up the security context never runs, and the operation proceeds with whatever state the variables had before the `case`**.
|
||||
|
||||
A canonical example, lifted from real review traffic:
|
||||
|
||||
```al
|
||||
case SharePointAccount."Authentication Type" of
|
||||
SharePointAccount."Authentication Type"::"Client Secret":
|
||||
GraphAuthInterface := GraphAuthClientCredentials;
|
||||
SharePointAccount."Authentication Type"::Certificate:
|
||||
GraphAuthInterface := GraphAuthCertificate;
|
||||
end;
|
||||
GraphClient.Initialize(GraphAuthInterface);
|
||||
```
|
||||
|
||||
If a new authentication type is added to the enum, or if a database row carries a value the deployed code does not yet handle, `GraphAuthInterface` is whatever the previous caller left in it (or default-initialised), and the client initialises against an unauthenticated or wrongly-authenticated context. The compiler does not warn — enums are not closed sets to the AL type system the way unions are in other languages.
|
||||
|
||||
The same shape shows up outside security: postings codeunits that handle two of three document types and silently skip the third; tax computation that branches on calculation method; report layouts that branch on output format. Wherever a `case` over an enum determines what code path executes, an `else` branch with a controlled error (or a deliberate documented no-op) is required.
|
||||
|
||||
## Best Practice
|
||||
|
||||
Add an `else` branch to every `case` statement that branches on an enum value when the code paths matter. For security-sensitive branches, raise a `Error` with a message that names the unsupported value:
|
||||
|
||||
```al
|
||||
case SharePointAccount."Authentication Type" of
|
||||
SharePointAccount."Authentication Type"::"Client Secret":
|
||||
GraphAuthInterface := GraphAuthClientCredentials;
|
||||
SharePointAccount."Authentication Type"::Certificate:
|
||||
GraphAuthInterface := GraphAuthCertificate;
|
||||
else
|
||||
Error(UnsupportedAuthTypeErr, SharePointAccount."Authentication Type");
|
||||
end;
|
||||
```
|
||||
|
||||
For deliberate no-op fall-through, document it: `else // intentional: format X is a passthrough.` so reviewers see the choice was made rather than forgotten. Pair the `else` arm of a security branch with telemetry — an unsupported value reaching this point in production is a deployment signal worth surfacing.
|
||||
|
||||
See sample: `case-must-handle-unknown-enum-values.good.al`.
|
||||
|
||||
## Anti Pattern
|
||||
|
||||
`case` over an enum with no `else`, used to choose which authentication, authorization, or security-state-initialising code path runs. Detection signal: a `case` whose arms write to a single shared output (an interface variable, a credentials record, a permission token) with no `else` arm. The narrower signal — a `case` whose value type is a security-related enum (`Authentication Type`, `Authorization Mode`, `Identity Provider`, `Permission Scope`, `Encryption Algorithm`) — is the high-confidence anti-pattern.
|
||||
|
||||
See sample: `case-must-handle-unknown-enum-values.bad.al`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue