From 5bb3a1825fbbc8867845996409691e7dab1a394b Mon Sep 17 00:00:00 2001 From: Suppaseth Charoenkarnka Date: Mon, 31 Aug 2026 17:19:47 +0700 Subject: [PATCH 1/2] Let the master workflow ask for a rebuild, and stop the docs promising one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `olaf_master_workflow` called `generate` with no arguments, so the notebook a pipeline actually runs had no way to request a rebuild. The framework has had the parameter since 1.0; only the workflow was missing it. The default stays `False`, and that is an access decision rather than a performance one. `generate`'s idempotency skip is keyed on `config_hash`, which fingerprints the config rows and cannot see the Delta catalog, so a table created since the last generate that matches an existing wildcard is absent from the lock-file and nothing is granted on it. A table created by last night's load does not become readable because a star matched it; opening it up is a decision somebody makes by passing `rebuild = True` — which re-resolves every wildcard, so it takes in every table added since the last generate, not only the one being asked about. Two documented claims said otherwise and are corrected. `config-examples.md` promised a new `sales.*` table "shows up in the mapping on the next generate"; the next generate is usually the one that skips. `architecture.md`'s second key invariant had the same gap. The CLS blacklist example described a newly discovered column the same way and is fixed alongside them. CHANGELOG records this under Unreleased. No version or tag is touched. --- CHANGELOG.md | 24 +++++++++++++++++++++++- docs/architecture.md | 2 +- docs/config-examples.md | 11 ++++++++--- notebooks/olaf_master_workflow.ipynb | 17 +++++++++++++++-- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b94b84f..67b3df4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,29 @@ Notable changes to OLAF — OneLake Access Framework — are recorded here using ## [Unreleased] -No changes yet. +### Added + +- **`olaf_master_workflow` takes a `rebuild` parameter, defaulting to `False`**, and passes it to + `generate`. The runner previously called `generate` with no arguments, so there was no way to + ask for a rebuild from the notebook a pipeline actually runs — the framework had the parameter, + the workflow did not expose it. The default is an access decision rather than a performance one: + `generate`'s idempotency skip is keyed on `config_hash`, which fingerprints the config rows and + cannot see the catalog, so a table created since the last generate that matches an existing + wildcard stays out of the mapping and ungranted until somebody deliberately passes + `rebuild = True`. Taking it in is all-or-nothing — every wildcard is re-resolved, so every table + added since the last generate arrives with it. + +### Fixed + +- `docs/config-examples.md` said a new table matching `sales.*` "shows up in the mapping on the + next generate with no config change needed". The first half is right and the second half is + what makes it wrong: an unchanged config takes the idempotency skip, so the *next* generate is + usually the one that rebuilds nothing. The example now says the table needs a generate that + actually runs, and names `rebuild=True` as the way to get one. The CLS blacklist example, which + described a newly discovered column the same way, is corrected alongside it. +- `docs/architecture.md`'s second key invariant read "New tables are absent from the saved mapping + until the next generate", which is true only of a generate that rebuilds. It now says so, and + says why `config_hash` cannot see the difference. ## [1.1.0] - 2026-08-27 diff --git a/docs/architecture.md b/docs/architecture.md index 9ba6fe7..0a9cfdb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -788,7 +788,7 @@ records when generate ran. Full detail: data-model.md. ## Key invariants 1. The plan/apply role build reads **only** the mapping lock-file — never the short config (TOCTOU closed). -2. New tables are absent from the saved mapping until the next generate. The resulting platform access still depends on workspace/item permissions and engine/access mode; OLAF does not infer that nobody can read them. +2. New tables are absent from the saved mapping until the next generate that actually rebuilds it. `config_hash` fingerprints the config rows and cannot see the catalog, so an unchanged config takes the idempotency skip and a table matching an existing glob stays out of the mapping — `rebuild=True` is the deliberate way in. The resulting platform access still depends on workspace/item permissions and engine/access mode; OLAF does not infer that nobody can read them. 3. Glob (A2): 0-match on an include OR an exclude = error; table schema part is literal-only. Case resolves through the catalog. Role and predicate limits are OLAF compatibility guards tied to the cited current platform pages, not permanent no-workaround guarantees. 4. `NOT IN` against a nullable column drops NULL rows silently — three-valued logic makes the term UNKNOWN for a NULL and `WHERE` keeps only TRUE. **This never widens access; it can only over-reject** — a row vanishes from a deny-list's result, so the failure is missing data, not exposure. Guard it with `OR IS NULL`, or assert the column is never NULL; do one of the two deliberately. OLAF warned about this per row (guardrail G3) until the release noted in the CHANGELOG and no longer does: the check was a substring test on the raw condition, so it fired on `status = 'CANNOT INVOICE'` (no `NOT IN` operator at all) and stayed silent on `region NOT IN ('a') AND type IS NULL` (a different column's `IS NULL` muted it). It could not be made sound without the literal-stripping lexer the other RLS rules share, and it named only one of the two mitigations above. 5. Cross-row rules (see the Rule catalog above) warn/block ambiguous multi-role policy shapes. Effective access is engine-explicit because SQL endpoint CLS differs from non-SQL CLS. C5 is a conservative guard around Microsoft's documented unsupported RLS/CLS combinations; it does not extrapolate untested behavior across tables or membership paths. diff --git a/docs/config-examples.md b/docs/config-examples.md index 6c390b0..a5ac6b1 100644 --- a/docs/config-examples.md +++ b/docs/config-examples.md @@ -100,8 +100,12 @@ name lookups against the catalog. | SalesRead | /Tables/sales/returns | Table | sg-analysts | **Takeaway:** the wildcard is resolved against the live catalog at generate time — a new -`sales.*` table shows up in the mapping on the next generate with no config change needed. The -schema part (`sales`) stays literal; only the table part accepts `*`/`?` (rule A2). +`sales.*` table needs no config change to be covered, but it does need a generate that actually +*runs*. `generate` is idempotent by default and its skip is keyed on `config_hash`, which +fingerprints the config rows and cannot see the catalog: an unchanged config skips, and the new +table stays out of the mapping — and ungranted — until `rebuild=True` (or an edit that changes the +hash) forces a real generate. The schema part (`sales`) stays literal; only the table part accepts +`*`/`?` (rule A2). See [modes.md](modes.md) for the skip and its exceptions. ### 3. All-except (`sales.*` minus `sales.returns`) @@ -218,7 +222,8 @@ declares (include and exclude alike), not only the survivors that reach the mapp **Takeaway:** `visible_columns` holds the allow-list OLAF places in the DAR request, not the authored exclusions. In blacklist mode, a newly discovered column enters the -allow-list on the next generate unless explicitly excluded; until regeneration, it is +allow-list on the next generate that actually runs, unless explicitly excluded — an +unchanged config takes the idempotency skip, so until a real regeneration the column is absent from OLAF's saved payload. Actual engine enforcement is outside this mapping example and differs by access path. See Microsoft's [CLS semantics](https://learn.microsoft.com/en-us/fabric/onelake/security/data-access-control-model#column-level-security). diff --git a/notebooks/olaf_master_workflow.ipynb b/notebooks/olaf_master_workflow.ipynb index 4e34ef7..f79d33a 100644 --- a/notebooks/olaf_master_workflow.ipynb +++ b/notebooks/olaf_master_workflow.ipynb @@ -165,6 +165,7 @@ "| `env` | `\"dev\"` | Tags every log row, so one estate's dev and prod audit trails stay apart. |\n", "| `auto_approve` | `False` | The gate. `True` lets `apply` run unattended — nobody reviews the omissions. |\n", "| `keep_unmanaged` | `False` | `False` = config is the whole truth: a live role it omits is left out of the payload. |\n", + "| `rebuild` | `False` | `True` re-resolves wildcards against the live catalog even when the config has not changed. |\n", "| `run_setup` | `False` | `True` after a framework upgrade — `setup` migrates control-table columns. |\n", "| `load_config` | `False` | `True` re-reads the authored workbook into the config + member tables. |\n", "| `config_workbook` | `\"Files/security/onelake_security.xlsx\"` | The authored workbook, resolved on the attached lakehouse. |\n", @@ -186,6 +187,7 @@ "lakehouse_name = \"YourLakehouse\" # the attached lakehouse this run secures (setup asserts it)\n", "auto_approve = False\n", "keep_unmanaged = False\n", + "rebuild = False\n", "run_setup = False\n", "load_config = False\n", "config_workbook = \"Files/security/onelake_security.xlsx\"\n", @@ -213,6 +215,7 @@ "# constant here through 1.0.x, invisible to Base parameters)\n", "# AUTO_APPROVE True = run apply · False = stop after plan\n", "# KEEP_UNMANAGED False = config is whole truth · omits roles absent from the config\n", + "# REBUILD True = re-resolve wildcards against the live catalog · see cell 13\n", "# LOAD_CONFIG True = reload the workbook into the config + member tables\n", "# WORKBOOK the authored workbook, resolved on the attached lakehouse\n", "# BATCH_ID the passed-in batch_id, or a fresh one when none was supplied\n", @@ -223,6 +226,7 @@ "LAKEHOUSE = lakehouse_name\n", "AUTO_APPROVE = auto_approve\n", "KEEP_UNMANAGED = keep_unmanaged\n", + "REBUILD = rebuild\n", "LOAD_CONFIG = load_config\n", "WORKBOOK = config_workbook\n", "BATCH_ID = batch_id.strip() or str(uuid.uuid4())\n", @@ -498,7 +502,16 @@ "`apply` read, and export the versioned CSV to the mapping-history folder.\n", "\n", "`skipped` means the config hash has not changed and the existing lock-file is still current — that\n", - "is a pass, not a problem.\n" + "is a pass, not a problem.\n", + "\n", + "**`rebuild` defaults to `False`, and that is an access decision, not a performance one.** The skip\n", + "is keyed on `config_hash`, which fingerprints the config rows only — it cannot see the live\n", + "catalog. So a table that appeared since the last generate and matches a wildcard (`sales.*`) is\n", + "**not** in the lock-file, and nothing is granted on it: a table created by last night's load does\n", + "not become readable because a star matched it. Opening it up is a decision somebody makes by\n", + "passing `rebuild = True` — which is all-or-nothing, re-resolving *every* wildcard, so it takes in\n", + "**every** table that has appeared since the last generate. Note the converse too: a table that was\n", + "**dropped** stays in the lock-file until the next real generate.\n" ] }, { @@ -509,7 +522,7 @@ "outputs": [], "source": [ "try:\n", - " generate = run_stage(\"generate\")\n", + " generate = run_stage(\"generate\", {\"rebuild\": REBUILD})\n", "\n", " if generate[\"status\"] == \"skipped\":\n", " print(\" → config unchanged, reusing the current lock-file\")\n", From 871ec441ea9e041197b706d709be7131e8f10fa8 Mon Sep 17 00:00:00 2001 From: Suppaseth Charoenkarnka Date: Mon, 31 Aug 2026 20:13:39 +0700 Subject: [PATCH 2/2] Document the skip where the mode manual should have had it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `docs/modes.md` is the mode manual, and its `generate` section never mentioned the idempotency skip — while the result-envelope table three sections above it already lists `status=skipped` as an outcome. So the one behaviour an operator meets on nearly every run had no home, and the previous commit's pointer from `config-examples.md` aimed at a section that did not exist. It now covers what `config_hash` fingerprints, the two things it therefore cannot see (the Delta catalog and the member table), the five exceptions that defeat the skip without a parameter, what `rebuild=True` re-resolves and drops, and the reminder that not granting a table is not the same as denying it. `docs/api/Deployment.md` was headed `generate(rebuild=False)` and never said what the parameter did; it does now, and points at the same section. --- CHANGELOG.md | 6 ++++++ docs/api/Deployment.md | 6 ++++++ docs/modes.md | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b3df4..39f4eab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,12 @@ Notable changes to OLAF — OneLake Access Framework — are recorded here using - `docs/architecture.md`'s second key invariant read "New tables are absent from the saved mapping until the next generate", which is true only of a generate that rebuilds. It now says so, and says why `config_hash` cannot see the difference. +- `docs/modes.md` documented every mode except the one behaviour an operator meets most often: its + `generate` section never mentioned the idempotency skip, even though the result-envelope table + above it already advertises `status=skipped`. It now carries the skip, what `config_hash` can and + cannot see, the five exceptions that defeat it, and what `rebuild=True` costs. +- `docs/api/Deployment.md` was headed `generate(rebuild=False)` and never said what the parameter + did. It does now. ## [1.1.0] - 2026-08-27 diff --git a/docs/api/Deployment.md b/docs/api/Deployment.md index 32e36c3..82e9a0a 100644 --- a/docs/api/Deployment.md +++ b/docs/api/Deployment.md @@ -42,6 +42,12 @@ Validates the authored config, resolves synthetic or approved member-cache entri the mapping and review artifact. OLAF deliberately does not call Microsoft Graph; this is a design choice, not a claim that Graph access is universally impossible in Fabric notebooks. +`rebuild=False` (the default) is idempotent: an unchanged config rebuilds nothing and returns +`status=skipped`. The key is `config_hash`, which fingerprints the config rows only — so a table +that has appeared since the last generate and matches an existing glob is not picked up, and is +not granted, until `rebuild=True` forces a full re-resolution. See +[modes.md](../modes.md) for the skip's five exceptions. + Generated mapping/control artifacts are sensitive. Do not commit a real mapping, output, or workbook to the public repository. diff --git a/docs/modes.md b/docs/modes.md index 0c2c15e..b14039a 100644 --- a/docs/modes.md +++ b/docs/modes.md @@ -73,6 +73,42 @@ A changed or unknown post-write boundary stops the chain, leaves the sentinel, a reports `possible_exposure` with the affected artifact/table version. Deleting the artifact would be containment only and is not described as erasing exposure. +### The idempotency skip, and `rebuild` + +`generate(rebuild=False)` — the default — is idempotent. When the config is unchanged +since the last generate it rebuilds nothing, logs one `no_change` row, and returns +`status=skipped`, `changed=false`, so a pipeline can call generate every run and +re-plan only when something moved. + +The comparison key is `config_hash`, a content fingerprint of the **config rows**. +That is the whole of what it sees, and two consequences follow: + +- **It cannot see the Delta catalog.** A table created since the last generate that + matches an existing table glob leaves `config_hash` byte-identical, so the skip + holds and the new table is neither resolved into the mapping nor granted. This is + deliberate — a table appearing in a lakehouse is not consent to share it — and + `rebuild=True` is the deliberate way in. It re-resolves *every* pattern, so it also + takes in every other table added since the last generate, and drops from the mapping + any table that has since been deleted. +- **It cannot see `onelake_security_member`**, which is a live input to every member + list. The exceptions below close that gap. + +Five things defeat the skip with no parameter, and a `rebuild=True` run never takes it: + +1. a config carrying a member `glob:` pattern is never eligible — the member table is + then a live grant list, and a principal added there must not be silently skipped; +2. stamped member objectIds that no longer match what the member table resolves the + same names to today; +3. a mapping stamped for a different workspace/lakehouse than the attached one; +4. a member table carrying resolution errors — the fast path must not certify a state + the full gate calls a hard error; +5. a mapping stamped by a different `framework_version` — content validated under + another version's rules has not been validated under these. + +Neither the skip nor a rebuild denies anything on its own. A table OLAF has not granted +is simply one OLAF has not granted; whether anyone can read it still depends on +workspace/item permissions and the access path. + OLAF's RLS parser is a conservative guard, not an exhaustive service grammar. The current platform source is Microsoft's [RLS syntax](https://learn.microsoft.com/en-us/fabric/onelake/security/row-level-security-syntax).