User consent workflow for secure document access with time-based expiration
CCDigital implements a consent-based access control model where citizens explicitly approve each request from authorized entities to view their documents.
Authorized entities create access requests for approved documents of a specific citizen.
1
Issuer Searches Person
Entity user navigates to /issuer/access-requests/new and searches for the citizen by identification.
2
Select Documents
System displays only APPROVED documents belonging to that person. Issuer selects one or more documents and provides a purpose.
3
Create Request
Backend validates all constraints and creates the request:
// AccessRequestService.java:126@Transactionalpublic AccessRequest createRequest(Long entityId, Long personId, String purpose, List<Long> personDocumentIds) { // Validate entity and person exist IssuingEntity entity = issuingEntityRepository.findById(entityId) .orElseThrow(() -> new IllegalArgumentException("Entidad no encontrada")); Person person = personRepository.findById(personId) .orElseThrow(() -> new IllegalArgumentException("Persona no encontrada")); AccessRequest request = new AccessRequest(); request.setEntity(entity); request.setPerson(person); request.setPurpose(purpose.trim()); request.setStatus(AccessRequestStatus.PENDIENTE); // Request expires in 15 days request.setExpiresAt(LocalDateTime.now().plusDays(15)); // Create items for each document List<AccessRequestItem> items = new ArrayList<>(); for (Long pdId : personDocumentIds) { PersonDocument pd = personDocumentRepository.findByIdWithFiles(pdId) .orElseThrow(() -> new IllegalArgumentException("Documento no encontrado")); // Validate document belongs to person if (!pd.getPerson().getId().equals(personId)) { throw new IllegalArgumentException("El documento no pertenece a la persona"); } // Validate document is approved if (pd.getReviewStatus() != ReviewStatus.APPROVED) { throw new IllegalArgumentException("El documento aún no está aprobado para consulta"); } AccessRequestItem item = new AccessRequestItem(); item.setAccessRequest(request); item.setPersonDocument(pd); items.add(item); } request.setItems(items); AccessRequest saved = accessRequestRepository.save(request); // Record audit event in Fabric recordAuditBestEffort(saved, null, null, "REQUEST_CREATED", "OK", "Solicitud de acceso creada por entidad emisora", "CREATE_REQUEST", "ISSUER", String.valueOf(entityId)); return saved;}
4
Notification
Citizen receives notification (implementation-specific) that a new request is pending at /user/requests.
Document Approval Requirement: Only documents with review_status = APPROVED can be included in access requests. Attempting to request PENDING or REJECTED documents will fail validation.
Citizens review and approve or reject access requests through their user dashboard.
1
Review Request
Citizen logs in with Indy proof and views pending requests at /user/requests.Request details include:
Requesting entity name
Purpose/justification
List of requested documents with titles
Expiration date
2
Make Decision
User clicks “Aprobar” or “Rechazar” with optional decision note.
// AccessRequestService.java:258@Transactionalpublic void decide(Long requestId, Long personId, boolean approve, String decisionNote) { AccessRequest request = accessRequestRepository.findByIdWithDetails(requestId) .orElseThrow(() -> new IllegalArgumentException("Solicitud no encontrada")); // Authorization: only document owner can decide if (!request.getPerson().getId().equals(personId)) { throw new IllegalArgumentException("No autorizado para decidir esta solicitud"); } // Must be in PENDIENTE status if (request.getStatus() != AccessRequestStatus.PENDIENTE) { throw new IllegalArgumentException("La solicitud ya fue decidida"); } // Check expiration if (request.getExpiresAt() != null && request.getExpiresAt().isBefore(LocalDateTime.now())) { request.setStatus(AccessRequestStatus.EXPIRADA); accessRequestRepository.save(request); throw new IllegalArgumentException("La solicitud se encuentra expirada"); } // If approving, sync to Fabric and validate if (approve) { syncApprovedPersonDocumentsToFabric(request); validateApprovedItemsAreInFabric(request); } // Update status request.setStatus(approve ? AccessRequestStatus.APROBADA : AccessRequestStatus.RECHAZADA); request.setDecidedAt(LocalDateTime.now()); request.setDecisionNote(decisionNote); accessRequestRepository.save(request);}
3
Fabric Synchronization
If approved, the system synchronizes the person’s documents to Hyperledger Fabric for blockchain traceability:
// AccessRequestService.java:497private void syncApprovedPersonDocumentsToFabric(AccessRequest request) { Person person = request.getPerson(); String idType = person.getIdType().name(); String idNumber = person.getIdNumber(); ExternalToolsService.ExecResult result = externalToolsService.runFabricSyncPerson(idType, idNumber); if (!result.isOk()) { throw new IllegalArgumentException( "No se pudo registrar la aprobación en Fabric. Intente nuevamente." ); }}
4
Validation
System validates that all requested documents are visible in Fabric ledger:
// AccessRequestService.java:519private void validateApprovedItemsAreInFabric(AccessRequest request) { List<FabricDocView> fabricDocs = loadFabricDocsForPerson(request.getPerson()); for (AccessRequestItem item : request.getItems()) { PersonDocument pd = item.getPersonDocument(); FileRecord latest = findLatestFile(pd); Optional<FabricDocView> match = findMatchingFabricDoc(fabricDocs, pd, latest); if (match.isEmpty()) { throw new IllegalArgumentException( "No se pudo validar en Fabric el documento. Intente nuevamente." ); } // Record verification event recordAuditStrict(request, pd, match.get(), "DOC_VERIFY_ON_REQUEST", "OK", "Documento validado en Fabric para aprobación de solicitud", "VERIFY_REQUEST", "USER", String.valueOf(person.getId())); }}
Blockchain Integration: Approvals require successful registration in Hyperledger Fabric. If the Fabric sync or validation fails, the approval is rolled back and the request remains PENDIENTE.
Document inclusion - Document must be in request items
Fabric presence - Document must exist in Fabric ledger
// AccessRequestService.java:326@Transactional(readOnly = true)public Resource loadApprovedDocumentResource(Long entityId, Long requestId, Long personDocumentId) { AccessRequest request = accessRequestRepository.findByIdWithDetails(requestId) .orElseThrow(() -> new IllegalArgumentException("Solicitud no encontrada")); // Authorization: entity owns request if (!request.getEntity().getId().equals(entityId)) { throw new IllegalArgumentException("No autorizado para consultar esta solicitud"); } // Status: must be approved if (request.getStatus() != AccessRequestStatus.APROBADA) { throw new IllegalArgumentException("La solicitud no está aprobada"); } // Expiration check if (request.getExpiresAt() != null && request.getExpiresAt().isBefore(LocalDateTime.now())) { throw new IllegalArgumentException("La solicitud se encuentra expirada"); } // Document must be in request items boolean requested = request.getItems().stream() .anyMatch(i -> i.getPersonDocument().getId().equals(personDocumentId)); if (!requested) { throw new IllegalArgumentException("El documento no pertenece a la solicitud"); } // Load document and latest file version PersonDocument pd = personDocumentRepository.findByIdWithFiles(personDocumentId) .orElseThrow(() -> new IllegalArgumentException("Documento no encontrado")); FileRecord latest = pd.getFiles().stream() .max(Comparator.comparingInt(fr -> fr.getVersion() != null ? fr.getVersion() : 0)) .orElseThrow(() -> new IllegalArgumentException("El documento no tiene archivos válidos")); // Validate document exists in Fabric FabricDocView matchedDoc = ensureDocumentPresentInFabric(request, pd, latest); // Load file resource Resource resource = fileStorageService.loadAsResource(latest); // Record access event in Fabric recordAuditStrict(request, pd, matchedDoc, "DOC_VIEW_GRANTED", "OK", "Consulta de documento autorizada", "VIEW_DOCUMENT", "ISSUER", String.valueOf(entityId)); return resource;}
Once approved, the expires_at timestamp continues to govern access. Entities cannot view documents after expiration even if previously approved.
Expiration Enforcement: Every document access attempt checks expiration in real-time. Expired requests immediately return an error even if the underlying approval exists.
// FabricAuditCliService.javapublic record AuditCommand( String idType, // CC, TI, etc. String idNumber, // Person's ID String eventType, // DOC_VIEW_GRANTED, etc. Long accessRequestId, // Request ID Long personDocumentId, // Document ID String fabricDocId, // Fabric ledger reference String documentTitle, // Human-readable title Long entityId, // Requesting entity ID String entityName, // Entity name String action, // VIEW_DOCUMENT, etc. String result, // OK or FAIL String reason, // Explanation or error String actorType, // ISSUER, USER, ADMIN String actorId, // Actor identifier String source // CCDIGITAL_SPRING) {}
Location:src/main/java/co/edu/unbosque/ccdigital/service/AccessRequestService.javaCentral service implementing the complete access request lifecycle: creation, consent decision, authorization validation, document loading, and Fabric audit recording.
SignedUrlService.java
Location:src/main/java/co/edu/unbosque/ccdigital/service/SignedUrlService.javaGenerates and validates HMAC-SHA256 signed URLs with time-based expiration for secure document links.
FabricLedgerCliService.java
Location:src/main/java/co/edu/unbosque/ccdigital/service/FabricLedgerCliService.javaQueries Hyperledger Fabric ledger for document listings and metadata via list-docs.js Node.js script.
FabricAuditCliService.java
Location:src/main/java/co/edu/unbosque/ccdigital/service/FabricAuditCliService.javaRecords access events to Fabric via record-access-event.js for immutable audit trail.
Authorization First: All access operations validate authorization BEFORE loading file resources. Unauthorized attempts are logged but never return file data.
Blockchain Verification: Every document access validates the document’s presence in Fabric ledger, ensuring traceability and preventing access to documents that haven’t been properly registered.
Path Traversal Protection: File paths are validated against allowed base directories by FileStorageService to prevent unauthorized file system access.