diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 540867c..c0687ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: run: python3 setup.py sdist - name: Install Python module - run: pip3 install ./dist/microesb-1.0.tar.gz + run: pip3 install ./dist/microesb-1.1.tar.gz - name: Run tests run: pytest --cov --cov-branch --cov-report=xml diff --git a/CHANGELOG.md b/CHANGELOG.md index d7bdd4f..ebb8956 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog + +## Version 1.1 + +- Add user-based service call routing via ServiceRouter class +- Introduce `property_dict` functionality for flexible property access excluding internal system properties +- Support recursive class hierarchy object deserialization in ServiceExecuter +- Improve JSONTransformer robustness with `_SYSClassNames` tracking for hierarchy processing +- Add property registration feature for internal system properties via `register_property()` +- Modify Example 02 to use NoSQL MongoDB backend for certificate management +- Add Example 04 with NLAP proxy integration documentation +- Add comprehensive unit and integration tests for recursive json_transform() +- Cleanup recursive hierarchy processing code in `execute_get_hierarchy()` +- Update documentation for version 1.1 features + ## Version 1.0 - Working pylint checks diff --git a/README.md b/README.md index a587ceb..9e9d178 100644 --- a/README.md +++ b/README.md @@ -8,16 +8,16 @@ *Enterprise Service Bus* is still a pretty vague term, first introduced in the Gartner Report of 2002. -It is essential for running a large SOA infrastructure. +It is essential for maintaining large SOA infrastructures. ## 2. Features Our interpretation of what an ESB should consist of: -- ✅ Service Abstraction / Metadata Definition -- ✅ Centralized Service / API Registry containing clean XML, JSON Model +- ✅ Service Abstraction / Declarative Metadata Modeling +- ✅ Centralized Service / API Registry providing clean XML, JSON Models - ✅ Centralized Service AAA (Authentication / Authorization / Accounting) -- ✅ Internal Service XML / (Python) Class Mapping +- ✅ Service Metadata XML, JSON / Internal (Python) Class Abstraction - ✅ Relational Backend OOP / ORM / ODM Mapper - ✅ Service Model Documentation / API (Auto)-Generation @@ -40,23 +40,24 @@ pip3 install microesb pip3 install pytest pytest-pep8 ``` -## 4. Platform As A Service (PaaS) - -Building web applications on PaaS infrastructure also relies on a clean Service Abstraction Model. +## 4. Platform As A Service (PaaS) / Microservices -> **Note** -> The Python **micro-esb** module will help. +The NoSQL conform JSON abstraction / data transformation capabilities make the micro-esb +suitable for modern, scalable Next-Level applications. ## 5. Current Features - ✅ Service Abstraction / Metadata Definition - ✅ Internal Code (Python) Class / Service Properties Mapping - ✅ Graph-Based / Recursive JSON Result Abstraction +- ✅ OOP Relational ODM Mapper / MongoDB Integration ### 5.1. In Progress -- :hourglass: OOP Relational ODM Mapper - :hourglass: Service Documentation (Auto)-Generation +- :hourglass: Service Registry / Encapsulated Service Routing +- :hourglass: YANG Model Import / Export / Transformation +- :hourglass: Web-Interface / Dashboard ## 6. Documentation / Examples diff --git a/doc/source/api.rst b/doc/source/api.rst index fbf0347..3371220 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -22,19 +22,59 @@ It provides template functions described in the *Internal Classes / Class Repres It inherits `transformer.JSONTransformer` to enable a generic, recursive dictionary representation of the class instance hierarchy. This feature allows for efficient assertions on complex object hierarchies. -Example test case (`/test/integration/test_base.py`, test: `test_multi_item_object`): +Example test case (`/test/integration/test_json_transform.py`, test: `test_list_object`): .. code-block:: python - assert shipment.json_dict == { + root_object = r[0]['Shipment']['object_instance'] + + assert root_object.json_dict == { 'id': 'testshipment1', - 'SYSServiceMethod': None, - 'Palette': [ - {'id': 1, 'label': 'label1'}, - {'id': 2, 'label': 'label2'} - ] + 'Palette': { + 'Palette': [ + { 'id': 1, 'label': 'label1' }, + { 'id': 2, 'label': 'label2' } + ] + } } +1.1. Property Dict +****************** + +The ``property_dict`` property (decorated with ``@property``) provides access to all class instance +properties excluding internal system properties like ``SYSServiceMethod`` and class references. + +This enables clean property access for serialization, logging, or external API integration without +exposing internal framework properties. + +Example usage: + +.. code-block:: python + + # access all user-defined properties + properties = instance.property_dict + # returns: {'id': 'value', 'name': 'test', ...} + # excludes: 'SYSServiceMethod' and internal references + +1.2. Property Registration +*************************** + +Properties can be registered with metadata using the ``register_property()`` method. This is useful for defining internal system properties that should be handled specially by the framework. + +Example from ``/example/02-pki-management/service_implementation.py``: + +.. code-block:: python + + self.register_property( + property_id='is_ca_cert', + property_data={ + 'type': 'bool', + 'default': True, + 'required': False, + 'description': 'Is CA Certificate' + } + ) + 2. Class Handler ================ @@ -81,6 +121,8 @@ Example: Standard `ClassHandler` vs. `MultiClassHandler` implementation: For an example *service call metadata* (Host properties), see Example 1 (:ref:`example-number1`). +.. _implementation-classes: + 4. Implementation Classes ========================= @@ -165,31 +207,22 @@ All *implementation classes* must be imported into the global Python namespace. from microesb import microesb -Importing the main `microesb` module will call `import esbconfig` (see the following code, line 14). +Importing the main `microesb` module will call `import esbconfig` (see the following code, line 5). .. code-block:: python :linenos: - # ]*[ --------------------------------------------------------------------- ]*[ - # . Micro ESB Python Module . - # ]*[ --------------------------------------------------------------------- ]*[ - # . . - # . Copyright Claus Prüfer (2016 - 2024) . - # . . - # . . - # ]*[ --------------------------------------------------------------------- ]*[ - import abc import sys import logging import importlib import esbconfig -The **microesb** standard installation will include an empty `esbconfig.py` in the global Python dist-packages or in the active Python environment. +The **microesb** default installation will include an empty `esbconfig.py` in the global Python dist-packages for working pytest tests. .. warning:: - If you do not provide an `esbconfig.py` in your project folder, the default installation file will be used. + If you do not provide an `esbconfig.py` in your project folder, the default installation file will be imported. The following **esbconfig.py** tells the `microesb` importer to use the file **service_classes.py** and import *implementation classes* `Class1`, `Class2`, and `Class3` into the global namespace. These classes will then be usable by `ClassMapper` and `ServiceMapper` during internal processing. @@ -239,7 +272,7 @@ The `ServiceMapper` class is responsible for mapping and populating the existing It requires the following as input parameters: - A reference to the `ClassMapper` instance -- **Service Call Metadata Dictionary** (see "Configuration / :ref:`service-call-metadata-config`") +- **Service Call Metadata Dictionary** (Metadata passed to the ServiceExecuter as input data) Example: @@ -251,3 +284,80 @@ Example: class_mapper=class_mapper, service_data=service_metadata ) + +7. Service Router +================= + +The `ServiceRouter` class provides user-defined service call routing capabilities. It enables direct routing of service calls to user-defined functions in a ``user_routing.py`` module. + +This is particularly useful for: + +- Database CRUD operations (MongoDB, PostgreSQL, etc.) +- External API integrations +- Custom business logic execution +- Decoupling service orchestration from implementation + +7.1. Usage +********** + +.. code-block:: python + + from microesb.router import ServiceRouter + + router = ServiceRouter() + result = router.send('FunctionName', metadata) + +The ``send()`` method dynamically invokes the specified function from the ``user_routing`` module +and passes the metadata as the first argument. + +7.2. User Routing Module +************************* + +Create a ``user_routing.py`` file in your project with functions that handle specific routing targets: + +.. code-block:: python + + from pymongo import MongoClient + + client = MongoClient('mongodb://127.0.0.1/') + mongodb = client.get_database('mydb') + + def GetDataById(metadata): + return mongodb.collection.find_one({"id": metadata}) + + def StoreData(metadata): + mongodb.collection.insert_one(metadata) + return True + +See the :ref:`routing` section for detailed routing documentation. + +8. Service Executer +=================== + +The `ServiceExecuter` class handles the execution of service methods and manages recursive class hierarchy object deserialization. This enables complex hierarchical data structures to be properly reconstructed from JSON representations. + +8.1. Recursive Deserialization +******************************* + +The framework automatically deserializes nested class hierarchies, ensuring that: +- Child class instances are properly instantiated +- Parent-child relationships are maintained +- JSON data is correctly mapped to class properties + +This is particularly useful when working with complex domain models that include multiple levels of nested objects. + +Example: + +.. code-block:: python + + # hierarchical structure: CertClient -> CertServer -> CertCA + # the framework automatically deserializes all levels + + service_executer = microesb.ServiceExecuter( + class_mapper=class_mapper, + service_mapper=service_mapper + ) + + result = service_executer.execute_get_hierarchy() + root_object = r[0]['RootObjectName']['object_instance'] + print(root_object.json_dict) diff --git a/doc/source/conf.py b/doc/source/conf.py index 80af39f..5b31cf3 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -12,8 +12,8 @@ project = 'Python microesb' copyright = '2016 - 2024, Claus Prüfer' author = 'Claus Prüfer' -version = '1.0' -release = '1.0' +version = '1.1' +release = '1.1' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/doc/source/conf.rst b/doc/source/conf.rst index 7f7bf56..718af6e 100644 --- a/doc/source/conf.rst +++ b/doc/source/conf.rst @@ -289,7 +289,9 @@ Example configuration: } } -5.3. Test Execution +.. _example-test-execution: + +2.7. Test Execution ******************* Before executing the configuration, ensure all referenced files are correctly provided: @@ -322,3 +324,101 @@ Example execution: class_mapper=class_mapper, service_data=service_metadata ) + +.. _class-mapping-config: + +3. Class Mappings +================= + +The *Class Mapping Config* dictionary defines the mapping between class names used in the *Class Reference Config* and the actual *Implementation Classes*. + +3.1. Dictionary Format +********************** + +The *Class Mapping Config* is a simple dictionary where each key represents a class name used in the configuration, and each value represents the corresponding *Implementation Class* name. + +.. code-block:: python + + class_mapping = { + '$config_class_name1': '$implementation_class_name1', + '$config_class_name2': '$implementation_class_name2', + '$config_class_name3': '$implementation_class_name3' + } + +3.2. Flat Mappings +****************** + +In most cases, a "flat" mapping is used where each class name maps directly to itself. This is a 1:1 mapping without any aliasing. + +.. note:: + + All examples in this documentation use flat mappings without aliasing. + +Example from :ref:`example-number1`: + +.. literalinclude:: ../../example/01-hosting-use-case/class_mapping.py + :linenos: + +Example from :ref:`example-number2`: + +.. literalinclude:: ../../example/02-pki-management/class_mapping.py + :linenos: + +3.3. Aliasing +************* + +**Aliasing** is a more advanced technique that **must** be used when adding multiple classes with the same name as child classes to a parent class. + +With aliasing, multiple different configuration class names can map to the same *Implementation Class*. This allows you to instantiate the same class type multiple times with different property values in a hierarchical structure. + +.. warning:: + + Aliasing is only necessary when you need multiple child instances of the same class type under a single parent. For simple hierarchies, use flat mappings. + +3.4. Aliasing Example +********************* + +The integration test `TestMapping.test_class_mapping` in `/test/integration/test_base.py` demonstrates the use of aliasing: + +.. code-block:: python + + class_mapping = { + 'CertCA': 'CertCA', + 'SmartcardCA': 'Smartcard', + 'SmartcardREQ': 'Smartcard', + 'SmartcardContainer': 'SmartcardContainer' + } + +In this example: + +- `SmartcardCA` and `SmartcardREQ` are aliases that both map to the `Smartcard` implementation class. +- This allows the `CertCA` parent class to have two separate child instances of the `Smartcard` class with different configurations. +- Each alias can be referenced independently in the *Class Reference Config* and *Service Call Metadata*. + +The corresponding *Class Reference Config* shows how these aliases are used: + +.. code-block:: python + + class_reference = { + 'CertCA': { + 'property_ref': 'Cert', + 'children': { + 'SmartcardCA': { + 'property_ref': 'Smartcard', + 'children': { + 'SmartcardContainer': {'property_ref': 'SmartcardContainer'} + } + }, + 'SmartcardREQ': { + 'property_ref': 'Smartcard', + 'children': { + 'SmartcardContainer': {'property_ref': 'SmartcardContainer'} + } + } + } + } + } + +.. note:: + + For more information on aliasing in practical use, see :ref:`example-number3` in the Examples documentation. diff --git a/doc/source/design.rst b/doc/source/design.rst index 74aa433..ee6c89d 100644 --- a/doc/source/design.rst +++ b/doc/source/design.rst @@ -22,10 +22,6 @@ The following diagram illustrates the *Base Layout / Model* of a **scalable SOA The **microesb** module focuses on the "Service Mapping / Abstraction Layer" (4). -.. note:: - - The "Load Balancing Layer" and the "Transport Layer Security Layer" can be easily adapted using a Kubernetes Cluster / Ingress-nginx (see Example 2). - 2. Centralized Service Management ================================= @@ -81,7 +77,7 @@ The primary focus of the Python **microesb** module is to map a clean JSON metad .. note:: - For a detailed example that includes transactional database access, refer to Example 1. + For a detailed example that includes transactional database access, refer to Example 1. Example 2 demonstrates a NoSQL approach using MongoDB. 4. Platform As A Service ======================== diff --git a/doc/source/examples.rst b/doc/source/examples.rst index 58aca2b..cd03610 100644 --- a/doc/source/examples.rst +++ b/doc/source/examples.rst @@ -15,13 +15,15 @@ In this example, assume our "virtual" company runs a **Hosting Business**. The company's customer data, including a) Internet Domains and b) DNS Hostnames, should be manageable by different subsystems. +This example demonstrates the integration with **PostgreSQL database transactions**, showcasing how the microesb framework handles atomic operations across multiple related entities while maintaining data integrity through commit/rollback mechanisms. + .. note:: Example number 1 will only cover Local Service Mapping **without** *Python Application Server* encapsulation. .. note:: - In example number 4, we will explore these aspects further, including *Service Scaling* and *Load Balancing*. + In example number 4, we will explore these aspects further, including *Service Scaling*, *Load Balancing* and *Service Authentication*. 1.1. Basic OOP Relations ************************* @@ -194,11 +196,253 @@ After execution, the newly created domain will be in the `sys_core."domain"` tab 2. PKI Provisioning / Class Types ================================= -Example number 1 only covers a "plain" database model without local (e.g., bash) or remote (web service) invocations. +This example demonstrates PKI (Public Key Infrastructure) certificate provisioning with a focus on class type hierarchies and **NoSQL MongoDB backend integration**. -.. note:: +Unlike Example 1's relational database approach, this example showcases how the microesb framework seamlessly integrates with document-based NoSQL databases. MongoDB is used for storing and retrieving certificate metadata, demonstrating the framework's flexibility in handling both traditional and modern database paradigms. + +The example implements a complete certificate generation workflow for: + +- **Certificate Authority (CA)** certificates +- **Server** certificates +- **Client** certificates + +Each certificate type can optionally use Hardware Security Module (HSM) / Smartcard integration for secure key pair generation. The implementation uses user-defined routing functions to interact with MongoDB for certificate storage and retrieval operations. + +2.0.1. MongoDB Collections +*************************** + +The example uses two MongoDB collections within the ``microesb`` database: + +a) **cert** - Stores certificate properties as *flat* key/value pairs for basic certificate metadata +b) **cert_hierarchy** - Contains complete hierarchical JSON data representing the full certificate object graph including relationships to CA, Server, and Smartcard entities + +The ``cert`` collection is used for simple queries by certificate ID, while ``cert_hierarchy`` stores the complete object hierarchy for complex certificate relationships and dependency tracking. + +2.0.2. MongoDB Collection Examples +*********************************** + +The following examples illustrate the structure of documents stored in the ``cert`` and ``cert_hierarchy`` collections. + +**CA Certificate (Flat Storage):** + +.. code-block:: json + + { + "_id": { + "$oid": "695e5b5c65ceda39b9c5388e" + }, + "id": "test-ca1", + "country": "DE", + "state": "Berlin", + "locality": "Berlin", + "org": "WEBcodeX", + "org_unit": "Security", + "common_name": "testca@domain.com", + "email": "pki@webcodex.de", + "valid_days": 365, + "generation_timestamp": "2026-01-07T14:10:52.927127", + "cert_data": "dummy_cacert_data" + } + +**CA Certificate (Hierarchical Storage with Smartcard):** + +.. code-block:: json + + { + "_id": { + "$oid": "695e5b5c65ceda39b9c5388f" + }, + "id": "test-ca1", + "country": "DE", + "state": "Berlin", + "locality": "Berlin", + "org": "WEBcodeX", + "org_unit": "Security", + "common_name": "testca@domain.com", + "email": "pki@webcodex.de", + "valid_days": 365, + "generation_timestamp": "2026-01-07T14:10:52.927127", + "cert_data": "dummy_cacert_data", + "SmartcardContainer": null, + "Smartcard": { + "label": "smartcard_ca_card", + "user_pin": "pin1", + "gen_status": true, + "SmartcardContainer": { + "label": "keypair_ca1" + } + } + } + +**Server Certificate (Flat Storage):** + +.. code-block:: json + + { + "_id": { + "$oid": "695e5b69bfa5128f5c1890f2" + }, + "id": "test-server1", + "country": "DE", + "state": "Berlin", + "locality": "Berlin", + "org": "WEBcodeX", + "org_unit": "Security", + "common_name": "testserver@domain.com", + "email": "pki@webcodex.de", + "valid_days": 365, + "generation_timestamp": "2026-01-07T14:11:05.030076", + "cert_data": "dummy_servercert_data" + } + +**Server Certificate (Hierarchical Storage with CA Reference):** + +.. code-block:: json + + { + "_id": { + "$oid": "695e5b69bfa5128f5c1890f3" + }, + "id": "test-server1", + "country": "DE", + "state": "Berlin", + "locality": "Berlin", + "org": "WEBcodeX", + "org_unit": "Security", + "common_name": "testserver@domain.com", + "email": "pki@webcodex.de", + "valid_days": 365, + "generation_timestamp": "2026-01-07T14:11:05.030076", + "cert_data": "dummy_servercert_data", + "SmartcardContainer": null, + "Smartcard": { + "label": "smartcard_customer1", + "user_pin": "pin2", + "gen_status": true, + "SmartcardContainer": { + "label": "testserver1_keypair" + } + }, + "CertCA": { + "id": "test-ca1", + "country": "DE", + "state": "Berlin", + "locality": "Berlin", + "org": "WEBcodeX", + "org_unit": "Security", + "common_name": "testca@domain.com", + "email": "pki@webcodex.de", + "valid_days": 365, + "generation_timestamp": "2026-01-07T14:10:52.927127", + "cert_data": "dummy_cacert_data", + "SmartcardContainer": null, + "Smartcard": { + "label": null, + "user_pin": null, + "gen_status": false, + "SmartcardContainer": { + "label": null + } + } + } + } - Example number 2 is a stripped-down excerpt from PKI management to demonstrate how *virtual class types* and a *clean OOP model setup* work. +**Client Certificate (Flat Storage):** + +.. code-block:: json + + { + "_id": { + "$oid": "695e5b81fa0258fe59b9461d" + }, + "id": "test-client1", + "country": "DE", + "state": "Berlin", + "locality": "Berlin", + "org": "WEBcodeX", + "org_unit": "Security", + "common_name": "testclient1@domain.com", + "email": "pki@webcodex.de", + "valid_days": 365, + "generation_timestamp": "2026-01-07T14:11:29.750133", + "cert_data": "dummy_clientcert_data" + } + +**Client Certificate (Hierarchical Storage with Full References):** + +.. code-block:: json + + { + "_id": { + "$oid": "695e5b81fa0258fe59b9461e" + }, + "id": "test-client1", + "country": "DE", + "state": "Berlin", + "locality": "Berlin", + "org": "WEBcodeX", + "org_unit": "Security", + "common_name": "testclient1@domain.com", + "email": "pki@webcodex.de", + "valid_days": 365, + "generation_timestamp": "2026-01-07T14:11:29.750133", + "cert_data": "dummy_clientcert_data", + "SmartcardContainer": null, + "Smartcard": { + "label": "smartcard_customer1", + "user_pin": "pin2", + "gen_status": true, + "SmartcardContainer": { + "label": "testserver1_client1_keypair" + } + }, + "CertCA": { + "id": "test-ca1", + "country": "DE", + "state": "Berlin", + "locality": "Berlin", + "org": "WEBcodeX", + "org_unit": "Security", + "common_name": "testca@domain.com", + "email": "pki@webcodex.de", + "valid_days": 365, + "generation_timestamp": "2026-01-07T14:10:52.927127", + "cert_data": "dummy_cacert_data", + "SmartcardContainer": null, + "Smartcard": { + "label": null, + "user_pin": null, + "gen_status": false, + "SmartcardContainer": { + "label": null + } + } + }, + "CertServer": { + "id": "test-server1", + "country": "DE", + "state": "Berlin", + "locality": "Berlin", + "org": "WEBcodeX", + "org_unit": "Security", + "common_name": "testserver@domain.com", + "email": "pki@webcodex.de", + "valid_days": 365, + "generation_timestamp": "2026-01-07T14:11:05.030076", + "cert_data": "dummy_servercert_data", + "SmartcardContainer": null, + "Smartcard": { + "label": null, + "user_pin": null, + "gen_status": false, + "SmartcardContainer": { + "label": null + } + } + } + } + +These examples demonstrate how the microesb framework stores both flat certificate data and hierarchical relationships in MongoDB, enabling efficient querying and complete object graph reconstruction. 2.1. CA Certificate Relations ****************************** @@ -303,7 +547,7 @@ The "Cert" base class provides three private abstract methods because the proces - `_load_ref_cert_data()` - `_gen_openssl_cert()` -- `_insert_cert_db_data()` +- `_store_cert_data()` .. code-block:: python @@ -317,15 +561,15 @@ The "Cert" base class provides three private abstract methods because the proces class CertServer(Cert): def _load_ref_cert_data(self): - self.CertCA._get_cert_dbdata_by_id() + self.CertCA._get_cert_data_by_id() .. code-block:: python class CertClient(Cert): def _load_ref_cert_data(self): - self.CertCA._get_cert_dbdata_by_id() - self.CertServer._get_cert_dbdata_by_id() + self.CertCA._get_cert_data_by_id() + self.CertServer._get_cert_data_by_id() 2.6.2. Generic Template Methods ------------------------------- @@ -333,7 +577,7 @@ The "Cert" base class provides three private abstract methods because the proces The following methods are generic template methods inherited by each Child Class: - `_gen_openssl_privkey()` -- `_get_cert_dbdata_by_id()` +- `_get_cert_data_by_id()` - `_hsm_gen_keypair()` 2.7. Accessing Properties @@ -350,7 +594,7 @@ Label as follows: In `CertClient` and `CertServer`, it is also possible to access the `CertCA's` `Smartcard` Properties (from Referenced Classes in Class Reference Config) to fill data from the database -inside `_get_cert_dbdata_by_id()`: +inside `_get_cert_data_by_id()`: .. code-block:: python @@ -374,7 +618,7 @@ Currently, the *Service Registry* feature is unimplemented. Execution is only po The following implementation demonstrates how to execute a `CertCA` type service: -.. literalinclude:: ../../example/02-pki-management/main-ca.py +.. literalinclude:: ../../example/02-pki-management/00-main-ca.py :linenos: To execute the script, run: @@ -396,7 +640,7 @@ Expected output: The following implementation demonstrates how to execute a `CertServer` type service: -.. literalinclude:: ../../example/02-pki-management/main-server.py +.. literalinclude:: ../../example/02-pki-management/01-main-server.py :linenos: To execute the script, run: @@ -420,7 +664,7 @@ Expected output: The following implementation demonstrates how to execute a `CertClient` type service: -.. literalinclude:: ../../example/02-pki-management/main-client.py +.. literalinclude:: ../../example/02-pki-management/02-main-client.py :linenos: To execute the script, run: @@ -488,53 +732,23 @@ The following is an example of the *Service Call Metadata* for all certificate t .. _example-number3: -3. Alias Class Mapping -====================== - -Alias Class Mapping **must** be used when setting up multiple *Child Class Instances*. +3. Hosting Use Case (Using DBPool) +================================== -3.1. Requirements -***************** - -The *Alias Definition* must exist in the *Class Mapping Config* and map to an existing *Implementation Class*. - -Children of classes defined in the *Class Reference Config* must map to the *Alias Class(es)*. +Example number 3 is an exact copy of example number 1 with the difference that a database pooling mechanism `python-dbpool` is used. -The *Alias Class* `property_ref` property in the *Class Reference Config* must always reference an existing *Implementation Class*. +Project information: -3.2. Example -************ - -Here is an example configuration for Alias Class Mapping: - -.. code-block:: python - - config = { - 'class_mapping': { - 'Test': 'Test', - 'Test2Ref1': 'Test2', - 'Test2Ref2': 'Test2' - }, - 'class_reference': { - 'Test': { - 'property_ref': 'Test', - 'children': { - 'Test2Ref1': { - 'property_ref': 'Test2' - }, - 'Test2Ref2': { - 'property_ref': 'Test2' - } - } - } - } - } +- https://github.com/clauspruefer/python-dbpool .. _example-number4: -4. SOA on Kubernetes -==================== +4. NLAP Integration +=================== + +Example number 4 will be added on NLAP (Next Level Application Protocol) completion. -This example includes a working Docker container and Kubernetes Minikube setup. It will be implemented after the FalconAS milestone *HTTP/1.1 implementation* is completed. +Ongoing project status can be viewed here: -For more information, see: https://github.com/WEBcodeX1/http-1.2. +- https://www.der-it-pruefer.de/network/Exemplary-HTTP-Processing-Protocol-Design +- https://github.com/WEBcodeX1/http-1.2 diff --git a/doc/source/features.rst b/doc/source/features.rst index 9cd963e..386cd33 100644 --- a/doc/source/features.rst +++ b/doc/source/features.rst @@ -85,10 +85,11 @@ For the full example, see :ref:`example-number2`. 5. Planned Features ==================== -The following features are planned for future releases: - -- Service Registry -- Service Registry Management -- Service-Based AAA (HSM / Smartcard) -- Automatic Service Interface Documentation Generation -- Integration with the x0 JavaScript Framework +Planned for upcoming releases: + +- Service Registry / API Server +- Service Registry / YANG Model Export +- Service Registry / Web Interface +- Service API / Auto Documentation +- Extended "Encapsulated" Service Routing +- Mincepy Integration / Metadata Mapping diff --git a/doc/source/index.rst b/doc/source/index.rst index 6914ff6..3365f90 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -12,6 +12,7 @@ Contents: features apidoc api + routing examples Indices and tables diff --git a/doc/source/intro.rst b/doc/source/intro.rst index 669dedd..c6f2d53 100644 --- a/doc/source/intro.rst +++ b/doc/source/intro.rst @@ -4,10 +4,9 @@ Intro / Module Description ========================== -The **microesb** Python module provides the foundational features to build a centralized, -structured Enterprise Service Bus (ESB) / SOA architecture. +The **microesb** Python module provides the foundational features to build a centralized, structured Enterprise Service Bus (ESB) / Service-Oriented Architecture (SOA). -Its primary feature is a clean, OOP-based **Service Model to Python Class Mapping**. +It enables clean **Service Model to Python Class Mapping** with support for both traditional relational databases and modern NoSQL backends. The module features user-based service call routing, recursive class hierarchy object deserialization, and graph-based JSON result abstraction, making it suitable for scalable Next-Level Platform-as-a-Service (PaaS) and Microservices architectures. A Docker example container can be downloaded here: `microesb-examples-latest.tar `_. @@ -35,22 +34,23 @@ Refer to ``/docker/README.md``. 3. Dependencies =============== -Using the **microesb** module generally does not require the **psycopg2** PostgreSQL -Python module. +Using the **microesb** module generally does not require the **psycopg2** PostgreSQL Python module. .. warning:: - Running example code requires **psycopg2**. + Running example code requires **psycopg2** for Example 1 and **pymongo** for Example 2. .. code-block:: bash # install psycopg2 apt-get install python3-psycopg2 +.. note:: + Example 2 uses MongoDB as a NoSQL backend. MongoDB cannot be installed via apt on all systems and is preinstalled in the Docker example container. For local development, install MongoDB separately or use the provided Docker container. + 4. Build Dependencies ===================== -On current Debian 12 / Ubuntu 22.04.3 or 24.04.1, install the following additional packages -(for documentation rendering and testing purposes). +On Debian 12 / Ubuntu 22.04 or 24.04, install the following additional packages (for documentation rendering and testing purposes). .. code-block:: bash @@ -87,8 +87,15 @@ To run all tests (unit and integration) after the system-wide package installati =================== - JSON Service Metadata to Python Internal Class / Object Mapping +- JSON Result Hierarchical Deserialization +- Service Routing / Data Aggregation 8. Planned Features ==================== -- Database Abstraction on "top" of the Object Mapping Model +- Service Registry / API Server +- Service Registry / YANG Model Export +- Service Registry / Web Interface +- Service API / Auto Documentation +- Extended “Encapsulated” Service Routing +- Mincepy Integration / Metadata Mapping diff --git a/doc/source/routing.rst b/doc/source/routing.rst new file mode 100644 index 0000000..f7f6a19 --- /dev/null +++ b/doc/source/routing.rst @@ -0,0 +1,84 @@ +.. _routing: + +======= +Routing +======= + +The **microesb** module provides flexible routing capabilities to direct service calls to appropriate backend implementations. This enables data aggregation (in ESB terminology, this is referred to as *routing*) from multiple data sources, including traditional relational databases, modern NoSQL platforms, and similar systems. + +1. Simple Routing +================= + +Simple Routing allows **direct**, **unencapsulated** routing of service calls to user-defined functions. This approach is suitable for straightforward use cases where service calls map directly to backend operations without complex orchestration requirements. + +**Encapsulated Routing** is a methodology designed to abstract and handle each service entity as an externally callable service, encapsulated within a network-accessible container (e.g., application server) with scaling and AAA (Authentication, Authorization, Accounting) functionality, such as "Kubernetes / Nginx / HTTPS" or "FalconAS / NLAP". + +The **Encapsulated Routing** concept is described in greater detail in section 2. + +1.1. Overview +************* + +Simple routing uses the ``ServiceRouter`` class to dynamically invoke functions defined in a ``user_routing.py`` module. Each routing function receives metadata from the service call and can return user-modified results (of any type) representing the outcome of the backend operation. + +1.2. Implementation +******************* + +To implement simple routing, create a ``user_routing.py`` module in your project with functions that match the routing identifiers used in your service calls (see next chapter). + +Example from ``/example/02-pki-management/user_routing.py``: + +.. literalinclude:: ../../example/02-pki-management/user_routing.py + :linenos: + +1.3. Service Calls +****************** + +A service call must be placed within the *service implementation*. The micro-esb's ServiceRouter class provides a reference there (self._ServiceRouter), making it easily callable as follows: + +``self._ServiceRouter.send('MethodId', metadata=metadata)``. + +A recommended approach is to pass metadata as a dictionary type (JSON serializable) into the routing function and expect a dictionary type as a result (JSON serializable), conforming to modern software development best practices. + +1.4. Common Use Cases +********************* + +Simple routing is particularly suitable for: + +- Aggregating data from internal systems (e.g., centralized databases with NoSQL data sources) +- Routing and propagating service calls (e.g., certificate generation) to network-attached subsystems + +.. warning:: + Authentication, accounting, and load balancing must be implemented by the user. + +1.5. Error Handling and Logging +******************************* + +The ``ServiceRouter`` class does not provide built-in error handling or logging. Users are responsible for implementing their own error and exception handling, as well as logging mechanisms. + +2. Encapsulated Routing +======================= + +**Encapsulated Routing** is a mechanism for hosting ESB API services securely within a network-accessible entity that provides the following features: + +- Load Balancing / Scaling +- AAA (Authentication, Authorization, Accounting) +- Service (API) Registration / Versioning +- Service (API) Discovery +- Service (API) Documentation +- Service Security / PKI Abstraction + +Detailed documentation (including examples) will be available starting from release version 1.3. + +3. Operating Modes +================== + +There are **no** strictly configurable modes. The micro-esb framework is designed to be extraordinarily flexible in class abstraction and modeling from the user's perspective; the *implementation mode* results from the user's program code. + +Nevertheless, two different **logical** modes can be distinguished: + +- Native Routing Mode +- Non-Native Routing Mode + +**Native Routing** is the concept of delegating all service *computations* (CPU-intensive operations) to external entities or application servers. The ESB's service_implementation exclusively routes data to external services and **does not** process data internally, thereby strongly enhancing security. + +**Non-Native Routing** does not encapsulate data calls, so data fetching is executed directly within the ESB's service_implementation (e.g., direct MongoDB driver usage). This approach should not be adopted in environments with high security requirements or non-reverse-proxied access. diff --git a/docker/examples.dockerfile b/docker/examples.dockerfile index 673cad9..3c660af 100644 --- a/docker/examples.dockerfile +++ b/docker/examples.dockerfile @@ -1,8 +1,8 @@ -FROM postgres:latest +FROM postgres:18-bookworm MAINTAINER Claus Prüfer ADD ./example / -COPY ./dist/microesb-1.0rc1.tar.gz / +COPY ./dist/microesb-1.1.tar.gz / COPY ./example/01-hosting-use-case/01-create-schema-sequence.sql /docker-entrypoint-initdb.d/ COPY ./example/01-hosting-use-case/02-create-table.sql /docker-entrypoint-initdb.d/ @@ -13,13 +13,22 @@ RUN apt-get -qq update -y RUN apt-get -qq install python3-pip python3-sphinx python3-sphinx-rtd-theme -y RUN apt-get -qq install python3-pytest python3-pytest-pep8 -y -RUN apt-get -qq install python3-psycopg2 -y +RUN apt-get -qq install python3-psycopg2 python3-pymongo -y +RUN apt-get -qq install curl -y -RUN pip3 install /microesb-1.0rc1.tar.gz --break-system-packages +RUN curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor + +RUN echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/debian bookworm/mongodb-org/8.0 main" | tee /etc/apt/sources.list.d/mongodb-org-8.0.list + +RUN apt-get -qq update +RUN apt-get -qq install mongodb-org -y + +RUN mkdir -p /data/db + +RUN pip3 install /microesb-1.1.tar.gz --break-system-packages ENV POSTGRES_USER postgres ENV POSTGRES_PASSWORD password ENV POSTGRES_DB hosting-example EXPOSE 5432 - diff --git a/docker/exec-example2.sh b/docker/exec-example2.sh index e28d502..7fbcbac 100755 --- a/docker/exec-example2.sh +++ b/docker/exec-example2.sh @@ -1,14 +1,13 @@ #/bin/sh # exec cert gen ca service -docker exec microesb-postgres python3 /02-pki-management/main-ca.py +docker exec microesb-postgres python3 /02-pki-management/00-main-ca.py echo "" # exec cert gen server service -docker exec microesb-postgres python3 /02-pki-management/main-server.py +docker exec microesb-postgres python3 /02-pki-management/01-main-server.py echo "" # exec cert gen client service -docker exec microesb-postgres python3 /02-pki-management/main-client.py +docker exec microesb-postgres python3 /02-pki-management/02-main-client.py echo "" - diff --git a/docker/start-examples-container.sh b/docker/start-examples-container.sh index 26d583f..09de49f 100755 --- a/docker/start-examples-container.sh +++ b/docker/start-examples-container.sh @@ -1,7 +1,10 @@ #!/bin/sh +# run container docker run --name microesb-postgres \ -d -p 5432:5432 \ --add-host=localdb:127.0.0.1 \ microesb-examples:latest postgres +# start mongodb +docker exec microesb-postgres mongod >/dev/null 2>/dev/null & diff --git a/example/01-hosting-use-case/README.md b/example/01-hosting-use-case/README.md index fd6ee6c..bbc62f1 100644 --- a/example/01-hosting-use-case/README.md +++ b/example/01-hosting-use-case/README.md @@ -1,8 +1,18 @@ -# Example 1: Hosting Use Case +# Example 1: Hosting Use Case with PostgreSQL Transactions -In this example, assume our "virtual" company runs a **Hosting Business**. +This example demonstrates a **DNS Hosting Management System** using the microesb framework with a **relational PostgreSQL** database backend and transactional processing. -The company's customer data, including a) Internet Domains and b) DNS Hostnames, should be manageable by different subsystems. +## Overview + +Example 1 showcases how microesb can manage hierarchical customer domain and DNS hostname data using the traditional relational database model with ACID transaction guarantees. + +In this example, assume our "virtual" company runs a **Hosting Business**. The company's customer data, including a) Internet Domains and b) DNS Hostnames, should be manageable by different subsystems. + +The example demonstrates: +- **Relational Database Transactions** with PostgreSQL for data integrity and atomic operations +- **Hierarchical Service Metadata Processing** to manage domain and hostname relationships +- **ACID Compliance** ensuring commit/rollback mechanisms for data consistency +- **1:n Relationships** between Customer → Domain → Hostname entities > **Note:** Example number 1 will only cover Local Service Mapping **without** a *Python Application Server* encapsulation. diff --git a/example/02-pki-management/main-ca.py b/example/02-pki-management/00-main-ca.py similarity index 56% rename from example/02-pki-management/main-ca.py rename to example/02-pki-management/00-main-ca.py index 4e0a3d8..c61a45b 100644 --- a/example/02-pki-management/main-ca.py +++ b/example/02-pki-management/00-main-ca.py @@ -1,3 +1,6 @@ +import sys +import logging + from microesb import microesb from class_reference import class_reference_ca as class_reference @@ -5,6 +8,19 @@ from class_mapping import class_mapping from service_call_metadata import service_metadata_ca as service_metadata +from pymongo import MongoClient + +client = MongoClient('mongodb://127.0.0.1/') +mongodb = client.get_database('microesb') + + +logging.getLogger().addHandler( + logging.StreamHandler(sys.stdout) +) + +logging.getLogger().setLevel( + logging.INFO +) class_mapper = microesb.ClassMapper( class_references=class_reference, @@ -13,9 +29,13 @@ ) try: - res = microesb.ServiceExecuter().execute( + res = microesb.ServiceExecuter().execute_get_hierarchy( class_mapper=class_mapper, service_data=service_metadata ) except Exception as e: print('Service execution error: {}'.format(e)) + +root_object = res[0]['CertCA']['object_instance'] + +mongodb.cert_hierarchy.insert_one(root_object.json_dict) diff --git a/example/02-pki-management/main-server.py b/example/02-pki-management/01-main-server.py similarity index 56% rename from example/02-pki-management/main-server.py rename to example/02-pki-management/01-main-server.py index 1697c0f..7ce0ba6 100644 --- a/example/02-pki-management/main-server.py +++ b/example/02-pki-management/01-main-server.py @@ -1,3 +1,6 @@ +import sys +import logging + from microesb import microesb from class_reference import class_reference_server as class_reference @@ -5,6 +8,19 @@ from class_mapping import class_mapping from service_call_metadata import service_metadata_server as service_metadata +from pymongo import MongoClient + +client = MongoClient('mongodb://127.0.0.1/') +mongodb = client.get_database('microesb') + + +logging.getLogger().addHandler( + logging.StreamHandler(sys.stdout) +) + +logging.getLogger().setLevel( + logging.INFO +) class_mapper = microesb.ClassMapper( class_references=class_reference, @@ -13,9 +29,13 @@ ) try: - res = microesb.ServiceExecuter().execute( + res = microesb.ServiceExecuter().execute_get_hierarchy( class_mapper=class_mapper, service_data=service_metadata ) except Exception as e: print('Service execution error: {}'.format(e)) + +root_object = res[0]['CertServer']['object_instance'] + +mongodb.cert_hierarchy.insert_one(root_object.json_dict) diff --git a/example/02-pki-management/main-client.py b/example/02-pki-management/02-main-client.py similarity index 56% rename from example/02-pki-management/main-client.py rename to example/02-pki-management/02-main-client.py index ce7f666..3dbbba2 100644 --- a/example/02-pki-management/main-client.py +++ b/example/02-pki-management/02-main-client.py @@ -1,3 +1,6 @@ +import sys +import logging + from microesb import microesb from class_reference import class_reference_client as class_reference @@ -5,6 +8,19 @@ from class_mapping import class_mapping from service_call_metadata import service_metadata_client as service_metadata +from pymongo import MongoClient + +client = MongoClient('mongodb://127.0.0.1/') +mongodb = client.get_database('microesb') + + +logging.getLogger().addHandler( + logging.StreamHandler(sys.stdout) +) + +logging.getLogger().setLevel( + logging.INFO +) class_mapper = microesb.ClassMapper( class_references=class_reference, @@ -13,9 +29,13 @@ ) try: - res = microesb.ServiceExecuter().execute( + res = microesb.ServiceExecuter().execute_get_hierarchy( class_mapper=class_mapper, service_data=service_metadata ) except Exception as e: print('Service execution error: {}'.format(e)) + +root_object = res[0]['CertClient']['object_instance'] + +mongodb.cert_hierarchy.insert_one(root_object.json_dict) diff --git a/example/02-pki-management/README.md b/example/02-pki-management/README.md new file mode 100644 index 0000000..61de2a7 --- /dev/null +++ b/example/02-pki-management/README.md @@ -0,0 +1,170 @@ +# Example 2: PKI Management with NoSQL (MongoDB) + +This example demonstrates a **Public Key Infrastructure (PKI) Certificate Management System** using the microesb framework with a **NoSQL MongoDB** backend. + +## Overview + +Example 2 showcases how microesb can manage a hierarchical PKI system with three certificate types: + +- **Certificate Authority (CA)** - Root certificate that signs server and client certificates +- **Server Certificates** - Signed by the CA, used for server authentication +- **Client Certificates** - Signed by the CA, used for client authentication with server verification + +The example demonstrates: +- **NoSQL Document Storage** using MongoDB for flexible certificate data management +- **User-Defined Routing** with custom routing functions for MongoDB operations +- **Hierarchical Class Dependencies** showing CA → Server → Client certificate relationships +- **Smartcard/HSM Integration** for secure key storage (simulated in this example) + +## Prerequisites + +### MongoDB Requirement + +This example requires a running MongoDB instance. + +**Local MongoDB Community Edition Installation:** + +https://www.mongodb.com/docs/manual/administration/install-community/#std-label-install-mdb-community-edition + +**Docker Example Container:** + +The docker example container contains both an installed MongoDB instance and the example code. + +### Python Dependencies + +- Python 3.8 or later +- pymongo library +- microesb package + +## Execution Order + +**IMPORTANT:** The execution order of the scripts is critical due to certificate dependencies: + +1. **First:** `00-main-ca.py` - Creates the Certificate Authority (CA) +2. **Second:** `01-main-server.py` - Creates Server Certificate (requires CA to exist) +3. **Third:** `02-main-client.py` - Creates Client Certificate (requires both CA and Server certificates to exist) + +The hierarchical dependencies mean: +- Server certificates require the CA certificate to be created first (for signing) +- Client certificates require both CA (for signing) and Server certificates (for verification) + +Running scripts out of order will result in missing certificate references in the MongoDB database. + +## Execution + +Navigate to the example directory and execute the scripts in the correct order: + +```bash +# Step 1: Create Certificate Authority +python3 ./00-main-ca.py + +# Step 2: Create Server Certificate +python3 ./01-main-server.py + +# Step 3: Create Client Certificate +python3 ./02-main-client.py +``` + +## How It Works + +### 1. Service Metadata + +Each script uses different service metadata defining the certificate hierarchy: + +- `service_metadata_ca` - CA certificate with Smartcard container +- `service_metadata_server` - Server certificate referencing CA +- `service_metadata_client` - Client certificate referencing both CA and Server + +See: [service_call_metadata.py](service_call_metadata.py) + +### 2. Class Hierarchy + +The class reference structures define parent-child relationships: + +``` +CertCA +├── Smartcard +│ └── SmartcardContainer + +CertServer +├── Smartcard +│ └── SmartcardContainer +└── CertCA (reference) + └── Smartcard + └── SmartcardContainer + +CertClient +├── Smartcard +│ └── SmartcardContainer +├── CertCA (reference) +│ └── Smartcard +│ └── SmartcardContainer +└── CertServer (reference) + └── Smartcard + └── SmartcardContainer +``` + +See: [class_reference.py](class_reference.py) + +### 3. User-Defined Routing + +The `user_routing.py` module provides custom routing functions for MongoDB operations: + +- `CertGetById(metadata)` - Retrieves certificate documents by ID +- `CertStore(metadata)` - Stores certificate documents +- `KeypairGenerate(metadata)` - Simulates key pair generation + +See: [user_routing.py](user_routing.py) + +### 4. MongoDB Collections + +The example uses the following MongoDB collections in the `microesb` database: + +- `cert` - Individual certificate data with properties +- `cert_hierarchy` - Complete certificate hierarchies with all related objects + +### 5. Class Implementation + +The implementation includes abstract base class `Cert` with three concrete implementations: + +- `CertCA` - Certificate Authority implementation +- `CertServer` - Server certificate implementation +- `CertClient` - Client certificate implementation + +Each class handles: +- Loading referenced certificate data from MongoDB +- Simulating OpenSSL certificate generation +- Smartcard/HSM key pair generation (simulated) +- Storing generated certificate data + +See: [service_implementation.py](service_implementation.py) + +## Post-Execution + +After executing all three scripts, you can verify the data in MongoDB: + +```bash +# Connect to MongoDB (using mongosh for MongoDB 5.0+) +mongosh + +# Switch to microesb database +use microesb + +# View individual certificates +db.cert.find().pretty() + +# View complete certificate hierarchies +db.cert_hierarchy.find().pretty() +``` + +## Key Features Demonstrated + +1. **NoSQL Flexibility:** MongoDB's schema-less design allows flexible certificate document structures +2. **Hierarchical Processing:** Automatic deserialization of nested certificate references +3. **Custom Routing:** User-defined routing functions for database operations +4. **Property Registration:** Dynamic property management with type checking and validation +5. **Recursive Class Hierarchy:** Automatic resolution and connection of certificate dependencies + +## Additional Information + +For more detailed documentation, see: [https://pythondocs.webcodex.de/micro-esb/examples.html](https://pythondocs.webcodex.de/micro-esb/examples.html) diff --git a/example/02-pki-management/service_implementation.py b/example/02-pki-management/service_implementation.py index 5f63ea4..15e87c7 100644 --- a/example/02-pki-management/service_implementation.py +++ b/example/02-pki-management/service_implementation.py @@ -1,13 +1,37 @@ import abc +import logging +import datetime from microesb import microesb +logger = logging.getLogger(__name__) + class Cert(microesb.ClassHandler, metaclass=abc.ABCMeta): def __init__(self): super().__init__() + self.register_property( + 'generation_timestamp', + { + 'type': 'str', + 'default': None, + 'required': False, + 'description': 'SysInternal Certificate Generation Date' + } + ) + + self.register_property( + 'cert_data', + { + 'type': 'str', + 'default': None, + 'required': False, + 'description': 'SysInternal Generated Certificate Base64 encoded' + } + ) + @abc.abstractmethod def _load_ref_cert_data(self): """ Abstract _load_ref_cert_data() method. @@ -19,8 +43,8 @@ def _gen_openssl_cert(self): """ @abc.abstractmethod - def _insert_cert_db_data(self): - """ Abstract _insert_cert_db_data() method. + def _store_cert_data(self): + """ Abstract _store_cert_data() method. """ def gen_cert(self): @@ -28,27 +52,38 @@ def gen_cert(self): self._load_ref_cert_data() if getattr(self, 'Smartcard') is not None: + logger.info('Gen HSM Keypair') self._hsm_gen_keypair() - else: + if getattr(self, 'Smartcard') is None: + logger.info('Gen OpenSSL PrivKey') self._gen_openssl_privkey() self._gen_openssl_cert() - self._insert_cert_db_data() + self.generation_timestamp = datetime.datetime.now().isoformat('T') + self._store_cert_data() def _gen_openssl_privkey(self): - print('Gen openssl private key.') + logger.info('Gen openssl private key.') - def _get_cert_dbdata_by_id(self): - print('Get cert data from db. Type: {}.'.format(self.type)) + def _get_cert_data_by_id(self): + logger.info('Get cert data from ESB. Type:{}.'.format(self.type)) + self.set_properties( + self._ServiceRouter.send('CertGetById', metadata=self.id) + ) def _hsm_gen_keypair(self): - print('Smartcard container label:{}'.format( + logger.info('Smartcard container label:{}'.format( self.Smartcard.SmartcardContainer.label) ) self.Smartcard.gen_keypair() + def _store_cert_data(self): + logger.info('Store {} cert metadata.'.format(self.type)) + self._ServiceRouter.send('CertStore', metadata=self.property_dict) + class CertCA(Cert): + def __init__(self): self.type = 'ca' super().__init__() @@ -57,54 +92,91 @@ def _load_ref_cert_data(self): pass def _gen_openssl_cert(self): - print('Gen openssl cert type:{}.'.format(self.type)) + logger.info('Generating {} cert.'.format(self.type)) - def _insert_cert_db_data(self): - print('Insert cert data type:{} into db.'.format(self.type)) + srv_metadata = { + "CertCA": self.property_dict + } + + self.cert_data = 'dummy_cacert_data' + logger.info('Generating cert with metadata:{}'.format(srv_metadata)) class CertServer(Cert): + def __init__(self): self.type = 'server' super().__init__() def _load_ref_cert_data(self): - self.CertCA._get_cert_dbdata_by_id() + self.CertCA._get_cert_data_by_id() def _gen_openssl_cert(self): - print('Gen openssl cert type:{}, rel to CA.'.format(self.type)) + logger.info('Generating {} cert.'.format(self.type)) - def _insert_cert_db_data(self): - print('Insert cert data type:{} into db.'.format(self.type)) + srv_metadata = { + "CertServer": self.property_dict, + "CertCA": self.CertCA.property_dict + } + + logger.info('Generating cert with metadata:{}'.format(srv_metadata)) + + self.cert_data = 'dummy_servercert_data' class CertClient(Cert): + def __init__(self): self.type = 'client' super().__init__() def _load_ref_cert_data(self): - self.CertCA._get_cert_dbdata_by_id() - self.CertServer._get_cert_dbdata_by_id() + self.CertCA._get_cert_data_by_id() + self.CertServer._get_cert_data_by_id() def _gen_openssl_cert(self): - print('Gen openssl cert type:{}, rel to cCA and cServer.'.format(self.type)) + logger.info('Generating {} cert.'.format(self.type)) - def _insert_cert_db_data(self): - print('Insert cert data type:{} into db.'.format(self.type)) + srv_metadata = { + "CertClient": self.property_dict, + "CertServer": self.CertServer.property_dict, + "CertCA": self.CertCA.property_dict + } + + logger.info('Generating cert with metadata:{}'.format(srv_metadata)) + self.cert_data = 'dummy_clientcert_data' class Smartcard(microesb.ClassHandler): + def __init__(self): super().__init__() + self.register_property( + 'gen_status', + { + 'type': bool, + 'default': False, + 'required': False, + 'description': 'SysInternal Generated Smartcard Keypair Status' + } + ) + def gen_keypair(self): - print('Gen keypair on smartcard:{} with keypair label:{}'.format( + logger.info('Gen keypair on smartcard:{} with keypair label:{}'.format( self.label, self.SmartcardContainer.label )) + srv_metadata = { + "SmartcardID": self.label, + "SmartcardContainerLabel": self.SmartcardContainer.label + } + + self.gen_status = self._ServiceRouter.send('KeypairGenerate', metadata=srv_metadata) + class SmartcardContainer(microesb.ClassHandler): + def __init__(self): super().__init__() diff --git a/example/02-pki-management/user_routing.py b/example/02-pki-management/user_routing.py new file mode 100644 index 0000000..5e6cbe8 --- /dev/null +++ b/example/02-pki-management/user_routing.py @@ -0,0 +1,16 @@ +from pymongo import MongoClient + +client = MongoClient('mongodb://127.0.0.1/') +mongodb = client.get_database('microesb') + + +def CertGetById(metadata): + return mongodb.cert.find_one( + {"id": metadata} + ) + +def CertStore(metadata): + return mongodb.cert.insert_one(metadata) + +def KeypairGenerate(metadata): + return True diff --git a/example/04-nlap-integration/README.md b/example/04-nlap-integration/README.md new file mode 100644 index 0000000..a1a0e2b --- /dev/null +++ b/example/04-nlap-integration/README.md @@ -0,0 +1,50 @@ +# Example 4: NLAP Proxy Integration + +> **Note:** This example is a **template** for future implementation of NLAP Proxy Authentication integration with Python MicroESB. + +## Overview + +This example will demonstrate how to integrate Python MicroESB with the **NLAP (Network Layer Authentication Protocol) Proxy** for secure, authenticated service communication. + +## NLAP Proxy + +The NLAP Proxy is an advanced authentication and security layer for NLAP protocol communication. For more information, see the NLAP (Next Level Application Protocol) project: + +**External Link:** [https://github.com/WEBcodeX1/http-1.2](https://github.com/WEBcodeX1/http-1.2) + +### Key Security Features + +The NLAP Proxy implementation enforces the following security requirements: + +1. **Encrypted Communication Only** + - The proxy will **not** communicate unencrypted + - All traffic must use encrypted channels + +2. **Restricted Communication Scope** + - The proxy will **not** allow global communication + - Communication is restricted to authorized endpoints only + +3. **X.509 Certificate-Based Authentication** + - The proxy will **only** allow communication by X.509 client certificates + - Client authentication is mandatory for all connections + +## Future Implementation + +This example will cover: + +- Setting up NLAP Proxy for MicroESB services +- Configuring X.509 client certificate authentication +- Integrating encrypted communication channels +- Service routing through the NLAP Proxy +- Load balancing and service scaling with NLAP authentication + +## Prerequisites + +- Python 3.8 or later +- NLAP Proxy (see external link above) +- X.509 client and server certificates +- Python MicroESB package + +## Additional Information + +For more detailed documentation on Python MicroESB, see: [https://pythondocs.webcodex.de/micro-esb/examples.html](https://pythondocs.webcodex.de/micro-esb/examples.html) diff --git a/example/README.md b/example/README.md index cb47e34..fd69b10 100644 --- a/example/README.md +++ b/example/README.md @@ -1,5 +1,42 @@ # Python MicroESB Examples +This directory contains comprehensive examples demonstrating different database paradigms and use cases with the microesb framework. + +## Example 1: Hosting Use Case - Relational Database Transactions + +Example 1 demonstrates the **classical relational database transactional model** using PostgreSQL. + +**Key Features:** +- **ACID Transactions:** Demonstrates atomic operations with commit/rollback functionality +- **Relational Integrity:** Shows how to maintain referential integrity across related entities (Customer → Domain → DNS Records) +- **Transaction Management:** Illustrates proper database transaction handling with the microesb framework +- **Normalized Schema:** Uses a normalized PostgreSQL database schema with foreign key relationships + +This example is ideal for understanding how microesb integrates with traditional SQL databases where data consistency and transaction atomicity are critical requirements. + +## Example 2: PKI Management - NoSQL Document Model + +Example 2 showcases the **NoSQL document-oriented model** using MongoDB. + +**Key Features:** +- **Document Storage:** Demonstrates flexible schema design with MongoDB collections +- **Denormalized Data:** Shows how to work with embedded documents and flexible structures +- **Schema Evolution:** Illustrates the advantages of schema-less document storage +- **User-Defined Routing:** Implements custom routing functions for MongoDB CRUD operations +- **Horizontal Scalability:** Designed for scenarios requiring easy horizontal scaling + +This example highlights how microesb seamlessly works with modern NoSQL databases, making it suitable for applications requiring rapid development, flexible data models, and high scalability. + +## Database Model Comparison + +| Aspect | Example 1 (PostgreSQL) | Example 2 (MongoDB) | +|--------|------------------------|---------------------| +| **Model** | Relational / Normalized | Document / Denormalized | +| **Transactions** | ACID compliant | Eventually consistent | +| **Schema** | Fixed schema | Flexible schema | +| **Relationships** | Foreign keys | Embedded documents | +| **Use Case** | Data integrity critical | Rapid development, scalability | + Refer to detailed documentation at: https://pythondocs.webcodex.de/micro-esb/examples.html diff --git a/pyproject.toml b/pyproject.toml index d84fab8..4457e13 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "microesb" -version = "1.0" +version = "1.1" authors = [ { name="Claus Prüfer", email="pruefer@webcodex.de" }, diff --git a/src/microesb.py b/src/microesb.py index b8e929d..a622cfd 100644 --- a/src/microesb.py +++ b/src/microesb.py @@ -2,7 +2,7 @@ # . Micro ESB Python Module . # ]*[ --------------------------------------------------------------------- ]*[ # . . -# . Copyright Claus Prüfer (2016 - 2025) . +# . Copyright Claus Prüfer (2016 - 2026) . # . . # . . # ]*[ --------------------------------------------------------------------- ]*[ @@ -10,11 +10,14 @@ import os import abc import sys +import copy import logging import importlib +from microesb.router import ServiceRouter from microesb.transformer import JSONTransformer + try: esb_python_path = os.environ['esbpythonpath'] os.environ['PYTHONPATH'] = esb_python_path @@ -47,12 +50,13 @@ def __init__(self): :ivar classref logger: logging logger reference :ivar dict[SYSProperties] _SYSProperties: internal properties processing dict :ivar classref _SYSParentObject: internal (hierarchical) class instance ref - :ivar list[classref] _SYSClassNames: internal class refs dict + :ivar list[classname] _SYSClassNames: internal class names dict """ self.logger = logging.getLogger(__name__) self._SYSProperties = None + self._SYSPropertiesRegister = {} self._SYSParentObject = None self._SYSClassNames = [] @@ -70,6 +74,9 @@ def set_properties(self): def iterate(self): """ Recursive iterate through hierarchical class instances. + + :return: generator yielding class instances in hierarchy + :rtype: generator """ yield self for x in self: @@ -86,9 +93,13 @@ def add_properties(self, properties, parent_instance): add_properties() method on initialization for each existing class instance. """ properties = self._add_sys_default_properties(properties) + properties.update(self._SYSPropertiesRegister) + self.logger.debug('add properties:{}'.format(properties)) + self._SYSParentObject = parent_instance setattr(self, '_SYSProperties', properties) + for p_key, p_value in properties.items(): setattr(self, p_key, p_value['default']) @@ -113,15 +124,29 @@ def _add_sys_default_properties(self, properties): } return properties - def _set_property(self, key, value): + def register_property(self, property_id, property_item): + """ register_property() method. + + :param str property_id: property id (internal class attribute name) + :param dict property_item: property item to be registered for internal processing only + + Modifying data internally (inside a Service-Implementation) requires setting + additional properties not defined in Service-Properties, e.g. generated data, + time-stamps or similar. + + Service-Implementation classes should call this method to register such + additional internal properties for processing by the Micro ESB framework. """ + self._SYSPropertiesRegister[property_id] = property_item + + def _set_property(self, property_id, value): """ _set_property() method. - :param str key: property key name + :param str property_id: property dict key :param str value: property value """ - if key in self._SYSProperties: - setattr(self, key, value) + if property_id in self._SYSProperties: + setattr(self, property_id, value) @property def parent_object(self): @@ -145,6 +170,24 @@ def properties(self): """ return self._SYSProperties + @property + def property_dict(self): + """ property_dict() method. + + Return all classes self._SYSProperties property_id, value dictionary. + + :return: dictionary of all properties excluding 'SYSServiceMethod' + :rtype: dict + + Decorated with @property so direct property access possible + """ + + return_dict = {} + for property_id in self._SYSProperties: + if property_id != 'SYSServiceMethod': + return_dict[property_id] = self.get_value_by_property_id(property_id) + return return_dict + @property def class_count(self): """ class_count() method. @@ -168,8 +211,13 @@ def class_name(self): return self.__class__.__name__ def get_value_by_property_id(self, property_id): - """ get_value_by_property_id() method.""" - raise NotImplementedError + """ get_value_by_property_id() method. + + :param str property_id: property identifier + :return: attribute value for given property_id + :rtype: dynamic + """ + return getattr(self, property_id) class ClassHandler(BaseHandler): @@ -179,9 +227,11 @@ class ClassHandler(BaseHandler): def __init__(self): """ :ivar str _SYSType: const internal system type to differentiate handler types + :ivar classref _ServiceRouter: ServiceRouter instance reference """ super().__init__() self._SYSType = 'class_instance' + self._ServiceRouter = ServiceRouter() def __add__(self, args): """ overloaded internal __add__() method (+ operator). @@ -194,7 +244,7 @@ def __add__(self, args): >>> 'class_name': class_name, >>> 'class_ref': class_ref >>> } - >>> parent_instance + args + >>> class_instance + args """ self._add_class(**args) @@ -202,6 +252,9 @@ def __iter__(self): """ overloaded internal __iter__() method. Overloaded for using iter() on class references. + + :return: generator yielding class instances from _SYSClassNames + :rtype: generator """ for class_name in self._SYSClassNames: yield getattr(self, class_name) @@ -211,7 +264,7 @@ def _add_class(self, *, class_name, class_ref): :param dict *: used for passing params as **args dictionary :param str class_name: class name - :param classref class_ref: class instance reference + :param classref class_ref: class instance reference name Append class_name to self._SYSClassNames. Setup new class instance in global namespace. @@ -219,7 +272,7 @@ def _add_class(self, *, class_name, class_ref): Primary called by overloaded __add__() method. """ - self._SYSClassNames.append(class_ref) + self._SYSClassNames.append(class_name) new_class = globals()[class_ref] instance = new_class() @@ -233,19 +286,33 @@ def set_properties(self, item_dict): Iterates over item_dict and calls self._set_property(property_id, value) foreach item. """ + for property_id, value in item_dict.items(): self._set_property(property_id, value) def set_json_dict(self): """ set_json_dict() method. - Preprare self.json_dict from self._SYSProperties (used by JSONTransformer). + Propagate self.json_dict with current class instance attribute values (self._SYSProperties) + and with empty (None) class instance references (processed from JSONTransformer). + + Removes 'SYSServiceMethod' from json_dict and initializes child class references to None. """ - self.logger.debug('self._SYSProperties:{}'.format(self._SYSProperties)) + for property_id in self._SYSProperties: self.logger.debug('processing property:{}'.format(property_id)) self.json_dict[property_id] = getattr(self, property_id) + # Remove optional service method key if present; ignore if absent. + self.json_dict.pop('SYSServiceMethod', None) + + self.logger.debug('self._SYSProperties:{}'.format(self._SYSProperties)) + + for class_name in self._SYSClassNames: + self.json_dict[class_name] = None + + self.logger.debug('JSONDict:{}'.format(self.json_dict)) + class MultiClassHandler(BaseHandler): """ MultiObject handler class. @@ -264,6 +331,9 @@ def __iter__(self): """ overloaded internal __iter__() method. Overloaded for using iter() on class references. + + :return: generator yielding class instances from _object_container + :rtype: generator """ for class_instance in self._object_container: yield class_instance @@ -301,7 +371,10 @@ def set_properties(self, property_list): def set_json_dict(self): """ set_json_dict() method. - Preprare self.json_dict from self (self._object_container)). + Prepare self.json_dict from self._object_container. + + Iterates through all instances in _object_container and appends their + json_dict to create a list. Removes empty entries. """ self.logger.debug('Object container:{}'.format(self._object_container)) class_name = self.class_name @@ -316,7 +389,10 @@ def set_json_dict(self): def set_instance_json_dict(self): """ set_instance_json_dict() method. - Preprare self.json_dict from self._SYSProperties (used by JSONTransformer). + Prepare self.json_dict from self._SYSProperties (used by JSONTransformer). + + Extracts all property values and populates json_dict, ignoring any errors + from missing or inaccessible attributes. """ for property_id in self._SYSProperties: try: @@ -365,6 +441,9 @@ def __repr__(self): """ overloaded __repr__() method. Print out class mappings, properties and references. + + :return: formatted string with class mappings, properties and references + :rtype: str """ return 'Class mappings:{} properties:{} references:{}'.format( self._class_mappings, @@ -393,6 +472,16 @@ def get_references(self): """ return self._class_references + def get_class_hierarchy(self): + """ get_class_hierarchy() method. + + :return: self._class_hierarchy + :rtype: dict + + Get class hierarchy dictionary. + """ + return self._class_hierarchy + def _map( self, *, @@ -406,7 +495,7 @@ def _map( :param dict *: used for passing params as **args dictionary :param str class_name: (root) class name :param dict property_ref: property reference dictionary - :param classref parent_instance: property reference dictionary + :param classref parent_instance: object reference :param dict children: children definition dictionary Recursive map class hierarchy / class instances. @@ -539,7 +628,7 @@ def _map( getattr(ci, ci.SYSServiceMethod)() except (TypeError, AttributeError) as e: self.logger.debug('SYSServiceMethod call exception:{}'.format(e)) - except Exception as e: + except (KeyError, TypeError, AttributeError) as e: self.logger.debug('Class reference in service call metadata not set:{}'.format(e)) @@ -548,23 +637,255 @@ class ServiceExecuter(): """ def __init__(self): - pass + """ + :ivar classref logger: logging logger reference + :ivar classref _cm_ref: class mapper reference + :ivar dict _con_ref_dict: connection reference dictionary + :ivar dict _class_hierarchy: class hierarchy dictionary + :ivar list _class_hierarchy_list: list of class hierarchy items + :ivar int _hierarchy_level: current hierarchy level counter + :ivar int _map_hierarchy_level: mapping hierarchy level counter + :ivar dict _class_hierarchy_comp: class hierarchy comparison dictionary + :ivar int _hierarchy_level_comp: hierarchy level comparison counter + """ + + self.logger = logging.getLogger(__name__) + + self._cm_ref = None + self._con_ref_dict = None + self._class_hierarchy = None + self._class_hierarchy_list = None + self._hierarchy_level = None + self._map_hierarchy_level = None + self._class_hierarchy_comp = None + self._hierarchy_level_comp = None def execute(self, class_mapper, service_data): + """ execute() method. + + Execute service calls by creating ServiceMapper instances for each data item. + + :param classref class_mapper: class mapper instance reference + :param dict service_data: data dictionary with 'data' key containing list of items + :return: list of ServiceMapper instances + :rtype: list """ + + rlist = [] + for item in service_data['data']: + class_mapper_copy = copy.deepcopy(class_mapper) + sm_ref = ServiceMapper( + class_mapper=class_mapper_copy, + service_call_data=item + ) + rlist.append(sm_ref) + return rlist + + def execute_get_hierarchy(self, class_mapper, service_data): + """ execute_get_hierarchy() method. + + Execute service calls and return connected class hierarchies with json_dicts. + :param classref class_mapper: class mapper instance reference - :param list service_data: list of service call metadata dictionary items + :param dict service_data: data dictionary with 'data' key containing list of items + :return: list of connected class hierarchy dictionaries + :rtype: list """ rlist = [] for item in service_data['data']: - res = ServiceMapper( - class_mapper=class_mapper, + class_mapper_copy = copy.deepcopy(class_mapper) + ServiceMapper( + class_mapper=class_mapper_copy, service_call_data=item ) - rlist.append(res) + rlist.append( + self._connect_hierarchy(class_mapper_copy) + ) return rlist + def _connect_hierarchy(self, class_mapper_ref): + """ _connect_hierarchy() method. + + Init method for connecting all generated json_dicts. + + :param classref class_mapper_ref: class mapper instance reference + :return: connected class hierarchy reference dictionary + :rtype: dict + """ + + self._cm_ref = class_mapper_ref + cm_ref_dict = self._cm_ref.get_references() + + self.logger.debug('Processing class_mapper references dict:{}'.format(cm_ref_dict)) + self.logger.debug('Mapping parent_object instances to child instances') + + self._map_hierarchy_level = -1 + self._map_object_instances(cm_ref_dict) + + sum_children = ChildCounter().get_sum_child_count(cm_ref_dict) + self.logger.debug('Sum children:{}'.format(sum_children)) + + while sum_children > 0: + + self._hierarchy_level = -1 + self._class_hierarchy = {} + self._class_hierarchy_list = [] + + self._connect_hierarchy_recursive(cm_ref_dict) + + self.logger.debug('Class hierarchy list:{}'.format( + self._class_hierarchy_list) + ) + + for class_hierarchy_item in self._class_hierarchy_list: + + self._class_hierarchy_comp = {} + self._hierarchy_level_comp = -1 + + self._class_hierarchy = class_hierarchy_item + + self._rename_dict_key(cm_ref_dict) + self.logger.debug('Renamed children dict:{}'.format(cm_ref_dict)) + + sum_children = ChildCounter().get_sum_child_count(cm_ref_dict) + self.logger.debug('Sum children:{}'.format(sum_children)) + + return cm_ref_dict + + def _map_object_instances(self, ref_dict): + """ _map_object_instances() method. + + Recursively map object instances to reference dictionary and perform JSON transform on root. + + :param dict ref_dict: reference dictionary to process + """ + + for class_name, class_props in ref_dict.items(): + + if 'children' in class_props: + + child_dict = class_props['children'] + key_first = next(iter(child_dict)) + elm_first = child_dict[key_first] + ref_dict[class_name]['object_instance'] = elm_first['parent_instance'] + + self._map_hierarchy_level += 1 + self._map_object_instances(class_props['children']) + self._map_hierarchy_level -= 1 + + if self._map_hierarchy_level == -1: + self.logger.debug('Root object JSON transform:{}'.format(class_name)) + class_props['object_instance'].json_transform() + + def _connect_hierarchy_recursive(self, reference_dict, parent_class=None, parent_dict=None): + """ _connect_hierarchy_recursive() method. + + Recursively connect all generated json_dicts to their parents. + + :param dict reference_dict: reference dictionary to process + :param str parent_class: parent class name (optional) + :param dict parent_dict: parent dictionary reference (optional) + """ + + self.logger.debug('Parent dict:{}'.format(parent_dict)) + + if parent_class is not None: + self._class_hierarchy[self._hierarchy_level] = parent_class + + for class_name, class_properties in reference_dict.items(): + + if 'children' in class_properties: + self._hierarchy_level += 1 + self._connect_hierarchy_recursive( + class_properties['children'], + class_name, + reference_dict + ) + del self._class_hierarchy[self._hierarchy_level] + self._hierarchy_level -= 1 + else: + parent_instance = class_properties['parent_instance'] + parent_class_name = parent_instance.class_name + src_instance = getattr(parent_instance, class_name) + parent_instance.json_dict[class_name] = src_instance.json_dict + + self.logger.debug('Mapping class_name:{} parent_class_name:{}'.format( + class_name, + parent_class_name)) + + self._class_hierarchy_list.append( + copy.deepcopy(self._class_hierarchy) + ) + + def _rename_dict_key(self, rename_dict, parent_dict=None, parent_class=None): + """ _rename_dict_key() method. + + Recursively rename 'children' keys to 'children_processed' when hierarchy matches. + + :param dict rename_dict: dictionary to process + :param dict parent_dict: parent dictionary reference (optional) + :param str parent_class: parent class name (optional) + """ + + self.logger.debug('Parent class:{}'.format(parent_class)) + self.logger.debug('RenameDict:{}'.format(rename_dict)) + + if parent_class is not None: + self._class_hierarchy_comp[self._hierarchy_level_comp] = parent_class + + if self._class_hierarchy == self._class_hierarchy_comp: + self.logger.debug('Match rename_dict:{}'.format(rename_dict)) + + # only remove when all 'children' keys have been altered to 'children_processed' + if ChildCounter().get_sum_child_count(dict(rename_dict)) == 0: + self.logger.debug('Parent dict:{}'.format(parent_dict)) + set_dict = parent_dict[parent_class].pop('children') + parent_dict[parent_class]['children_processed'] = set_dict + + for class_name, class_properties in rename_dict.items(): + if 'children' in class_properties: + self._hierarchy_level_comp += 1 + self._rename_dict_key( + class_properties['children'], + rename_dict, + class_name + ) + del self._class_hierarchy_comp[self._hierarchy_level_comp] + self._hierarchy_level_comp -= 1 + + +class ChildCounter(): + """ Child node counter class. + """ + + def __init__(self): + """ + :ivar int _children_occurrences: counter for children node occurrences + :ivar classref logger: logging logger reference + """ + self._children_occurrences = 0 + self.logger = logging.getLogger(__name__) + + def get_sum_child_count(self, reference_dict): + """ get_sum_child_count() method. + + Count children nodes recursively and return sum. + + :param dict reference_dict: reference dictionary to process + :return: total count of 'children' keys in hierarchy + :rtype: int + """ + + self.logger.debug('Sum count ref-dict:{}'.format(reference_dict)) + + for class_name, class_properties in reference_dict.items(): + if 'children' in class_properties: + self._children_occurrences += 1 + self.get_sum_child_count(class_properties['children']) + + return self._children_occurrences + # import classes into current namespace current_mod = sys.modules[__name__] diff --git a/src/router.py b/src/router.py new file mode 100644 index 0000000..eedea47 --- /dev/null +++ b/src/router.py @@ -0,0 +1,41 @@ +# ]*[ --------------------------------------------------------------------- ]*[ +# . Micro ESB Router Python Module . +# ]*[ --------------------------------------------------------------------- ]*[ +# . . +# . Copyright Claus Prüfer 2016-2026 . +# . . +# . . +# ]*[ --------------------------------------------------------------------- ]*[ + +import logging +import importlib + +logger = logging.getLogger(__name__) + +try: + mod_ref = importlib.import_module('user_routing') +except ImportError as e: + pass + + +class ServiceRouter(): + """ ServiceRouter class. + + Provides routing functionality to user-defined service methods in user_routing module. + """ + + def send(self, send_id, metadata): + """ send() method. + + Execute method with given id in `send_id` from imported user_routing.py module + and return result dict or None. + + :param str send_id: service method id (function name in user_routing module) + :param dynamic metadata: first argument passed to service method function + :return: result from user routing function + :rtype: dict | None + """ + logger.debug('ServiceRouter send_id:{} metadata:{}'.format(send_id, metadata)) + func_ref = getattr(mod_ref, send_id) + logger.debug('FuncRef:{}'.format(func_ref)) + return func_ref(metadata) diff --git a/src/test.py b/src/test.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/testclasses.py b/src/testclasses.py index 4502674..86aaad7 100644 --- a/src/testclasses.py +++ b/src/testclasses.py @@ -1,43 +1,92 @@ +# ]*[ --------------------------------------------------------------------- ]*[ +# . Micro ESB Test Classes Module . +# ]*[ --------------------------------------------------------------------- ]*[ +# . . +# . Copyright Claus Prüfer (2016 - 2026) . +# . . +# . . +# ]*[ --------------------------------------------------------------------- ]*[ + from microesb import microesb class Cert(microesb.ClassHandler): - pass + """ Certificate handler class. + + Base class for certificate types (CA, Server, Client). + """ class CertCA(Cert): + """ Certificate Authority handler class. + + Handles CA certificate instances. + """ def __init__(self): + """ + :ivar str type: certificate type identifier + """ self.type = 'ca' super().__init__() class CertServer(Cert): + """ Server certificate handler class. + + Handles server certificate instances. + """ def __init__(self): + """ + :ivar str type: certificate type identifier + """ self.type = 'server' super().__init__() class CertClient(Cert): + """ Client certificate handler class. + + Handles client certificate instances. + """ def __init__(self): + """ + :ivar str type: certificate type identifier + """ self.type = 'client' super().__init__() class Smartcard(microesb.ClassHandler): + """ Smartcard handler class. + + Handles smartcard instances for certificate storage. + """ def __init__(self): super().__init__() class SmartcardContainer(microesb.ClassHandler): + """ Smartcard container handler class. + + Handles smartcard container instances for key pair storage. + """ def __init__(self): super().__init__() class Shipment(microesb.ClassHandler): + """ Shipment handler class. + + Handles shipment instances. + """ def __init__(self): super().__init__() class Palette(microesb.MultiClassHandler): + """ Palette handler class. + + Handles multiple palette instances using MultiClassHandler. + """ def __init__(self): super().__init__() diff --git a/src/transformer.py b/src/transformer.py index d9ea923..351cfa5 100644 --- a/src/transformer.py +++ b/src/transformer.py @@ -2,53 +2,44 @@ # . Micro ESB transformer Python Module . # ]*[ --------------------------------------------------------------------- ]*[ # . . -# . Copyright Claus Prüfer 2016-2025 . +# . Copyright Claus Prüfer 2016-2026 . # . . # . . # ]*[ --------------------------------------------------------------------- ]*[ import json -import copy class JSONTransformer(): - """ JSON transfomer class. + """ JSON transformer class. + + Provides JSON transformation functionality for class hierarchies. """ def __init__(self): """ - :ivar dict[dict] _json_dict: recursive internal properties processing dict + :ivar dict _json_dict: recursive internal properties processing dict """ self._json_dict = {} def json_transform(self): """ json_transform() method. - Recursive generate _json_dict for complete object hierarchy. - """ + Recursively generate _json_dict for complete object hierarchy. - root_instance = copy.copy(self) + Iterates through all elements in the hierarchy and calls set_json_dict() + on each to populate their json_dict representation. + """ - for element in root_instance.iterate(): + for element in self.iterate(): element.set_json_dict() - self.logger.debug('JSON:{} properties:{}'.format( + self.logger.debug( + 'JSON:{} properties:{}'.format( element.json_dict, element._SYSProperties ) ) - while root_instance.class_count > 0: - for element in root_instance.iterate(): - if element.class_count == 0 and element._SYSType != 'multiclass_instance': - cname = element.class_name - parent_element = element.parent_object - parent_element._json_dict[cname] = element.json_dict[cname] - class_names_list = parent_element._SYSClassNames - del class_names_list[class_names_list.index(cname)] - - self._json_dict = root_instance.json_dict - del root_instance - @property def json(self): """ json() method. diff --git a/test/integration/test_base.py b/test/integration/test_base.py index 02559bd..8cbbab6 100644 --- a/test/integration/test_base.py +++ b/test/integration/test_base.py @@ -273,6 +273,8 @@ def test_multi_item_object( shipment = getattr(s[0]._class_mapper, 'Shipment') palette = getattr(shipment, 'Palette') + assert getattr(shipment, 'id') == 'testshipment1' + p1 = palette._object_container[0] p2 = palette._object_container[1] @@ -280,13 +282,3 @@ def test_multi_item_object( assert getattr(p1, 'label') == 'label1' assert getattr(p2, 'id') == 2 assert getattr(p2, 'label') == 'label2' - - shipment.json_transform() - assert shipment.json_dict == { - 'id': 'testshipment1', - 'SYSServiceMethod': None, - 'Palette': [ - {'id': 1, 'label': 'label1'}, - {'id': 2, 'label': 'label2'} - ] - } diff --git a/test/integration/test_json_transform.py b/test/integration/test_json_transform.py new file mode 100644 index 0000000..6f4a809 --- /dev/null +++ b/test/integration/test_json_transform.py @@ -0,0 +1,280 @@ +import pytest + +from microesb import microesb + + +@pytest.fixture +def config_class_listobject(): + config = { + 'class_mapping': { + 'Shipment': 'Shipment', + 'Palette': 'Palette', + }, + 'class_reference': { + 'Shipment': { + 'property_ref': 'Shipment', + 'children': { + 'Palette': { + 'property_ref': 'Palette' + } + } + } + } + } + return config + + +@pytest.fixture +def config_properties_listobject(): + config = { + 'Shipment': { + 'properties': { + 'id': { + 'type': 'int', + 'default': None, + 'required': True, + 'description': '' + } + } + }, + 'Palette': { + 'properties': { + 'id': { + 'type': 'str', + 'default': None, + 'required': True, + 'description': '' + }, + 'label': { + 'type': 'str', + 'default': None, + 'required': True, + 'description': '' + } + } + } + } + return config + + +@pytest.fixture +def config_service_listobject(): + config = { + 'id': 'processScan', + 'data': [ + { + 'Shipment': { + 'id': 'testshipment1', + 'Palette': [ + { + 'id': 1, + 'label': 'label1' + }, + { + 'id': 2, + 'label': 'label2' + } + ] + } + } + ] + } + return config + + +@pytest.fixture +def config_class_pki_service(): + config = { + 'class_mapping': { + 'CertCA': 'CertCA', + 'SmartcardCA': 'Smartcard', + 'SmartcardREQ': 'Smartcard', + 'SmartcardContainer': 'SmartcardContainer' + }, + 'class_reference': { + 'CertCA': { + 'property_ref': 'Cert', + 'children': { + 'SmartcardCA': { + 'property_ref': 'Smartcard', + 'children': { + 'SmartcardContainer': {'property_ref': 'SmartcardContainer'} + } + }, + 'SmartcardREQ': { + 'property_ref': 'Smartcard', + 'children': { + 'SmartcardContainer': {'property_ref': 'SmartcardContainer'} + } + } + } + } + } + } + return config + + +@pytest.fixture +def config_properties_pki_service(): + config = { + 'Cert': { + 'properties': { + 'id': { + 'type': 'str', + 'default': None, + 'required': True, + 'description': 'cert id' + }, + 'country': { + 'type': 'str', + 'default': 'DE', + 'required': True, + 'description': 'country' + }, + 'state': { + 'type': 'str', + 'default': None, + 'required': True, + 'description': '' + } + } + }, + 'Smartcard': { + 'properties': { + 'label': { + 'type': 'str', + 'default': None, + 'required': True, + 'description': 'label' + }, + 'user_pin': { + 'type': 'str', + 'default': None, + 'required': True, + 'description': 'user pin' + } + } + }, + 'SmartcardContainer': { + 'properties': { + 'label': { + 'type': 'str', + 'default': None, + 'required': True, + 'description': 'label' + } + } + } + } + return config + + +@pytest.fixture +def config_service_pki(): + config = { + 'id': 'addCACertificate', + 'data': [ + { + 'CertCA': { + 'id': 'testid1', + 'country': 'DE', + 'state': 'Berlin', + 'locality': 'Berlin', + 'org': 'WEBcodeX', + 'org_unit': 'Security', + 'common_name': 'testcn1', + 'email': 'pki@webcodex.de', + 'valid_days': 365, + 'key_ref': 'Smartcard', + 'SmartcardCA': { + 'label': 'label1', + 'user_pin': 'pin1', + 'SmartcardContainer': { + 'label': 'container_label1' + } + }, + 'SmartcardREQ': { + 'label': 'label2', + 'user_pin': 'pin2', + 'SmartcardContainer': { + 'label': 'container_label2' + } + } + } + } + ] + } + return config + + +class TestExecuteGetHierarchy: + + def test_transform_pki( + self, + config_class_pki_service, + config_properties_pki_service, + config_service_pki + ): + + class_mapper = microesb.ClassMapper( + class_references=config_class_pki_service['class_reference'], + class_mappings=config_class_pki_service['class_mapping'], + class_properties=config_properties_pki_service + ) + + r = microesb.ServiceExecuter().execute_get_hierarchy( + class_mapper=class_mapper, + service_data=config_service_pki + ) + + root_object = r[0]['CertCA']['object_instance'] + + assert root_object.json_dict == { + 'id': 'testid1', + 'country': 'DE', + 'state': 'Berlin', + 'SmartcardCA': { + 'label': 'label1', + 'user_pin': 'pin1', + 'SmartcardContainer': { + 'label': 'container_label1' + } + }, + 'SmartcardREQ': { + 'label': 'label2', + 'user_pin': 'pin2', + 'SmartcardContainer': { + 'label': 'container_label2' + } + } + } + + def test_list_object( + self, + config_service_listobject, + config_class_listobject, + config_properties_listobject + ): + + class_mapper = microesb.ClassMapper( + class_references=config_class_listobject['class_reference'], + class_mappings=config_class_listobject['class_mapping'], + class_properties=config_properties_listobject + ) + + r = microesb.ServiceExecuter().execute_get_hierarchy( + class_mapper=class_mapper, + service_data=config_service_listobject + ) + + root_object = r[0]['Shipment']['object_instance'] + + assert root_object.json_dict == { + 'id': 'testshipment1', + 'Palette': { + 'Palette': [ + { 'id': 1, 'label': 'label1' }, + { 'id': 2, 'label': 'label2' } + ] + } + }