Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .changeset/d5-recertification-claim-withdrawn.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
---
"@objectstack/plugin-security": patch
---

fix(security): the ADR-0091 D5 attestation columns stop claiming a recertification review the platform does not run (#9046)

`last_certified_at` and `certified_by` are declared on both grant tables
(`sys_user_permission_set`, `sys_user_position`) as the ADR-0091 D5
recertification *substrate*. A whole-tree sweep over `packages/`, `apps/` and
`examples/` — every `.ts`/`.tsx`, tests included — finds the pair in exactly two
kinds of place: those two declarations and the generated i18n bundles carrying
their strings. **No producer and no consumer.** Nothing stamps either column,
nothing reads either one, and no surface derives "never certified" or
"certification stale" from them. The sweep is not blind: the sibling ADR-0091
columns on the same objects all resolve to real enforcement — `valid_from` /
`valid_until` through `isGrantActive` at resolution time, `reason` and
`delegated_from` through the delegated-admin gate and the security-posture lint.

Their descriptions nonetheless stated D5's intent as though it were the
behavior — *"When this grant was last attested in a recertification review. Null
= never certified"* and *"Reviewer who last attested this grant."* Access
recertification is a compliance control (SOX / ISO 27001 access review), so that
misreading is the expensive kind: an admin walking `plugin-security`'s objects,
or an AI agent authoring against this model, takes a populated `Last Certified
At` as evidence of a review the platform never performed and never checked.

ADR-0049 enforce-or-remove, settled the way `sys_capability.active` was
(maintainer ruling, 2026-08-13): **the claim is withdrawn in prose.** Building
the review workflow is a designed feature with no measured pull, and dropping
shipped columns costs a migration over existing rows while buying nothing the
prose fix does not — the harm here is the promise, not the storage, and a
description is one line to change back if D5 is ever implemented. The columns,
their types and their storage are untouched; no producer and no consumer is
added, deliberately.

Both descriptions now state the inertness outright rather than merely omitting
the promise, so a reader who remembers the old wording is told it was wrong
instead of being left to infer it. All four locale bundles carry the corrected
text.
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,17 +98,50 @@ export const SysUserPermissionSet = ObjectSchema.create({
'A row with delegated_from set is not itself delegatable and not self-renewable.',
}),

// [#9046] ADR-0091 D5 calls these two columns the recertification
// "substrate", and they are exactly that and nothing more. A whole-tree
// sweep (packages/, apps/, examples/, every .ts/.tsx, tests included)
// finds the pair in two kinds of place only: these declarations and the
// generated i18n bundles that carry their strings. No producer, no
// consumer - nothing stamps them, nothing reads them, and no surface
// derives "never certified" or "certification stale". Their siblings on
// this object are not like that: valid_from/valid_until are enforced by
// isGrantActive at resolution time, and reason/delegated_from are read by
// the delegated-admin gate and the security-posture lint.
//
// The old descriptions ("When this grant was last attested in a
// recertification review", "Reviewer who last attested this grant") stated
// D5's intent as though it were the behavior. Access recertification is a
// compliance control (SOX / ISO 27001 access review), so that misreading
// is the expensive kind: an admin walking these objects - or an AI agent
// authoring against this model - takes a populated "Last Certified At" as
// evidence of a review the platform never performed and never checked.
//
// ADR-0049 enforce-or-remove, settled the way sys_capability.active was
// (maintainer ruling, 2026-08-13): building the review workflow is a
// designed feature with no measured pull, and dropping shipped columns
// costs a migration over existing rows while buying nothing the prose fix
// does not - the harm here is the promise, not the storage. So the claim
// is withdrawn, and the descriptions state the inertness outright rather
// than merely omitting the promise: a reader who remembers the old wording
// has to be told it was wrong, not left to infer it. If D5 is ever
// implemented, these two descriptions are what must change with it.
last_certified_at: Field.datetime({
label: 'Last Certified At',
required: false,
description:
'[ADR-0091 D5] When this grant was last attested in a recertification review. Null = never certified.',
'[ADR-0091 D5] Reserved for a future access-recertification workflow, which would stamp here when this grant was last attested. ' +
'Inert today: no platform code writes this column and none reads it — no resolution path, gate or lint consults it, and nothing derives ' +
'"never certified" or "certification stale" from it. Null therefore means the workflow does not exist, not that this grant went unreviewed.',
}),

certified_by: Field.lookup('sys_user', {
label: 'Certified By',
required: false,
description: '[ADR-0091 D5] Reviewer who last attested this grant.',
description:
'[ADR-0091 D5] Reserved for the same future access-recertification workflow: the reviewer who would attest this grant. ' +
'Inert today: no platform code writes or reads it. A value written here by a client is an unverified annotation — ' +
'the platform checks nothing about it and grants nothing on the strength of it.',
}),

created_at: Field.datetime({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,17 +117,36 @@ export const SysUserPosition = ObjectSchema.create({
'A row with delegated_from set is not itself delegatable and not self-renewable — chains are cut both ways.',
}),

// [#9046] The same declared-but-inert D5 pair as on
// sys_user_permission_set, with the same disposition. The whole-tree sweep
// finds these two columns in the two declarations and the generated i18n
// bundles and nowhere else: nothing writes them, nothing reads them, no
// surface derives "never certified" or "certification stale" - while
// valid_from/valid_until (isGrantActive) and reason/delegated_from (the
// delegated-admin gate, the security-posture lint) on this same object all
// resolve to real enforcement. The old descriptions stated D5's intent as
// though it were the behavior, which on a compliance surface reads as
// evidence of an access review that never happened. ADR-0049
// enforce-or-remove, settled as sys_capability.active was (maintainer
// ruling, 2026-08-13): the claim is withdrawn in prose rather than the
// workflow built or the columns dropped. Full rationale sits with the
// sibling declaration in sys-user-permission-set.object.ts.
last_certified_at: Field.datetime({
label: 'Last Certified At',
required: false,
description:
'[ADR-0091 D5] When this grant was last attested in a recertification review. Null = never certified.',
'[ADR-0091 D5] Reserved for a future access-recertification workflow, which would stamp here when this grant was last attested. ' +
'Inert today: no platform code writes this column and none reads it — no resolution path, gate or lint consults it, and nothing derives ' +
'"never certified" or "certification stale" from it. Null therefore means the workflow does not exist, not that this grant went unreviewed.',
}),

certified_by: Field.lookup('sys_user', {
label: 'Certified By',
required: false,
description: '[ADR-0091 D5] Reviewer who last attested this grant.',
description:
'[ADR-0091 D5] Reserved for the same future access-recertification workflow: the reviewer who would attest this grant. ' +
'Inert today: no platform code writes or reads it. A value written here by a client is an unverified annotation — ' +
'the platform checks nothing about it and grants nothing on the strength of it.',
}),

created_at: Field.datetime({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -323,11 +323,11 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
},
last_certified_at: {
label: "Last Certified At",
help: "[ADR-0091 D5] When this grant was last attested in a recertification review. Null = never certified."
help: "[ADR-0091 D5] Reserved for a future access-recertification workflow, which would stamp here when this grant was last attested. Inert today: no platform code writes this column and none reads it — no resolution path, gate or lint consults it, and nothing derives \"never certified\" or \"certification stale\" from it. Null therefore means the workflow does not exist, not that this grant went unreviewed."
},
certified_by: {
label: "Certified By",
help: "[ADR-0091 D5] Reviewer who last attested this grant."
help: "[ADR-0091 D5] Reserved for the same future access-recertification workflow: the reviewer who would attest this grant. Inert today: no platform code writes or reads it. A value written here by a client is an unverified annotation — the platform checks nothing about it and grants nothing on the strength of it."
},
created_at: {
label: "Created At"
Expand DownExpand Up@@ -409,11 +409,11 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
},
last_certified_at: {
label: "Last Certified At",
help: "[ADR-0091 D5] When this grant was last attested in a recertification review. Null = never certified."
help: "[ADR-0091 D5] Reserved for a future access-recertification workflow, which would stamp here when this grant was last attested. Inert today: no platform code writes this column and none reads it — no resolution path, gate or lint consults it, and nothing derives \"never certified\" or \"certification stale\" from it. Null therefore means the workflow does not exist, not that this grant went unreviewed."
},
certified_by: {
label: "Certified By",
help: "[ADR-0091 D5] Reviewer who last attested this grant."
help: "[ADR-0091 D5] Reserved for the same future access-recertification workflow: the reviewer who would attest this grant. Inert today: no platform code writes or reads it. A value written here by a client is an unverified annotation — the platform checks nothing about it and grants nothing on the strength of it."
},
created_at: {
label: "Created At"
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -323,11 +323,11 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
},
last_certified_at: {
label: "Última certificación",
help: "[ADR-0091 D5] Cuándo se atestiguó por última vez esta concesión en una revisión de recertificación. Nulo = nunca certificada."
help: "[ADR-0091 D5] Reservada para un futuro flujo de recertificación de accesos, que registraría aquí cuándo se atestiguó por última vez esta concesión. Hoy es inerte: ningún código de la plataforma escribe esta columna ni la lee; ninguna ruta de resolución, control ni regla de lint la consulta, y nada deriva de ella «nunca certificada» ni «certificación obsoleta». Por tanto, un valor nulo significa que el flujo no existe, no que esta concesión no se haya revisado."
},
certified_by: {
label: "Certificado por",
help: "[ADR-0091 D5] Revisor que atestiguó por última vez esta concesión."
help: "[ADR-0091 D5] Reservada para ese mismo futuro flujo de recertificación de accesos: el revisor que atestiguaría esta concesión. Hoy es inerte: ningún código de la plataforma la escribe ni la lee. Un valor escrito aquí por un cliente es una anotación sin verificar; la plataforma no comprueba nada sobre él ni concede nada en función de él."
},
created_at: {
label: "Creado el"
Expand DownExpand Up@@ -409,11 +409,11 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
},
last_certified_at: {
label: "Última certificación",
help: "[ADR-0091 D5] Cuándo se atestiguó por última vez esta concesión en una revisión de recertificación. Nulo = nunca certificada."
help: "[ADR-0091 D5] Reservada para un futuro flujo de recertificación de accesos, que registraría aquí cuándo se atestiguó por última vez esta concesión. Hoy es inerte: ningún código de la plataforma escribe esta columna ni la lee; ninguna ruta de resolución, control ni regla de lint la consulta, y nada deriva de ella «nunca certificada» ni «certificación obsoleta». Por tanto, un valor nulo significa que el flujo no existe, no que esta concesión no se haya revisado."
},
certified_by: {
label: "Certificado por",
help: "[ADR-0091 D5] Revisor que atestiguó por última vez esta concesión."
help: "[ADR-0091 D5] Reservada para ese mismo futuro flujo de recertificación de accesos: el revisor que atestiguaría esta concesión. Hoy es inerte: ningún código de la plataforma la escribe ni la lee. Un valor escrito aquí por un cliente es una anotación sin verificar; la plataforma no comprueba nada sobre él ni concede nada en función de él."
},
created_at: {
label: "Creado el"
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -323,11 +323,11 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
},
last_certified_at: {
label: "最終認証日時",
help: "[ADR-0091 D5] この付与が再認証レビューで最後に証明された日時。Null = 未認証。"
help: "[ADR-0091 D5] 将来のアクセス再認証ワークフロー用に予約された列で、その際にこの付与が最後に証明された日時を記録します。現時点では不活性です。プラットフォームはこの列に書き込まず、読み取るコードもありません。解決経路もゲートも lint も参照せず、「未認証」や「認証が古い」を導出する箇所もありません。したがって Null はワークフローが存在しないことを意味するだけで、この付与が未レビューであることを意味しません。"
},
certified_by: {
label: "認証者",
help: "[ADR-0091 D5] この付与を最後に証明したレビュアー。"
help: "[ADR-0091 D5] 同じく将来のアクセス再認証ワークフロー用に予約された列で、その際にこの付与を証明したレビュアーを記録します。現時点では不活性で、プラットフォームは書き込みも読み取りも行いません。クライアントがここに書き込んだ値は検証されない注記にすぎず、プラットフォームはそれを検証せず、それを根拠に権限を与えることもありません。"
},
created_at: {
label: "作成日時"
Expand DownExpand Up@@ -409,11 +409,11 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
},
last_certified_at: {
label: "最終認証日時",
help: "[ADR-0091 D5] この付与が再認証レビューで最後に証明された日時。Null = 未認証。"
help: "[ADR-0091 D5] 将来のアクセス再認証ワークフロー用に予約された列で、その際にこの付与が最後に証明された日時を記録します。現時点では不活性です。プラットフォームはこの列に書き込まず、読み取るコードもありません。解決経路もゲートも lint も参照せず、「未認証」や「認証が古い」を導出する箇所もありません。したがって Null はワークフローが存在しないことを意味するだけで、この付与が未レビューであることを意味しません。"
},
certified_by: {
label: "認証者",
help: "[ADR-0091 D5] この付与を最後に証明したレビュアー。"
help: "[ADR-0091 D5] 同じく将来のアクセス再認証ワークフロー用に予約された列で、その際にこの付与を証明したレビュアーを記録します。現時点では不活性で、プラットフォームは書き込みも読み取りも行いません。クライアントがここに書き込んだ値は検証されない注記にすぎず、プラットフォームはそれを検証せず、それを根拠に権限を与えることもありません。"
},
created_at: {
label: "作成日時"
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -323,11 +323,11 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
},
last_certified_at: {
label: "最近认证时间",
help: "[ADR-0091 D5] 此授予在重新认证复核中最近一次被认证的时间。为空 = 从未认证。"
help: "[ADR-0091 D5] 为未来的访问权限重新认证(定期复核)流程预留:届时用于记录此授予最近一次被认证的时间。当前该列是惰性的:平台不会写入它,也没有任何代码读取它——没有任何解析路径、门禁或 lint 会参考它,也没有任何地方据此推导出「从未认证」或「认证已过期」。因此为空只表示该流程尚不存在,并不表示此授予未经复核。"
},
certified_by: {
label: "认证人",
help: "[ADR-0091 D5] 最近一次认证此授予的复核人。"
help: "[ADR-0091 D5] 同样为未来的访问权限重新认证流程预留:届时用于记录认证此授予的复核人。当前该列是惰性的:平台既不写入也不读取它。客户端在此写入的值只是一条未经验证的备注——平台不会对它做任何校验,也不会据此授予任何权限。"
},
created_at: {
label: "创建时间"
Expand DownExpand Up@@ -409,11 +409,11 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
},
last_certified_at: {
label: "最近认证时间",
help: "[ADR-0091 D5] 此授予在重新认证复核中最近一次被认证的时间。为空 = 从未认证。"
help: "[ADR-0091 D5] 为未来的访问权限重新认证(定期复核)流程预留:届时用于记录此授予最近一次被认证的时间。当前该列是惰性的:平台不会写入它,也没有任何代码读取它——没有任何解析路径、门禁或 lint 会参考它,也没有任何地方据此推导出「从未认证」或「认证已过期」。因此为空只表示该流程尚不存在,并不表示此授予未经复核。"
},
certified_by: {
label: "认证人",
help: "[ADR-0091 D5] 最近一次认证此授予的复核人。"
help: "[ADR-0091 D5] 同样为未来的访问权限重新认证流程预留:届时用于记录认证此授予的复核人。当前该列是惰性的:平台既不写入也不读取它。客户端在此写入的值只是一条未经验证的备注——平台不会对它做任何校验,也不会据此授予任何权限。"
},
created_at: {
label: "创建时间"
Expand Down
Loading