Skip to content

A passport cannot state its product category, so credential scope cannot be enforced #173

Description

@LKSNDRTMLKV

The gap

A passport cannot state its product category in any uniform way, so nothing can
read one off a Passport to make a decision with.

  • Passport has no envelope-level product category. The old product_category
    field was renamed to sector, and its doc records why: it "actually held a
    sector".
  • Three of twelve sectors carry one inside sector_data, at three different
    shapes: electronics.product_category: DeviceType (an enum),
    steel.product_category: String, unsold_goods.product_category: String. The
    other nine carry none.
  • SectorDescriptor::product_categories: Vec<String> declares the legal values
    per sector and is the obvious authority — but has no reader.

Why the missing reader is not hypothetical

product_categories_are_legal_values_of_their_schema_enum exists because the
catalog said clothing_accessories and starting-lighting-ignition where the
schemas said accessories and starting-lighting-ignition. Its own comment
records the situation exactly:

Neither was load-bearing, because SectorDescriptor::product_categories has
no reader in Rust today; both would have become load-bearing the moment one
appeared.

That test catches catalog-versus-schema drift. It does not give a caller a way
to ask a passport what it is.

The consumer waiting on this

dpp-vc::verify_credential_claims_with_trust already accepts
required_product_category: Option<&str> and applies the same "empty means
unscoped, populated must contain" rule it applies to sectors. The engine passes
None, so a credential scoped to one product category grants its full audience
over every product in a sector it covers — tracked as
odal-node/dpp-engine#64.

The enforcement exists here. The engine cannot use it because there is nothing
to pass.

Shape

The decision is where a product category lives, and it is a modelling question
rather than a plumbing one:

  1. An envelope field on Passport, validated against the sector's
    SectorDescriptor::product_categories. Uniform across all twelve sectors and
    readable without knowing the sector's data type — but the envelope's rule is
    additive-only and permanent, so it would have to be Option<String> with
    #[serde(default)], and "absent" would need a defined meaning for access
    control (almost certainly: unscoped credentials only).
  2. A SectorData accessorfn product_category(&self) -> Option<&str>
    that each variant implements, returning None for the nine that carry
    nothing and normalising DeviceType to its wire string. No schema change, no
    migration, and it makes the existing three readable today. But it leaves nine
    sectors permanently unscopeable.
  3. Both: the accessor now, the envelope field when a sector needs scoping
    that its data does not already express.

(2) is the one that unblocks the engine without touching a persisted shape, and
it is reversible. (1) is the one that makes product-category scope a real
control for every sector.

Worth settling before the first credential is issued against a scope that is
silently not enforced.

Note for whoever picks this up

Do not reach for dpp_domain::ProductCategory — it was decomposed and no longer
exists. Both sides of this are strings now; engine#64's description of an
enum-versus-string mismatch is stale and has been corrected there.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions