Skip to content

feat(templates): enforce template usage, validate against schema#330

Open
daniel-mader wants to merge 72 commits into
betafrom
feat/enforce-templates
Open

feat(templates): enforce template usage, validate against schema#330
daniel-mader wants to merge 72 commits into
betafrom
feat/enforce-templates

Conversation

@daniel-mader
Copy link
Copy Markdown
Contributor

@daniel-mader daniel-mader commented Apr 28, 2026

Description of change

  • Templates are required for credential issuance.
  • Credential configurations are manged implicitly through the templates.
  • Nested structures (such as in Open Badges 3.0) are flattened.
  • Some properties in the JSON Schema can be marked as "immutable" through schema_properties_attributes, so they are locked and can never be altered/removed.

BREAKING CHANGE

Require templateId when creating a credential. The two fields data_model and holder_type become immutable after the initial template creation.

Links to any relevant issues

Be sure to reference any related issues by adding fixes issue #.

How the change has been tested

Describe the tests that you ran to verify your changes.
Make sure to provide instructions for the maintainer as well as any relevant configurations.

Definition of Done checklist

Add an x to the boxes that are relevant to your changes.

  • I have followed the contribution guidelines for this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have successfully tested this change in a docker environment

@daniel-mader daniel-mader self-assigned this Apr 28, 2026
@daniel-mader daniel-mader changed the title feat(templates): enforce templates feat(templates): enforce template usage, validate against schema Apr 28, 2026
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 28, 2026

Codecov Report

❌ Patch coverage is 84.86974% with 151 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
agent_api_http/src/v0/issuance/credentials.rs 76.10% 38 Missing ⚠️
agent_application/src/lib.rs 0.00% 36 Missing ⚠️
agent_api_http/src/v0/library/error.rs 0.00% 25 Missing ⚠️
...application/credential_configuration_projection.rs 94.33% 21 Missing ⚠️
agent_library/src/template/aggregate.rs 94.05% 18 Missing ⚠️
agent_api_http/src/lib.rs 0.00% 6 Missing ⚠️
agent_api_http/src/v0/templates/mod.rs 16.66% 5 Missing ⚠️
agent_library/src/template/views/mod.rs 83.33% 1 Missing ⚠️
agent_store/src/lib.rs 66.66% 1 Missing ⚠️
Files with missing lines Coverage Δ
...v0/authorization/authorization_server/authorize.rs 100.00% <ø> (ø)
...p/src/v0/authorization/authorization_server/par.rs 100.00% <ø> (ø)
...src/v0/authorization/authorization_server/token.rs 95.45% <ø> (ø)
...tp/src/v0/issuance/credential_issuer/credential.rs 97.02% <100.00%> (ø)
.../src/v0/issuance/credential_issuer/notification.rs 98.94% <100.00%> (+0.02%) ⬆️
...v0/issuance/credential_issuer/token_status_list.rs 98.13% <100.00%> (+0.01%) ⬆️
...al_issuer/well_known/oauth_authorization_server.rs 97.61% <100.00%> (+0.11%) ⬆️
...tial_issuer/well_known/openid_credential_issuer.rs 97.14% <100.00%> (+0.17%) ⬆️
agent_api_http/src/v0/issuance/mod.rs 100.00% <100.00%> (ø)
agent_api_http/src/v0/issuance/nonce/mod.rs 100.00% <100.00%> (ø)
... and 14 more

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@daniel-mader daniel-mader marked this pull request as ready for review April 28, 2026 10:42
@daniel-mader daniel-mader marked this pull request as draft April 28, 2026 10:43
daniel-mader and others added 7 commits April 29, 2026 21:30
Co-authored-by: Copilot <copilot@github.com>
- Extend PropertyAttribute with `immutable: bool` (default false)
- Auto-populate `immutable: true` for all OpenBadges 3.0 schema properties
- Enforce immutable properties cannot be removed during UpdateSchema
- Ensure immutable flag cannot be altered via UpdateSchemaPropertiesAttributes
- Add NonRemovablePropertyViolation error with HTTP 409 Conflict mapping
- Add tests for immutable enforcement, auto-population, and flag preservation

