Skip to content

fix(guardian-service): clone a template-bound policy whole, or not at all - #6698

Open
vshvets-bc wants to merge 2 commits into
hashgraph:developfrom
Climission:fix/clone-template-bound-policy
Open

fix(guardian-service): clone a template-bound policy whole, or not at all#6698
vshvets-bc wants to merge 2 commits into
hashgraph:developfrom
Climission:fix/clone-template-bound-policy

Conversation

@vshvets-bc

Copy link
Copy Markdown
Collaborator

Two halves of the same defect: cloning a policy that has a schema-template binding either hard-fails, or succeeds and produces a half-bound clone.

1. Cloning a policy bound to an unpublished template throws

resolveSchemaTemplate only tries metadata.schemaTemplate.templateId and a message id:

if (metadata?.schemaTemplate?.templateId) {  }
const messageId = metadata?.schemaTemplate?.templateMessageId || binding.templateMessageId;

clonePolicy passes no metadata, and an unpublished template has no templateMessageId — so the clone fails with Selected schema template is inaccessible, even though the template is sitting right there in the database and the caller owns it.

It now falls back to the binding's own templateId, under the same accessibility rule the metadata branch already applies: published, or owned by the caller. An inaccessible template still falls through to the message-id path and the existing error.

2. The half-bound clone

saveSchemaTemplateSnapshot nulls policy.schemaTemplate when there is no snapshot to remap — which is exactly what clonePolicy produces. But it runs after the schemas are persisted, so they keep their templateId, templateSchemaId and templateFieldId markers.

The result is a policy claiming no template, sitting over schemas that still claim one. Any subsequent template operation then sees an inconsistent pair.

The decision moves to the top of the import, next to the explicit detach and before the schemas are written — which is what makes the marker strip persist:

if (this.mustDropSchemaTemplateBinding(policy, schemaTemplateSnapshot, metadata)) {
    policy.schemaTemplate = null;
    this.clearTemplateMetadataFromSchemas(schemas);
}

A binding with no snapshot to carry is dropped whole rather than half.

Testability note

mustDropSchemaTemplateBinding is extracted so the decision can be tested without standing up the entire import pipeline, and clearTemplateMetadataFromSchemas loses its private for the same reason. Neither changes behaviour.

Tests

guardian-service/tests/unit/policy-import-schema-template-binding.test.mjs — 9 cases, 6 fail against develop:

  • an unpublished bound template resolves with no metadata supplied
  • a bound template owned by someone else and unpublished is still refused
  • a published template belonging to another owner is accepted
  • an explicit detach still performs no lookup at all
  • the binding is dropped when there is no snapshot, kept when there is one, dropped on explicit detach, and untouched for a policy that was never bound
  • the strip leaves no marker on the schemas about to be written

guardian-service unit suite: 1798 passing.

🤖 Generated with Claude Code

… all

Two halves of the same defect.

resolveSchemaTemplate only tried metadata.schemaTemplate.templateId and a
message id, and clonePolicy passes no metadata - so cloning a policy bound to
an unpublished template threw "Selected schema template is inaccessible" even
though the template was sitting right there in the database. It now falls back
to the binding's own templateId, under the same accessibility rule as the
metadata branch: published, or owned by the caller.

The other half is the half-bound clone. saveSchemaTemplateSnapshot nulls
policy.schemaTemplate when there is no snapshot to remap - exactly what a clone
produces - but it runs after the schemas are persisted, so they kept their
templateId, templateSchemaId and templateFieldId markers: a policy claiming no
template over schemas still claiming one. The decision now happens up front,
beside the explicit detach and before the schemas are written, which is what
makes the strip persist. A binding with no snapshot to carry is dropped whole.

That decision is extracted as mustDropSchemaTemplateBinding so it can be tested
without standing up the whole import pipeline; clearTemplateMetadataFromSchemas
loses its `private` for the same reason.

Tests: 9 cases in guardian-service/tests/unit/policy-import-schema-template-binding.test.mjs,
6 of which fail against the previous implementation.

Signed-off-by: Volodymyr Shvets <Volodymyr.Shvets@envisionblockchain.com>
@vshvets-bc
vshvets-bc requested review from a team as code owners August 18, 2026 01:43
@Pyatakov Pyatakov self-assigned this Aug 18, 2026
Kept the mechanism, dropped the retelling of the symptom - that belongs in the
commit message.

Signed-off-by: Volodymyr Shvets <volodymyr.shvets@climission.com>

// clonePolicy passes no metadata, so an unpublished template was unreachable
// even though it sits in the database. Same rule: published, or owned.
if (binding.templateId) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The binding fallback is unreachable on the clone path it cites.

import() evaluates mustDropSchemaTemplateBinding at line 931, before resolveSchemaTemplate at 993. clonePolicy at guardian-service/src/policy-engine/policy-engine.ts:798 builds dataToCreate with no schemaTemplateSnapshot, so for a bound policy mustDrop is true, policy.schemaTemplate is nulled at 932, and resolveSchemaTemplate returns at 617–620 on !binding. The new block never runs on a clone – the fix for the "inaccessible" throw is the drop, not the fallback. The three resolveSchemaTemplate tests pass only because they call the method directly with a still-bound policy, bypassing that ordering. Either remove the branch and its comment, or (if a clone should stay bound to its unpublished template) run resolveSchemaTemplate first and take the drop decision from this.schemaTemplate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants