SIENTIAPDE-1973
SIENTIAPDE-1973: Implement per-connector OPC client certificates and remove legacy read engine. This update overhauls certificate management, transitioning from a single platform certificate to individual client certificates defined in the slot configuration. The CertProvider now downloads certificates from MinIO based on certificate_id, applying dynamic security policies (mode, policy) from the slot. The legacy READ_ENGINE=legacy option and OpcManager have been entirely removed, standardizing on the ServerReader with strict slot v2 enforcement. Write command resolution (ServerConfigResolver) now consistently uses connectors joined with opc_certificates. The CERT_BUCKET environment variable is no longer used.
This commit is contained in:
92
ingestor/connection/certificates.py
Normal file
92
ingestor/connection/certificates.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""Client certificate of a tag database connector (SIENTIAPDE-1974 D5).
|
||||
|
||||
Mirrors the Orchestrator's `tag_database.resolve_certificate`, which puts the
|
||||
certificate in the read slot: the write lookup (ServerConfigResolver, D1) resolves a
|
||||
server that is not in the pool with the same rule, so a write uses the certificate the
|
||||
slot would have. Keep both in sync.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
# security fields of an OPC UA connector: the source of its OPC security (D4)
|
||||
SECURITY_FIELDS = ('mode', 'policy')
|
||||
CERTIFICATE_FIELDS = ('certificate_id', 'application_uri')
|
||||
CERTIFICATE_OBJECTS = ('cert', 'private_key')
|
||||
STORED_OBJECT_FIELDS = ('bucket', 'object_name')
|
||||
|
||||
# opc_certificates lookup of a connectors aggregation: the certificate the connector's
|
||||
# security.client_certificate_id points to, only when it is active and of kind client.
|
||||
CERTIFICATE_LOOKUP = {
|
||||
'$lookup': {
|
||||
'from': 'opc_certificates',
|
||||
'let': {'certificate_id': '$security.client_certificate_id'},
|
||||
'pipeline': [
|
||||
{
|
||||
'$match': {
|
||||
'$expr': {'$eq': ['$certificate_id', '$$certificate_id']},
|
||||
'status': 'active',
|
||||
'kind': 'client',
|
||||
}
|
||||
},
|
||||
{
|
||||
'$project': {
|
||||
'_id': 0,
|
||||
'certificate_id': 1,
|
||||
'application_uri': 1,
|
||||
'cert.bucket': 1,
|
||||
'cert.object_name': 1,
|
||||
'private_key.bucket': 1,
|
||||
'private_key.object_name': 1,
|
||||
}
|
||||
},
|
||||
],
|
||||
'as': 'certificates',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _is_complete(document: Any, fields: tuple[str, ...]) -> bool:
|
||||
return isinstance(document, dict) and all(
|
||||
document.get(field) not in (None, '') for field in fields
|
||||
)
|
||||
|
||||
|
||||
def _is_usable_certificate(certificate: Any) -> bool:
|
||||
return _is_complete(certificate, CERTIFICATE_FIELDS) and all(
|
||||
_is_complete(certificate.get(name), STORED_OBJECT_FIELDS) for name in CERTIFICATE_OBJECTS
|
||||
)
|
||||
|
||||
|
||||
def client_certificate_id(connector: dict[str, Any]) -> Any:
|
||||
"""The connector's security.client_certificate_id (None when it has none)."""
|
||||
security = connector.get('security')
|
||||
return security.get('client_certificate_id') if isinstance(security, dict) else None
|
||||
|
||||
|
||||
def has_security(connector: dict[str, Any]) -> bool:
|
||||
"""Whether the connector has security.mode and security.policy (D4)."""
|
||||
return _is_complete(connector.get('security'), SECURITY_FIELDS)
|
||||
|
||||
|
||||
def resolve_certificate(connector: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""
|
||||
The client certificate of a connector from CERTIFICATE_LOOKUP, or None (D5).
|
||||
|
||||
None means no certificate: the connection is opened without security. A connector
|
||||
whose client_certificate_id is set but has no usable certificate is most likely a
|
||||
registration error; the caller reports it (see client_certificate_id).
|
||||
|
||||
Returns:
|
||||
dict[str, Any] | None: certificate_id, application_uri, and cert and private_key
|
||||
as {bucket, object_name}, as in the slot
|
||||
"""
|
||||
for certificate in connector.get('certificates') or []:
|
||||
if _is_usable_certificate(certificate):
|
||||
return {
|
||||
**{field: certificate[field] for field in CERTIFICATE_FIELDS},
|
||||
**{
|
||||
name: {field: certificate[name][field] for field in STORED_OBJECT_FIELDS}
|
||||
for name in CERTIFICATE_OBJECTS
|
||||
},
|
||||
}
|
||||
return None
|
||||
Reference in New Issue
Block a user