Agent-Logs-Url: https://github.com/impierce/ssi-agent/sessions/8907cf6d-663a-4880-9dd8-9699fbfa3e5b

Co-authored-by: daniel-mader <45594838+daniel-mader@users.noreply.github.com>
…rcement

Agent-Logs-Url: https://github.com/impierce/ssi-agent/sessions/8907cf6d-663a-4880-9dd8-9699fbfa3e5b

Co-authored-by: daniel-mader <45594838+daniel-mader@users.noreply.github.com>
…al mapping

- Define default required OB3 properties: achievement.name, achievement.criteria.narrative
- Auto-merge these into the schema when creating an OpenBadges 3.0 template
- Inject fixed achievement.type="Achievement" during credential mapping
- Add map_open_badges_input_to_credential() to transform flat template input to nested OBv3 structure
- Integrate mapping into credential creation endpoint for OB3 templates
- Add unit tests for mapping and schema merging

Agent-Logs-Url: https://github.com/impierce/ssi-agent/sessions/1932e429-576c-4b09-9988-fd8fc69574b0

Co-authored-by: daniel-mader <45594838+daniel-mader@users.noreply.github.com>
…dundant immutable assignment

Agent-Logs-Url: https://github.com/impierce/ssi-agent/sessions/1932e429-576c-4b09-9988-fd8fc69574b0

Co-authored-by: daniel-mader <45594838+daniel-mader@users.noreply.github.com>
@daniel-mader daniel-mader requested a review from Copilot May 5, 2026 14:05
@daniel-mader daniel-mader marked this pull request as ready for review May 5, 2026 14:05
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 35 out of 36 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread agent_library/src/template/aggregate.rs
Comment thread agent_api_http/src/v0/templates/mod.rs
@daniel-mader daniel-mader changed the title feat(templates): enforce template usage, validate against schema feat(templates): enforce template usage, validate against schema May 6, 2026
@daniel-mader daniel-mader requested a review from coplat May 7, 2026 07:56
.type_url(type_url("templates#invalid-schema-properties-attributes"))
.source(self)
.finish(),
TemplateError::NonRemovablePropertyViolation(_) => ApiError::builder(StatusCode::CONFLICT)
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.

These errors should be added to the problem details documentation. The type_url's should be "library#..." instead of "templates#".

