Skip to content

Integrare multi-protocol — Registrul Interdicțiilor

Specificație tehnică pentru cerința de interoperabilitate (caiet_de_sarcini.md §6, CF-11): aplicația trebuie consumabilă dinamic, din orice limbaj de programare (Java, .NET, Python, Go și altele), prin trei protocoale paralele: REST/OpenAPI, gRPC și TCP raw.

Acest document e scris pentru dezvoltatori — atât echipa internă, cât și integratorii externi (bănci, ANAF, alte sisteme govtech) care vor consuma registrul. Pentru cerințele de business, vezi caiet_de_sarcini.md. Pentru arhitectura de bază a aplicației, vezi platform_design.md.


Principiu nenegociabil: toate cele trei protocoale apelează același strat de servicii de business (service.*Service din arhitectura existentă, vezi CLAUDE.md). Niciun protocol nu reimplementează validări, tranziții de stare sau reguli de scope — fiecare e doar un front de transport diferit peste aceeași logică.

┌─────────────────────────────────────┐
│ service.*Service (+ impl/) │
│ — logica de business unică — │
│ validări, tranziții de stare, │
│ scope ActiuneInterzisa, audit │
└───────────────┬───────────────────────┘
┌─────────────────────┼─────────────────────┐
│ │ │
┌──────────▼─────────┐ ┌─────────▼──────────┐ ┌────────▼───────────┐
│ REST Controller │ │ gRPC Service impl │ │ TCP Frame Handler │
│ web.rest.*Resource │ │ grpc.*ServiceImpl │ │ tcp.*FrameHandler │
│ port 8080 │ │ port 9090 │ │ port 9091 │
└──────────┬─────────┘ └─────────┬──────────┘ └────────┬───────────┘
│ │ │
HTTP/JSON gRPC/protobuf TCP/length-prefixed JSON
│ │ │
┌───────▼───────┐ ┌────────▼────────┐ ┌───────▼────────┐
│ Client Java/ │ │ Client Java/ │ │ Client Java/ │
│ .NET/Python/Go │ │ .NET/Python/Go │ │ .NET/Python/Go │
│ (SDK generat) │ │ (stub generat) │ │ (socket manual) │
└───────────────┘ └─────────────────┘ └─────────────────┘

Toate trei front-urile trăiesc într-un modul nou integration (sau pachete web.rest existent + grpc + tcp noi), fără duplicare de DTO-uri de business — reutilizează service.dto existent unde e posibil.


flowchart TB
subgraph Clients["Clienți externi (orice limbaj)"]
JavaC["Java"]
DotnetC[".NET"]
PyC["Python"]
GoC["Go"]
end
subgraph Frontends["Fronturi de protocol (md.gov.interdictii)"]
REST["REST Controllers<br/>:8080 /api/v1/*"]
GRPC["gRPC Services<br/>:9090"]
TCP["TCP Frame Handler<br/>:9091"]
end
subgraph Core["Strat comun"]
Auth["AuthN/AuthZ adapter<br/>(JWT / API key / mTLS / token)"]
RateLimit["Rate limiter comun"]
ErrMap["Mapare erori unitară"]
Svc["service.*Service<br/>(business logic unică)"]
AuditLog["Audit log consultări externe"]
end
JavaC & DotnetC & PyC & GoC -->|HTTPS+JSON| REST
JavaC & DotnetC & PyC & GoC -->|HTTP/2+protobuf| GRPC
JavaC & DotnetC & PyC & GoC -->|TCP socket| TCP
REST --> Auth
GRPC --> Auth
TCP --> Auth
Auth --> RateLimit --> Svc
Svc --> AuditLog
REST --> ErrMap
GRPC --> ErrMap
TCP --> ErrMap

  • Toate rutele sub /api/v1/... (versionare explicită în path, pentru compatibilitate pe termen lung).
  • Specificația OpenAPI 3.x generată automat din controllere via springdoc-openapi (compatibil nativ cu stack-ul Spring Boot 3 existent), publicată la /v3/api-docs și UI la /swagger-ui.html.
  • Endpoint-urile interne (portal operator) și cele publice (consultare externă) sunt documentate în aceeași spec, dar grupate sub tag-uri separate (internal, public) pentru claritate la generarea SDK-urilor.
ConsumatorMecanism
Portal Angular (intern)JWT (HS512), conform fluxului existent /api/authenticate
Sisteme externe (bănci, ANAF, etc.)API key (header X-Api-Key) sau OAuth2 client-credentials (token bearer), configurabil per consumator

Comenzi de referință (openapi-generator-cli, rulate din CI/CD după fiecare release de API):

