DPP Access Control
The EU ESPR regulation mandates differentiated access to Digital Product Passport data based on user roles and data sensitivity. This guide covers tiered access control, field visibility, and redaction strategies.
Overviewโ
Why Access Control?โ
Digital Product Passports contain sensitive information that must be accessible to different stakeholders at different levels:
- Consumers need basic environmental impact and composition information
- Supply chain partners require material sourcing and regulatory certificates
- Market surveillance authorities need full data access for compliance verification
- Economic operators (manufacturers/importers) retain control over their data
The ESPR requires granular access control to:
- Prevent unauthorized disclosure of proprietary formulations
- Enable consumer transparency
- Support regulatory oversight
- Maintain competitive confidentiality
Access Tiersโ
DPPAccessLevel Enumerationโ
enum DPPAccessLevel {
PUBLIC = "public", // Consumers and general public
AUTHENTICATED = "authenticated", // Verified supply chain partners
REGULATORY = "regulatory", // Market surveillance, customs authorities
OWNER = "owner" // Economic operator/manufacturer
}
Tier Characteristicsโ
| Tier | Audience | Purpose | Typical Fields Visible |
|---|---|---|---|
| PUBLIC | Consumers, retailers, general public | Product transparency, sustainability claims | productName, carbonFootprint, recycledContent, repairabilityScore, fiberComposition (textiles) |
| AUTHENTICATED | Authorized distributors, traders, supply chain partners | Supply chain collaboration, compliance verification | All PUBLIC + manufacturer details, material sources, factory locations, regulatory certifications |
| REGULATORY | EU enforcement authorities, market surveillance, customs | Compliance enforcement, product safety verification | All AUTHENTICATED + costOfSubstances, dueDiligenceReports, testing protocols, sensitive SVHC concentrations |
| OWNER | Economic operator, manufacturer, importer | Full control and management | All fields, including proprietary manufacturing processes and internal notes |
Default Field Visibilityโ
The following table defines which fields are visible at each access level by default:
| Field | Data Type | PUBLIC | AUTH | REG | OWNER |
|---|---|---|---|---|---|
| productId | string | โ | โ | โ | โ |
| productName | string | โ | โ | โ | โ |
| manufacturer | string | โ | โ | โ | โ |
| countryOfOrigin | string | โ | โ | โ | โ |
| category | enum | โ | โ | โ | โ |
| carbonFootprint | number | โ | โ | โ | โ |
| recycledContent | number | โ | โ | โ | โ |
| durabilityYears | number | โ | โ | โ | โ |
| repairabilityScore | number | โ | โ | โ | โ |
| substancesOfConcern | array | โ* | โ* | โ | โ |
| conformityDeclarations | array | โ | โ | โ | โ |
| sectorData | object | โ* | โ* | โ | โ |
| manufacturingDetails | object | โ | โ | โ | โ |
| supplierInformation | object | โ | โ | โ | โ |
| testingReports | array | โ | โ | โ | โ |
| proprietaryNotes | string | โ | โ | โ | โ |
*Category-specific visibility rules apply (see next section)
Category-Specific Overridesโ
Battery Passport (Battery Regulation Art. 13)โ
Battery substances and sector data must be PUBLIC to enable consumer awareness:
const batteryOverrides = {
substancesOfConcern: {
minimumLevel: DPPAccessLevel.PUBLIC
},
sectorData: {
fields: ["chemistry", "capacityKwh", "cobaltContent", "lithiumContent"],
minimumLevel: DPPAccessLevel.PUBLIC
}
};
Textile Passport (ESPR Annex VI)โ
Fiber composition must be PUBLIC; water usage visible at AUTHENTICATED:
const textileOverrides = {
sectorData: {
fiberComposition: DPPAccessLevel.PUBLIC,
waterUsageLiters: DPPAccessLevel.AUTHENTICATED,
processes: DPPAccessLevel.AUTHENTICATED,
microfiberRelease: DPPAccessLevel.AUTHENTICATED
}
};
Electronics (ESPR Annex VII)โ
Energy consumption and hazardous substances are PUBLIC:
const electronicsOverrides = {
substancesOfConcern: {
minimumLevel: DPPAccessLevel.PUBLIC
},
sectorData: {
energyConsumption: DPPAccessLevel.PUBLIC,
hazardousSubstances: DPPAccessLevel.PUBLIC
}
};
Core Functionsโ
filterDPPByAccess / filter_dpp_by_accessโ
Filters a DPP object to contain only fields visible at a given access level:
- TypeScript
- Python
import {
DPPAccessLevel,
filterDPPByAccess,
DPPMetadata
} from '@heimdall/dpp-access';
const fullDPP: DPPMetadata = {
productId: "08714043123452",
productName: "Eco Battery Pack 5000mAh",
manufacturer: "Green Energy Corp",
countryOfOrigin: "DE",
category: "battery",
carbonFootprint: 2.5,
recycledContent: 35,
durabilityYears: 5,
repairabilityScore: 7,
substancesOfConcern: [
{
substanceId: "7440-38-2",
name: "Cobalt",
concentration: 8.5,
status: "SVHC"
}
],
conformityDeclarations: [
{
regulation: "Directive 2014/30/EU",
conformityMarked: true,
testingDate: "2025-11-15"
}
],
manufacturingDetails: {
factoryLocation: "Shanghai Plant 2",
processType: "wet chemistry"
},
sectorData: {
chemistry: "lithium-ion",
capacityKwh: 0.018,
cobaltContent: 8.5,
lithiumContent: 4.2
}
};
// PUBLIC view: Only basic transparency data
const publicDPP = filterDPPByAccess(fullDPP, DPPAccessLevel.PUBLIC);
console.log(publicDPP);
/* Output:
{
productId: "08714043123452",
productName: "Eco Battery Pack 5000mAh",
manufacturer: "Green Energy Corp",
carbonFootprint: 2.5,
recycledContent: 35,
repairabilityScore: 7,
substancesOfConcern: [...], // SVHC data visible per Battery Reg
sectorData: {
chemistry: "lithium-ion",
capacityKwh: 0.018,
cobaltContent: 8.5,
lithiumContent: 4.2
}
// conformityDeclarations, manufacturingDetails NOT included
}
*/
// AUTHENTICATED view: Supply chain partner data
const authDPP = filterDPPByAccess(fullDPP, DPPAccessLevel.AUTHENTICATED);
console.log(authDPP);
/* Includes:
...publicDPP,
manufacturingDetails,
conformityDeclarations
// testingReports NOT included
*/
// REGULATORY view: Full enforcement authority access
const regDPP = filterDPPByAccess(fullDPP, DPPAccessLevel.REGULATORY);
console.log(regDPP);
// Includes all fields except proprietaryNotes
// OWNER view: Manufacturer retains all data
const ownerDPP = filterDPPByAccess(fullDPP, DPPAccessLevel.OWNER);
console.log(ownerDPP === fullDPP); // true (no redaction)
from heimdall.dpp_access import (
DPPAccessLevel,
filter_dpp_by_access,
DPPMetadata
)
full_dpp = DPPMetadata(
product_id="08714043123452",
product_name="Eco Battery Pack 5000mAh",
manufacturer="Green Energy Corp",
country_of_origin="DE",
category="battery",
carbon_footprint=2.5,
recycled_content=35,
durability_years=5,
repairability_score=7,
substances_of_concern=[
{
"substance_id": "7440-38-2",
"name": "Cobalt",
"concentration": 8.5,
"status": "SVHC"
}
],
conformity_declarations=[
{
"regulation": "Directive 2014/30/EU",
"conformity_marked": True,
"testing_date": "2025-11-15"
}
],
manufacturing_details={
"factory_location": "Shanghai Plant 2",
"process_type": "wet chemistry"
},
sector_data={
"chemistry": "lithium-ion",
"capacity_kwh": 0.018,
"cobalt_content": 8.5,
"lithium_content": 4.2
}
)
# PUBLIC view
public_dpp = filter_dpp_by_access(full_dpp, DPPAccessLevel.PUBLIC)
print(public_dpp)
# AUTHENTICATED view
auth_dpp = filter_dpp_by_access(full_dpp, DPPAccessLevel.AUTHENTICATED)
print(auth_dpp)
# REGULATORY view
reg_dpp = filter_dpp_by_access(full_dpp, DPPAccessLevel.REGULATORY)
print(reg_dpp)
# OWNER view
owner_dpp = filter_dpp_by_access(full_dpp, DPPAccessLevel.OWNER)
assert owner_dpp == full_dpp
Redaction Modesโ
Control how restricted fields are handled:
- TypeScript
- Python
import { RedactionMode } from '@heimdall/dpp-access';
// Mode 1: OMIT (default)
// Restricted fields are completely removed from output
const filtered1 = filterDPPByAccess(fullDPP, DPPAccessLevel.PUBLIC, {
redactionMode: RedactionMode.OMIT
});
// conformityDeclarations field does not exist in output
// Mode 2: PLACEHOLDER
// Restricted fields are included but marked as redacted
const filtered2 = filterDPPByAccess(fullDPP, DPPAccessLevel.PUBLIC, {
redactionMode: RedactionMode.PLACEHOLDER
});
console.log(filtered2.conformityDeclarations);
// Output: "[REDACTED]"
// Mode 3: SHADOW
// Array fields show count; object fields show keys only
const filtered3 = filterDPPByAccess(fullDPP, DPPAccessLevel.PUBLIC, {
redactionMode: RedactionMode.SHADOW
});
console.log(filtered3.conformityDeclarations);
// Output: { count: 1, summary: "1 declaration(s) restricted" }
from heimdall.dpp_access import RedactionMode
# Mode 1: OMIT (default)
filtered_1 = filter_dpp_by_access(
full_dpp,
DPPAccessLevel.PUBLIC,
redaction_mode=RedactionMode.OMIT
)
# Mode 2: PLACEHOLDER
filtered_2 = filter_dpp_by_access(
full_dpp,
DPPAccessLevel.PUBLIC,
redaction_mode=RedactionMode.PLACEHOLDER
)
print(filtered_2.conformity_declarations)
# Output: "[REDACTED]"
# Mode 3: SHADOW
filtered_3 = filter_dpp_by_access(
full_dpp,
DPPAccessLevel.PUBLIC,
redaction_mode=RedactionMode.SHADOW
)
print(filtered_3.conformity_declarations)
# Output: { "count": 1, "summary": "1 declaration(s) restricted" }
Authorization Checksโ
isAtLeast / is_at_leastโ
Verify user authorization before returning DPP data:
- TypeScript
- Python
import { DPPAccessLevel } from '@heimdall/dpp-access';
class DPPService {
getDPP(productId: string, userAccessLevel: DPPAccessLevel): DPPMetadata {
const fullDPP = this.repository.findById(productId);
// Check authorization
if (!userAccessLevel.isAtLeast(DPPAccessLevel.PUBLIC)) {
throw new UnauthorizedError("User not permitted to view DPP");
}
// Filter and return
return filterDPPByAccess(fullDPP, userAccessLevel);
}
/**
* Example authorization checks in REST endpoint
*/
async getPublicDPP(req: Request, res: Response) {
const userLevel = DPPAccessLevel.PUBLIC;
const dpp = this.getDPP(req.params.productId, userLevel);
res.json(dpp);
}
async getAuthenticatedDPP(req: Request, res: Response) {
// Verify OAuth token or API key
const userLevel = await this.authService.verifyToken(req.headers.authorization);
if (!userLevel.isAtLeast(DPPAccessLevel.AUTHENTICATED)) {
return res.status(403).json({ error: "Insufficient permissions" });
}
const dpp = this.getDPP(req.params.productId, userLevel);
res.json(dpp);
}
async getRegulatoryDPP(req: Request, res: Response) {
// Verify regulatory authority certificate
const cert = req.client.cert;
const userLevel = this.authService.verifyRegulatoryAuthority(cert);
if (!userLevel.isAtLeast(DPPAccessLevel.REGULATORY)) {
return res.status(403).json({ error: "Not an authorized authority" });
}
const dpp = this.getDPP(req.params.productId, userLevel);
res.json(dpp);
}
}
from heimdall.dpp_access import DPPAccessLevel
from flask import request, jsonify
class DPPService:
def get_dpp(self, product_id: str, user_access_level: DPPAccessLevel):
"""Retrieve DPP with access control filtering"""
full_dpp = self.repository.find_by_id(product_id)
# Check authorization
if not user_access_level.is_at_least(DPPAccessLevel.PUBLIC):
raise UnauthorizedError("User not permitted to view DPP")
# Filter and return
return filter_dpp_by_access(full_dpp, user_access_level)
# Flask route examples
def get_public_dpp(self, product_id):
user_level = DPPAccessLevel.PUBLIC
dpp = self.get_dpp(product_id, user_level)
return jsonify(dpp)
def get_authenticated_dpp(self, product_id):
# Verify OAuth token or API key
user_level = self.auth_service.verify_token(
request.headers.get("Authorization")
)
if not user_level.is_at_least(DPPAccessLevel.AUTHENTICATED):
return {"error": "Insufficient permissions"}, 403
dpp = self.get_dpp(product_id, user_level)
return jsonify(dpp)
def get_regulatory_dpp(self, product_id):
# Verify regulatory authority certificate
cert = request.environ.get("SSL_CLIENT_CERT")
user_level = self.auth_service.verify_regulatory_authority(cert)
if not user_level.is_at_least(DPPAccessLevel.REGULATORY):
return {"error": "Not an authorized authority"}, 403
dpp = self.get_dpp(product_id, user_level)
return jsonify(dpp)
Field-Level Visibility Customizationโ
Override default visibility for specific fields:
- TypeScript
- Python
import { filterDPPByAccess, DPPAccessLevel } from '@heimdall/dpp-access';
const customConfig = {
fieldOverrides: {
// Make factory location visible to AUTHENTICATED users
"manufacturingDetails.factoryLocation": DPPAccessLevel.AUTHENTICATED,
// Restrict recycled content percentage to REGULATORY only
"recycledContent": DPPAccessLevel.REGULATORY,
// Hide proprietary notes from all but OWNER
"proprietaryNotes": DPPAccessLevel.OWNER,
// Make certification date public (override default)
"conformityDeclarations.testingDate": DPPAccessLevel.PUBLIC
}
};
const publicDPP = filterDPPByAccess(fullDPP, DPPAccessLevel.PUBLIC, customConfig);
// recycledContent is now hidden from public view
custom_config = {
"field_overrides": {
"manufacturing_details.factory_location": DPPAccessLevel.AUTHENTICATED,
"recycled_content": DPPAccessLevel.REGULATORY,
"proprietary_notes": DPPAccessLevel.OWNER,
"conformity_declarations.testing_date": DPPAccessLevel.PUBLIC,
}
}
public_dpp = filter_dpp_by_access(full_dpp, DPPAccessLevel.PUBLIC, custom_config)
Compliance Audit Trailโ
Log access requests to meet ESPR audit requirements:
- TypeScript
- Python
interface AccessLog {
timestamp: string;
productId: string;
requestingParty: string;
accessLevel: DPPAccessLevel;
fieldsRetrieved: string[];
dataHash: string; // SHA-256 of returned data
reason: string; // "CONSUMER_VIEW" | "SUPPLY_CHAIN" | "INSPECTION" | etc.
}
class AuditedDPPService extends DPPService {
async getDPP(productId: string, userAccessLevel: DPPAccessLevel): Promise<DPPMetadata> {
const fullDPP = await super.getDPP(productId, userAccessLevel);
// Log access
await this.auditLog.record({
timestamp: new Date().toISOString(),
productId,
requestingParty: this.context.userId,
accessLevel: userAccessLevel,
fieldsRetrieved: Object.keys(fullDPP),
dataHash: sha256(JSON.stringify(fullDPP)),
reason: this.context.accessReason
});
return fullDPP;
}
}
from dataclasses import dataclass
from datetime import datetime
import hashlib
@dataclass
class AccessLog:
timestamp: str
product_id: str
requesting_party: str
access_level: DPPAccessLevel
fields_retrieved: list
data_hash: str
reason: str
class AuditedDPPService(DPPService):
def get_dpp(self, product_id: str, user_access_level: DPPAccessLevel):
full_dpp = super().get_dpp(product_id, user_access_level)
# Log access
self.audit_log.record(
AccessLog(
timestamp=datetime.utcnow().isoformat(),
product_id=product_id,
requesting_party=self.context.user_id,
access_level=user_access_level,
fields_retrieved=list(full_dpp.keys()),
data_hash=hashlib.sha256(
str(full_dpp).encode()
).hexdigest(),
reason=self.context.access_reason
)
)
return full_dpp
Best Practicesโ
1. Default to Least Privilegeโ
Always start with the most restrictive access level and escalate only upon verification.
2. Regular Access Reviewsโ
Audit access logs monthly to identify unusual patterns or unauthorized access attempts.