Skip to main content
CCDigital implements a consent-based access control model where citizens explicitly approve each request from authorized entities to view their documents.

Access Request Model

The access control system is built on two core entities:

Access Request

A formal request from an issuing entity to view specific documents of a citizenEntity: AccessRequest.javaTable: access_requests

Request Items

Individual documents included in the access requestEntity: AccessRequestItem.javaTable: access_request_items

Entity Structure

// AccessRequest.java
@Entity
@Table(name = "access_requests")
public class AccessRequest {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @ManyToOne(optional = false)
    @JoinColumn(name = "entity_id")
    private IssuingEntity entity; // Requesting organization
    
    @ManyToOne(optional = false)
    @JoinColumn(name = "person_id")
    private Person person; // Document owner
    
    @Column(name = "purpose", length = 300)
    private String purpose; // Justification for request
    
    @Enumerated(EnumType.STRING)
    @Column(name = "status")
    private AccessRequestStatus status; // PENDIENTE, APROBADA, RECHAZADA, EXPIRADA
    
    @Column(name = "requested_at")
    private LocalDateTime requestedAt;
    
    @Column(name = "decided_at")
    private LocalDateTime decidedAt;
    
    @Column(name = "expires_at")
    private LocalDateTime expiresAt;
    
    @Column(name = "decision_note")
    private String decisionNote;
    
    @OneToMany(mappedBy = "accessRequest", cascade = CascadeType.ALL)
    private List<AccessRequestItem> items;
}

Request Creation Workflow

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
@Transactional
public 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
@Transactional
public 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:497
private 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:519
private 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 Access Controls

Once approved, entities can view and download the requested documents with strict authorization checks.

Authorization Validation

Every document access operation validates:
  1. Request ownership - Entity must own the request
  2. Request status - Must be APROBADA
  3. Expiration - Must not be expired
  4. Document inclusion - Document must be in request items
  5. 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;
}

Signed URL Generation

Document links use cryptographically signed URLs with time-based expiration:
// SignedUrlService.java
@Service
public class SignedUrlService {
    
    @Value("${app.security.signed-urls.secret}")
    private String secret;
    
    @Value("${app.security.signed-urls.ttl-seconds:3600}")
    private int ttlSeconds;
    
    public String generateSignedUrl(String basePath, Map<String, String> params) {
        long expiresAt = System.currentTimeMillis() / 1000 + ttlSeconds;
        params.put("expires", String.valueOf(expiresAt));
        
        String signature = calculateHmacSha256(buildQueryString(params), secret);
        params.put("signature", signature);
        
        return basePath + "?" + buildQueryString(params);
    }
    
    public boolean validateSignedUrl(Map<String, String> params) {
        String providedSignature = params.remove("signature");
        String expiresStr = params.get("expires");
        
        // Check expiration
        long expires = Long.parseLong(expiresStr);
        if (System.currentTimeMillis() / 1000 > expires) {
            return false;
        }
        
        // Verify signature
        String expectedSignature = calculateHmacSha256(buildQueryString(params), secret);
        return MessageDigest.isEqual(
            expectedSignature.getBytes(), 
            providedSignature.getBytes()
        );
    }
}
Endpoints:
  • View: /issuer/access-requests/{requestId}/documents/{personDocumentId}/view
  • Download: /issuer/access-requests/{requestId}/documents/{personDocumentId}/download

Access Request States

StatusDescription
PENDIENTEAwaiting citizen’s decision (initial state)
APROBADACitizen approved, entity can access documents
RECHAZADACitizen rejected, no access granted
EXPIRADARequest expired before decision or after approval period

Time-Based Expiration

Access requests have two expiration scenarios:

Pre-Decision Expiration

Requests expire 15 days from creation if not decided:
// AccessRequestService.java:151
request.setExpiresAt(LocalDateTime.now().plusDays(15));
If a citizen attempts to decide an expired pending request, it’s automatically marked as EXPIRADA.

Post-Approval Expiration

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.

Audit Trail

All access events are recorded in Hyperledger Fabric for immutable audit logging:

Recorded Events

  • REQUEST_CREATED - New access request initiated
  • DOC_VERIFY_ON_REQUEST - Document validation during approval
  • DOC_VIEW_GRANTED - Document viewed by authorized entity
  • DOC_DOWNLOAD_GRANTED - Document downloaded by authorized entity
  • DOC_BLOCK_TRACE_QUERY - Blockchain trace metadata queried
  • DOC_ACCESS_CHECK - Access authorization validation

Audit Command Structure

// FabricAuditCliService.java
public 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
) {}
Script: record-access-event.js (Fabric Node.js client)

Fabric Document Matching

The system matches database documents to Fabric ledger entries using multiple strategies:
// AccessRequestService.java:643
private Optional<FabricDocView> findMatchingFabricDoc(List<FabricDocView> fabricDocs,
                                                      PersonDocument pd, 
                                                      FileRecord latest) {
    String dbRelativePath = normalizePath(latest.getStoragePath());
    String title = pd.getDocumentDefinition().getTitle();
    
    return fabricDocs.stream()
        .filter(doc -> matchesFilePath(doc.filePath(), dbRelativePath) 
                    || matchesTitle(doc.title(), title))
        .findFirst();
}

private boolean matchesFilePath(String fabricPath, String dbRelativePath) {
    String fabricNorm = normalizePath(fabricPath);
    String dbNorm = normalizePath(dbRelativePath);
    
    return fabricNorm.equals(dbNorm)
        || fabricNorm.endsWith("/" + dbNorm)
        || fabricNorm.endsWith(dbNorm);
}
Matching order:
  1. Path matching (exact or suffix match handling absolute vs relative paths)
  2. Title matching (case-insensitive fallback)

Key Services

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.
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.
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.
Location: src/main/java/co/edu/unbosque/ccdigital/service/FabricAuditCliService.javaRecords access events to Fabric via record-access-event.js for immutable audit trail.

Security Considerations

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.

Build docs developers (and LLMs) love