Terminal window
openapi-generator-cli generate -i openapi.yaml -g java -o sdk/java --additional-properties=library=resttemplate
openapi-generator-cli generate -i openapi.yaml -g csharp -o sdk/dotnet --additional-properties=targetFramework=net8.0
openapi-generator-cli generate -i openapi.yaml -g python -o sdk/python
openapi-generator-cli generate -i openapi.yaml -g go -o sdk/go

Publicare: pachetele generate se publică în registry-ul intern (GitLab Package Registry, pe git.esempla.systems/govtech/gstack/* — repo-ul SDK- urilor poate fi propriu, ex. saas_interdictii-sdk), nu pe registry-uri publice (Maven Central, NuGet, PyPI, Go proxy public), din motive de suveranitate a datelor (CNF-9 din caiet de sarcini).

GET /api/v1/public/interdictii?identificator=2002004001234 HTTP/1.1
Host: interdictii.gov.md
X-Api-Key: <api-key-consumator>
{
"rezultate": [
{
"interdictieId": 4821,
"statut": "ACTIV",
"categorieDomeniu": "PENAL",
"startDate": "2026-01-15",
"endDate": null,
"autoritate": "Judecătoria Chișinău",
"actiuniInterzise": [
{ "actiune": "IESIRE_TARA", "intensitate": "TOTALA" }
]
}
]
}

Câmpurile sensibile (ex. description, detalii atribute interne) sunt mascate conform CF-10.2 din caietul de sarcini — nu apar în răspunsul public.


Mapare 1:1 pe domeniul existent. Exemplu (interdictie.proto):

syntax = "proto3";
package md.gov.interdictii.grpc;
option java_package = "md.gov.interdictii.grpc";
option csharp_namespace = "Md.Gov.Interdictii.Grpc";
service InterdictieService {
rpc GetByIdentificator(IdentificatorRequest) returns (InterdictieListResponse);
rpc StreamEvenimente(InterdictieIdRequest) returns (stream EvenimentInterdictie);
}
message IdentificatorRequest {
string identificator = 1;
string categorie = 2; // optional filtru CategorieEntitate
}
message InterdictieListResponse {
repeated Interdictie rezultate = 1;
}
message Interdictie {
int64 id = 1;
string statut = 2;
string categorieDomeniu = 3;
string startDate = 4;
string endDate = 5;
string autoritate = 6;
}
message EvenimentInterdictie {
string tip = 1;
string actor = 2;
string data = 3;
}
message InterdictieIdRequest {
int64 interdictieId = 1;
}
  • grpc-spring-boot-starter (sau grpc-server-spring-boot-starter de la net.devh) — server gRPC rulat ca bean Spring Boot, port separat 9090, în același proces ca aplicația principală (monolith, fără serviciu nou de deploy).
  • Implementarea InterdictieServiceImpl extends InterdictieServiceGrpc.InterdictieServiceImplBase apelează direct service.InterdictieService existent — zero logică duplicată.
Terminal window
protoc --java_out=sdk/java/grpc --grpc-java_out=sdk/java/grpc interdictie.proto
protoc --csharp_out=sdk/dotnet/grpc --grpc_out=sdk/dotnet/grpc interdictie.proto
python -m grpc_tools.protoc -I. --python_out=sdk/python/grpc --grpc_python_out=sdk/python/grpc interdictie.proto
protoc --go_out=sdk/go/grpc --go-grpc_out=sdk/go/grpc interdictie.proto
  • mTLS pentru consumatori server-to-server cu certificat emis de CA internă, sau token bearer transmis prin gRPC metadata (authorization: Bearer <token>) — alegere per consumator, documentată în contractul de onboarding.

Pentru consumatori cu cerințe de latență minimă și volum mare (ex. o bancă care verifică zilnic zeci de mii de IDNP-uri în batch, sau verificare în timp real la fiecare tranzacție), overhead-ul HTTP (headere, TLS handshake per request fără keep-alive corect configurat, parsare HTTP) poate fi nejustificat. Protocolul TCP raw permite o conexiune persistentă, cu framing minimal și parsare JSON simplă — implementabilă nativ în orice limbaj cu suport de socket-uri (Java, .NET, Python, Go au toate API de socket TCP în biblioteca standard, fără dependențe externe).

┌────────────────────────────┬──────────────────────────────────┐
│ 4 bytes, big-endian uint32 │ N bytes, UTF-8 JSON │
│ = lungimea payload-ului │ = payload-ul mesajului │
└────────────────────────────┴──────────────────────────────────┘
  • Lungimea nu include cei 4 bytes de prefix, doar payload-ul JSON.
  • Encoding text: UTF-8 (necesar pentru caractere românești în câmpuri precum numeAfisat).
  • Limită maximă de payload: 1 MiB per mesaj (protecție DoS) — mesaje mai mari sunt respinse cu ERROR / PAYLOAD_TOO_LARGE.
{
"id": "uuid-sau-secventa-client",
"type": "REQUEST | RESPONSE | EVENT | ERROR",
"op": "string — numele operațiunii",
"payload": { },
"authToken": "string — doar pe primul mesaj de pe conexiune (handshake)"
}
  • id — generat de client pe REQUEST, ecou-uit identic pe RESPONSE/ ERROR corespunzător, pentru a permite pipelining (mai multe request-uri in-flight pe aceeași conexiune, fără a aștepta răspunsul fiecăruia).
  • authToken — obligatoriu doar pe primul mesaj trimis de client după conectare (handshake). Serverul validează și asociază token-ul sesiunii TCP; mesajele următoare pe aceeași conexiune nu retrimit token-ul.
opDescrierepayload requestpayload response
AUTHHandshake — prima operație obligatorie pe conexiune{ "token": "..." }{ "ok": true, "sessionId": "..." }
CHECK_INTERDICTIEVerifică dacă o entitate are interdicție activă{ "identificator": "...", "categorie": "PF" }{ "areInterdictie": true, "interdictii": [...] }
GET_BY_IDNPListă completă interdicții active pentru IDNP{ "idnp": "2002004001234" }{ "rezultate": [...] }
GET_BY_IDNOIdem pentru IDNO (persoană juridică){ "idno": "..." }{ "rezultate": [...] }
SUBSCRIBE_EVENTSAbonare la evenimente noi (push asincron de tip EVENT pe aceeași conexiune){ "filtru": { "categorieDomeniu": "FISCAL" } }{ "subscribed": true } urmat de mesaje EVENT
PINGKeep-alive{}{ "pong": true }

Răspunsurile de eroare folosesc type: "ERROR" cu payload { "code": "STRING_CODE", "message": "..." } — coduri partajate cu celelalte două protocoale (vezi §6).

  • Bază: Netty (preferat pentru throughput și model non-blocking) sau Java NIO direct, ca bean Spring Boot independent, rulat pe port propus 9091, pornit/oprit împreună cu ciclul de viață al aplicației (SmartLifecycle).
  • Fiecare conexiune TCP = o sesiune; după AUTH reușit, sesiunea e legată de identitatea consumatorului (pentru rate limiting și audit).
  • Thread model: event loop Netty (sau pool dedicat dacă NIO simplu), delegă procesarea business către service.*Service pe un executor separat pentru a nu bloca I/O.
  • Server trimite/așteaptă PING/PONG la fiecare 30s; absența unui PONG în 90s → server închide conexiunea.
  • Clienții implementează reconectare cu backoff exponențial (ex. 1s, 2s, 4s, max 30s) și re-AUTH automat la reconectare.

Java:

Socket socket = new Socket("interdictii.gov.md", 9091);
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
byte[] json = "{\"id\":\"1\",\"type\":\"REQUEST\",\"op\":\"AUTH\",\"authToken\":\"...\"}"
.getBytes(StandardCharsets.UTF_8);
out.writeInt(json.length); // big-endian implicit cu DataOutputStream
out.write(json);

C# (.NET):

using var client = new TcpClient("interdictii.gov.md", 9091);
using var stream = client.GetStream();
var json = Encoding.UTF8.GetBytes("{\"id\":\"1\",\"type\":\"REQUEST\",\"op\":\"AUTH\",\"authToken\":\"...\"}");
var lenPrefix = BitConverter.GetBytes(json.Length);
Array.Reverse(lenPrefix); // big-endian
await stream.WriteAsync(lenPrefix); await stream.WriteAsync(json);

Python:

import socket, struct, json as js
s = socket.create_connection(("interdictii.gov.md", 9091))
payload = js.dumps({"id": "1", "type": "REQUEST", "op": "AUTH", "authToken": "..."}).encode("utf-8")
s.sendall(struct.pack(">I", len(payload)) + payload)

Go:

conn, _ := net.Dial("tcp", "interdictii.gov.md:9091")
payload, _ := json.Marshal(map[string]string{"id": "1", "type": "REQUEST", "op": "AUTH", "authToken": "..."})
lenBuf := make([]byte, 4)
binary.BigEndian.PutUint32(lenBuf, uint32(len(payload)))
conn.Write(lenBuf)
conn.Write(payload)

Toate patru exemplele demonstrează același framing — niciun limbaj necesită biblioteci externe pentru a vorbi acest protocol.


Indiferent de protocol, erorile de business folosesc același set de coduri:

CodSensHTTP status echivalentgRPC status echivalent
NOT_FOUNDEntitate/interdicție inexistentă404NOT_FOUND
INVALID_TRANSITIONTranziție de statut nepermisă409FAILED_PRECONDITION
VALIDATION_ERRORDate de intrare invalide400INVALID_ARGUMENT
UNAUTHENTICATEDToken/API key invalid sau absent401UNAUTHENTICATED
RATE_LIMITEDLimită de request-uri atinsă429RESOURCE_EXHAUSTED
PAYLOAD_TOO_LARGEMesaj TCP peste limita de 1 MiB413INVALID_ARGUMENT
INTERNALEroare neașteptată server500INTERNAL
  • Un singur serviciu de rate limiting (ex. bucket per consumator, identificat prin API key / mTLS subject / token de sesiune TCP), consultat de toate cele trei fronturi înainte de a apela service.*Service.
  • Limitele implicite (configurabile per consumator): N request-uri/secundă pentru REST/gRPC, N mesaje/secundă pentru TCP.

6.3 Audit logging al consultărilor externe

Section titled “6.3 Audit logging al consultărilor externe”

Reia fluxul deja documentat în platform_design.md §5.3 (activity) și §6.3 (sequence) pentru consultarea publică — extins să acopere uniform toate cele trei protocoale: fiecare consultare externă (cine, ce identificator, când, prin ce protocol) e logată în jurnalul de audit existent.


ProtocolAutentificareTransport
REST (portal intern)JWT HS512HTTPS
REST (extern)API key sau OAuth2 client-credentialsHTTPS
gRPCmTLS sau token în metadataHTTP/2 + TLS
TCP rawToken în mesajul AUTH (handshake)TLS opțional (recomandat TLS peste socket pentru producție — SSLSocket/echivalent per limbaj)

  • OpenAPI: versionare semantică în path (/api/v1, /api/v2 la breaking change); spec publicată la fiecare release.
  • .proto: câmpuri noi doar adăugate (niciodată renumerotate/șterse) — regulă standard protobuf de backward-compatibility.
  • Schema JSON TCP: câmpuri noi opționale; op-uri noi pot fi adăugate fără breaking change; eliminarea unui op necesită perioadă de deprecation anunțată (minim 6 luni) consumatorilor înregistrați.

  • Contract testing: spec OpenAPI + fișiere .proto ca sursă unică de adevăr pentru contractul REST/gRPC; validare automată în CI că implementarea respectă contractul (ex. openapi-generator validate mode).
  • Smoke tests per SDK generat: pipeline CI separat per limbaj (Java, .NET, Python, Go) care instanțiază SDK-ul generat și execută un apel real către mediul de testare, pentru fiecare din cele 3 protocoale.
  • Test TCP dedicat: test automat care deschide o conexiune raw, trimite AUTH + CHECK_INTERDICTIE, verifică framing-ul răspunsului byte-cu-byte.

10. Decizii deschise / propuneri pentru model (neaplicate încă)

Section titled “10. Decizii deschise / propuneri pentru model (neaplicate încă)”

Următoarele necesită confirmare explicită înainte de a fi adăugate în interdictii.jdl — nu sunt aplicate în această etapă, doar documentate ca propuneri:

  • Entitate ApiConsumer — consumator extern înregistrat (nume, organizație, protocoale permise, status activ/suspendat).
  • Entitate ApiKey (sau ApiCredential) — cheie/credențial asociat unui ApiConsumer, cu expiresAt, scopes, rateLimit.
  • Mapare ApiConsumer ↔ protocoale permise (un consumator poate fi restricționat doar la REST, sau autorizat și pentru TCP raw).
  • Tabelă/contor de rate-limit persistat (sau soluție in-memory distribuită, ex. Redis, dacă se trece la deployment orizontal — menționat deja ca opțiune în platform_design.md §7.7).

11. Roadmap implementare strat de integrare

Section titled “11. Roadmap implementare strat de integrare”

Aliniat cu platform_design.md §7.6, ca sub-fază a Fazei 1:

PasConținut
1REST + OpenAPI peste serviciile existente (/api/v1), publicare spec
2Generare + publicare SDK Java/.NET/Python/Go pentru REST, în CI
3Server gRPC (port 9090) + .proto + stub-uri generate
4Server TCP raw (port 9091) — framing, AUTH, CHECK_INTERDICTIE, GET_BY_IDNP/GET_BY_IDNO
5SUBSCRIBE_EVENTS (push asincron pe TCP) + rate limiting comun + audit unificat
6Hardening: mTLS gRPC, TLS opțional pe TCP raw, teste de contract automate în CI