warn!("Template view not yet initialized; skipping credential configuration sync for `{template_id}`");
return;
};
match query_handler(template_id, view).await {
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 match arm skips Draft and Deleted but implicitly allows Published and Archived. Should this be intended for Archived?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We should indeed treat "Archived" as "read-only", but that has more implications. We're not actively supporting that at the moment, so we can leave it as is for now and properly cover it in an upcoming task. The main goal for now is to keep the credential configurations and template updates in sync.

Comment thread agent_library/src/template/aggregate.rs Outdated
/// Panics if `schema` is not a JSON object. Callers must ensure the schema is an object
/// (or default to `{"type": "object"}`) before calling this function.
#[cfg(test)]
fn merge_open_badges_defaults(schema: &mut serde_json::Value) {
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.

this test fn seems to only be called by test_merge_open_badges_defaults_into_empty_schema. but since the actual production code seems to be using validate_open_badges_required_properties as opposed to this auto-merge logic, I think this could be removed as it doesn't reflect the actual production code

Copy link
Copy Markdown
Contributor

@nanderstabel nanderstabel left a comment

Choose a reason for hiding this comment

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

Edit: nvm the comment below, I figured out I had to update the status to published 👌

When I Create a new Template:

curl --location 'http://localhost:3033/v0/templates/create-template' \
--header 'Content-Type: application/json' \
--header 'X-API-KEY: {{API_KEY}}' \
--data '{
    "title": "My Example Template",
    "credentialFormat": "w3c_vc_data_model_v1-1",
    "holderType": "organization"
}'

I assumed that a corresponding Credential Configuration would be created and included to the Credential Issuer Metadata, but when I hit:

curl --location 'http://localhost:3033/.well-known/openid-credential-issuer' \
--header 'X-API-KEY: {{API_KEY}}'

No new Credential Configuration (with title "My Example Template") is included in the Metadata. Is this expected, should I somehow trigger a Syncing command explicitly?

Comment on lines -704 to -706
DataModelUpdated,
CreatorUpdated,
HolderTypeUpdated,
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 event types can now also be removed from ssi-agent/agent_event_publisher_http/README.md

vec!["VerifiableCredential".to_string()]
}
DataModel::OpenBadges3_0 => {
vec!["OpenBadgeCredential".to_string(), "AchievementCredential".to_string()]
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.

For OpenBadges, the type value should be an array where the first item must be "VerifiableCredential" and the second item must me either "OpenBadgeCredential" OR "AchievementCredential" (I think in most cases we default to "OpenBadgeCredential") (see: https://www.imsglobal.org/spec/ob/v3p0#achievementcredential)

Comment thread agent_api_http/src/v0/library/error.rs Outdated
Comment on lines +20 to +40
.type_url(type_url("templates#immutable-property-violation"))
.source(self)
.finish(),
TemplateError::DisallowedOpenBadgesProperties(_) => ApiError::builder(StatusCode::BAD_REQUEST)
.title("Disallowed OpenBadges Schema Properties")
.type_url(type_url("templates#disallowed-open-badges-properties"))
.source(self)
.finish(),
TemplateError::MissingRequiredOpenBadgesProperties(_) => ApiError::builder(StatusCode::BAD_REQUEST)
.title("Missing Required OpenBadges Schema Properties")
.type_url(type_url("templates#missing-required-open-badges-properties"))
.source(self)
.finish(),
TemplateError::InvalidRequiredPropertyType(_) => ApiError::builder(StatusCode::BAD_REQUEST)
.title("Invalid Required Property Type")
.type_url(type_url("templates#invalid-required-property-type"))
.source(self)
.finish(),
TemplateError::MissingTitle => ApiError::builder(StatusCode::BAD_REQUEST)
.title("Missing Title")
.type_url(type_url("templates#missing-title"))
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.

All these type_urls are supposed to point to a specific Problem Details, but there is no description for them in the /docs/problem-details/library.md file

Comment thread agent_api_http/src/v0/issuance/mod.rs
pub credential: Value,
#[serde(default)]
pub is_signed: bool,
pub credential_configuration_id: String,
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.

Not only the Credentials Endpoint includes the credential_configuration_id but also the Offers Endpoint includes the credential_configuration_ids property (in agent_api_http/src/v0/issuance/offers/mod.rs), which is only used for the JIT Credentials flow. I think for consistency it should be removed there as well (or for now add a TODO comment so we can address that at a later point).

@nanderstabel
Copy link
Copy Markdown
Contributor

Since this PR makes makes Credential Configurations unusable in the API, it also does not make sense anymore to initialize those 'default' Credential Configurations on startup, so imo fn update_credential_configurations in agent_issuance/src/state.rs should be completely removed 🥳

Comment thread agent_api_http/src/v0/issuance/credentials.rs Outdated
Comment on lines +184 to +189
let path_elements: Vec<ClaimPathElement> = key
.split('.')
.map(|segment| ClaimPathElement::String(segment.to_string()))
.collect();

let path = ClaimPathPointer::try_new(path_elements).ok()?;
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.

If the provided properties look like this:

"properties": {
            "age": {},
            "name": {}
}

then the claim path pointers will look like this:

  • ["age"]
  • [name"]

but especially since this code is only executed when format == "vc+sd-jwt", it's more likely that the claim pat pointers end up as:

  • ["credentialSubject", "age"]
  • ["credentialSubject", name"]

Comment on lines +150 to +154
let claims = if format == "vc+sd-jwt" {
build_claims_from_schema(template)
} else {
None
};
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.

Why only build claims when format == "vc+sd-jwt"? I think you can make it work for "dc+sd-jwt" as well if you rewrite this part to something like this:

    let claims = if format == "vc+sd-jwt" {
        build_claims_from_schema(template, "credentialSubject."])
    } else if format == "dc+sd-jwt" {
        build_claims_from_schema(template, "")
    } else {
        None
    };

where "credentialSubject." can be prepended to key to create the path_elements in fn build_claims_from_schema

related: https://github.com/impierce/ssi-agent/pull/330/changes#r3220225971

daniel-mader and others added 6 commits May 11, 2026 18:33
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
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.

6 participants