Federation
@startuml skinparam ParticipantPadding 50 participant "Node A\n(Origin Node)" as NodeA participant "Node B\n(Destination Node)" as NodeB == Node pairing == NodeA <-> NodeB: Pair nodes note over NodeA, NodeB: The nodes exchange identity and authentication credentials via a node handshake ... == Structure syncing == NodeA -> NodeA: Expose structures note over NodeA Origin node determines what structures it wishes to expose to the destination node. end note NodeA -> NodeB: Structure sync NodeB -> NodeB: Module mapping note over NodeB Destination node determines what origin fields should map to what destination fields. end note ... == Data syncing == NodeA -> NodeB: Data sync @enduml
- Node pairing
-
The process of establishing a federated network between two nodes. After they are paired, they are able to securely exchange data. Refer to Pairing nodes and Authentication for details.
- Structure syncing
-
The process of determining what structures on the origin node (the one that contains the data), is exposed to it’s destination node (the one that wishes to access the data). The origin node has full control over what data the destination node is allowed to access. This can be as simple as allowing access to only specific modules; and as complex as allowing access to only specific module fields. Refer to Syncing structures and Access control for details.
- Data syncing
-
The process of updating data on the destination node, based on the updated data source. The destination node has the ability to determine what field from the origin node maps into what field in the destination node. Refer to Syncing data for details.
Glossary
Pairing
|
Before the two nodes are paired, we are unable to determine what node is the data source and what node is the destination. To avoid confusion, we use node A and node B when referring to the two nodes. |
- Node URI
-
The URI that identifies a federation node.
- Node A
-
The node that wishes to include another node in it’s federated network — the node that initialized the pairing process.
- Node B
-
The node that wishes to join the federated network.
- NodeID A
-
Node ID of the federated node A.
- NodeID B
-
Node ID of the federated node B.
- Token A
-
The authentication token to access protected resources on node A.
- Token B
-
The authentication token to access protected resources on node B.
Data exchange
- Origin node
-
The node that shares the data to the other node. If node A requests data from node B, node B is origin, as it contains the data.
- Destination node
-
The node that receives the data from the origin node. If node A requests data from node B, node A is destination as it receives the data.
Federated node
- Node status
-
The current status of the federated node. Can be one of the following:
-
Pairing: the node is in the pairing process,
-
paired: the node has been paired successfully,
-
failed: the node failed to pair.
-
- Structure sync status
-
The current status of the structure sync. Can be one of the following:
-
Syncing: the node is currently syncing structures from the origin node,
-
synced: the node has successfully finished syncing,
-
failed: there was an error while syncing.
-
- Data sync status
-
The current status of the data sync. Can be one of the following:
-
Syncing: the node is currently syncing data from the origin node,
-
synced: the node has successfully finished syncing,
-
failed: there was an error while syncing.
-
Security
Authentication
Federated nodes leverage Corteza’s already established authentication facility that uses system user and JWT tokens (later referred as a token). During the pairing process between the two nodes, both nodes create a system user and securely exchange their authentication tokens.
-
Node A defines a system user and token A that allows node B to access protected resources,
-
node B defines a system user and token B that allows node A to access protected resources.
Whenever node A wishes to access protected resources on node B, it uses token B for authentication; vice-versa for node B accessing protected resources on node A.
|
System users and authentication tokens are unique for each node pair. When a new node is added to the federated network, a new pair of system users and tokens are generated. |
|
Token A and token B are not the same. |
Access control
The federation service uses Corteza’s already established RBAC access control facility that operates over user roles, system resources and operations over given resources.
Each node in a federated network has the ability to define what resources a paired node is allowed to access. This can be as simple as controlling access to records belonging to specific modules, or as detailed as controlling access to specific fields for specific modules.
-
Passes the request through the access control facility to check if we are allowed to access the resource,
-
collects the requested data, following the defined RBAC permissions, excluding data that the destination node is not allowed to access.
The federation service then performs some additional operations, see Syncing data, and sends it over to the destination node.
Logging
Corteza federation service keeps track of the important events (actions) that occurred between the two federated nodes. The logs are stored inside Corteza’s actionlog facility; it stores:
-
When the action ocurred,
-
the invoking user,
-
the accessed resource,
-
the performed operation,
-
the result of the operation, …
Logged events
Node pairing
- Pairing started
-
Logged when the nodes initiate in the pairing process.
- Pairing failed
-
Logged when an error occurs during the pairing process.
- Pairing finished
-
Logged when the nodes have been paired successfully.
Pairing nodes
The process of establishing a federated network between two nodes with intent of securely sharing data. The process consists of two steps:
- Node identification
-
The step identifies the two nodes so they know where to access the information.
- Node handshake
-
The step exchanges the authentication tokens so the two nodes can access protected resources from each other.
@startuml skinparam responseMessageBelowArrow true actor "Administrator A" as AdministratorA participant "Node A" as NodeA participant "Node B" as NodeB actor "Administrator B" as AdministratorB == Node identification == AdministratorA->NodeA: Register federated node B note right of AdministratorA Administrator A sends the node URI to administrator B via a secure channel. end note AdministratorA-->AdministratorB: Send node URI ... note left of AdministratorB Administrator B registers node A using the node URI. end note AdministratorB->NodeB: Register federated node A == Node handshake == AdministratorB->NodeB: Initialize the handshake note over NodeB #FFAAAA Prepare the node's state for a federated network. end note NodeB->NodeA: Request handshake NodeA-->AdministratorA: Notify administrator A note right of AdministratorA #ffffff Administrator A should manually approve the handshake. end note ... AdministratorA->NodeA: Approve handshake request note over NodeA #FFAAAA Prepare the node's state for a federated network. end note NodeA->NodeB: Complete handshake @enduml
Node identification
@startuml skinparam responseMessageBelowArrow true actor "Administrator A" as AdministratorA participant "Node A" as NodeA participant "Node B" as NodeB actor "Administrator B" as AdministratorB AdministratorA->NodeA: Register federated node B note right of AdministratorA Administrator A sends the node URI to administrator B via a secure channel. end note AdministratorA-->AdministratorB: Send node URI ... note left of AdministratorB Administrator B registers node A using the node URI. end note AdministratorB->NodeB: Register federated node A @enduml
|
No authentication tokens are exchanged in during the identification step. |
- Node A administrator registers node B and generates it’s node URI
-
This step lets node A know about node B. The generated node URI identifies node A and is in the form of
corteza://$NODE_ID_A:$OTT@$DOMAIN_A?name=$NAME.
|
|
# Base URL of node A api
$API_A_BASE
# Main administrator JWT for node A
$MAIN_JWT_A
# Node A domain
$DOMAIN_A
# Node B domain
$DOMAIN_B
# Node name
$NODE_NAME
# Node A nodeID
$NODE_ID_A
# Node B nodeID
$NODE_ID_B
# Node URI
$NODE_URI
curl -X POST "$API_A_BASE/federation/nodes" \
-H "authorization: Bearer $MAIN_JWT_A" \
--header "Content-Type: application/json" \
--data "{
\"myDomain\": \"$DOMAIN_A\",
\"domain\": \"$DOMAIN_B\",
\"name\": \"$NODE_NAME\"
}";
{
"response": {
"nodeID": "$NODE_ID_A",
"sharedNodeID": "$NODE_ID_B",
"name": "\"$NODE_NAME\"",
"domain": "\"$DOMAIN_B\"",
"status": "\"pending\"",
"nodeURI": "\"$NODE_URI\""
}
}
- Node A administrator sends the node URI to the node B administrator
-
This step transports the node URI to the node that we wish to pair with.
|
This step is performed manually by the node A administrator. The two administrators should use a secure channel in order to exchange this information. |
- Node B administrator registers node A using the node URI
-
This step lets node B know about node A. Both nodes A and B are now identified and prepared to perform the Node handshake.
# Base URL of node B api
$API_B_BASE
# Main administrator JWT for node B
$MAIN_JWT_B
# Node B domain
$DOMAIN_B
# Node A domain
$DOMAIN_A
# Node name
$NODE_NAME
# Node B nodeID
$NODE_ID_B
# Node A nodeID
$NODE_ID_A
# Node URI
$NODE_URI
curl -X POST "$API_B_BASE/federation/nodes" \
-H "authorization: Bearer $MAIN_JWT_B" \
--header "Content-Type: application/json" \
--data "{
\"myDomain\": \"$DOMAIN_B\",
\"nodeURI\": \"$NODE_URI\"
}";
{
"response": {
"nodeID": "$NODE_ID_B",
"sharedNodeID": "$NODE_ID_A",
"name": "\"$NODE_NAME\"",
"domain": "\"$DOMAIN_A\"",
"status": "\"pending\"",
"nodeURI": "\"$NODE_URI\""
}
}
|
In the above response, |
Node handshake
@startuml skinparam responseMessageBelowArrow true actor "Administrator A" as AdministratorA participant "Node A" as NodeA participant "Node B" as NodeB actor "Administrator B" as AdministratorB AdministratorB->NodeB: Initialize the handshake note over NodeB #FFAAAA Prepare the node's state for a federated network. end note NodeB->NodeA: Request handshake NodeA-->AdministratorA: Notify administrator A note right of AdministratorA #ffffff Administrator A should manually approve the handshake. end note ... AdministratorA->NodeA: Approve handshake request note over NodeA #FFAAAA Prepare the node's state for a federated network. end note NodeA->NodeB: Complete handshake @enduml
- Node B administrator initializes the process with node A
-
This configures the state on node B and generates the
$TOKEN_B; so node A will be able to access protected resources on node B.
# Base URL of node B api
$API_B_BASE
# Main administrator JWT for node B
$MAIN_JWT_B
# Node B nodeID
$NODE_ID_B
curl -X POST "$API_B_BASE/federation/nodes/$NODE_ID_B/pair" \
-H "authorization: Bearer $MAIN_JWT_B" \
--header "Content-Type: application/json";
{}
- Node B sends a handshake request to node A
-
This notifies the node A administrator that node B wishes to establish a federated network. This request must be manually confirmed by the node administrator.
|
This request is authenticated by the before generated |
# Base URL of node A api
$API_A_BASE
# Node A nodeID
$NODE_ID_A
# Node URI
$NODE_URI
# Node B auth token
$TOKEN_B
# Node B nodeID
$NODE_ID_B
curl -X POST "$API_A_BASE/federation/nodes/$NODE_ID_A/handshake" \
--header "Content-Type: application/json" \
--data "{
\"nodeURI\": \"$NODE_URI\",
\"token\": \"$TOKEN_B\",
\"nodeIDB\": \"$NODE_ID_B\"
}";
{}
- Node A administrator confirms the handshake request
-
This configures the state on node A and generates the
$TOKEN_A; so node B will be able to access protected resources on node A.
# Base URL of node A api
$API_A_BASE
# Node A nodeID
$NODE_ID_A
# Main administrator JWT for node A
$MAIN_JWT_A
curl -X POST "$API_A_BASE/federation/nodes/$NODE_ID_A/handshake-confirm" \
-H "authorization: Bearer $MAIN_JWT_A" \
--header "Content-Type: application/json";
{}
- Node A completes the handshake with node B
-
This sends the
$TOKEN_Ato node B so it will be able to access protected resources on node A.
|
Notice how node A uses |
# Base URL of node B api
$API_B_BASE
# Node B nodeID
$NODE_ID_B
# Node B auth token
$TOKEN_B
# Node A auth token
$TOKEN_A
curl -X POST "$API_B_BASE/federation/nodes/$NODE_ID_B/handshake-complete" \
-H "authorization: Bearer $TOKEN_B" \
--header "Content-Type: application/json" \
--data "{
\"token\": \"$TOKEN_A\"
}";
{}
Syncing structures
The process of determining what structures on the origin node, is exposed to it’s destination node. The origin node has full control over what data the destination node is allowed to access. This can be as simple as allowing access to only specific modules; and as complex as allowing access to only specific module fields.
|
The two nodes must be paired prior to this. See Node pairing. |
Exposing structures
Firstly, in order to perform any data sharing, the origin node must define what structures (in our case modules and fields) the destination node can access. This can be performed via the Corteza Low-Code administration panel, or directly via the API.
|
Besides the module itself, the origin node must also specify what fields the destination node is allowed to access. |
API
Expose module
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node
$NODE_ID
# Federation module id
$MODULE_ID
curl -X PUT "$BASE_URL/federation/nodes/$NODE_ID/modules/$MODULE_ID" \
-H "Authorization: Bearer $JWT"
-H "Content-Type: application/json" \
--data "[{
\"name\":\"LinkedIn\",
\"label\":\"LinkedIn Url\",
\"kind\":\"Url\",
\"is_multi\":0
}]";
{
"response": {
"moduleID": "122709113267335170",
"handle": "Account",
"name": "Account",
"createdAt": "2019-12-18T17:45:15Z",
"updatedAt": "2020-05-26T13:29:36Z",
"fields": [
{
"name": "LinkedIn",
"label": "LinkedIn Url",
"kind": "Url",
"is_multi": 0
}
]
}
}
See exposed module
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node
$NODE_ID
curl -X GET "$BASE_URL/federation/nodes/$NODE_ID/modules/$MODULE_ID/exposed" \
-H "Authorization: Bearer $JWT";
{
"response": {
"moduleID": "122709113267335170",
"handle": "Account",
"name": "Account",
"createdAt": "2019-12-18T17:45:15Z",
"updatedAt": "2020-05-26T13:29:36Z",
"fields": [
{
"kind": "Url",
"name": "LinkedIn",
"label": "LinkedIn",
"isMulti": false,
},
{
"kind": "String",
"name": "Phone",
"label": "Phone",
"isMulti": false,
}
]
}
}
Remove exposed module
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node (?exposed) or the origin node (?shared)
$NODE_ID
# Federation module id
$MODULE_ID
curl -X DELETE "$BASE_URL/federation/nodes/$NODE_ID/modules/$MODULE_ID" \
-H "authorization: Bearer $JWT";
Change exposed module fields
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node
$NODE_ID
# Federation module id
$MODULE_ID
curl -X PUT "$BASE_URL/federation/nodes/$NODE_ID/modules/$MODULE_ID/exposed" \
-H "Authorization: Bearer $JWT"
-H "Content-Type: application/json" \
--data "[{
\"name\":\"LinkedIn\",
\"label\":\"LinkedIn\",
\"kind\":\"Url\",
\"is_multi\":1
}, {
\"name\":\"Phone\",
\"label\":\"Phone\",
\"kind\":\"String\",
\"is_multi\":0
}]";
{
"response": {
"moduleID": "122709113267335170",
"handle": "Account",
"name": "Account",
"createdAt": "2019-12-18T17:45:15Z",
"updatedAt": "2020-05-26T13:29:36Z",
"fields": [
{
"kind": "Url",
"name": "LinkedIn",
"label": "LinkedIn",
"isMulti": 0,
},
{
"kind": "String",
"name": "Phone",
"label": "Phone",
"isMulti": 0,
}
]
}
}
Syncing structures
When the two nodes are paired and the origin node has exposed some structures, the actual structure sync can occur.
@startuml skinparam responseMessageBelowArrow true hide footbox actor "Destination Node" as NodeDestination box "Origin Node" #f7f7f7 participant "Federation Rest Controller" as FRestController participant "Federation Service" as FederationService participant "Compose Module Service" as ComposeMS database Store participant "Federation Structure Service" as FStructureService participant Encoder NodeDestination -> FRestController: get structure note left : only the structure\nthat was updated after\nlast successful sync activate FRestController FRestController -> FederationService: get structure for all modules activate FederationService FederationService -> Store: get module id list activate Store Store -> FederationService: federated modules id list deactivate Store FederationService -> ComposeMS: get filtered compose Modules activate ComposeMS ComposeMS -> Store: get filtered list of modules activate Store Store -> ComposeMS: list of compose Modules deactivate Store ComposeMS -> FederationService: list of compose Modules deactivate ComposeMS FederationService -> FStructureService: module list + federated module ID list + federated field ID list note left : omit the fields we do not wish to share activate FStructureService FStructureService -> FederationService: module list with specific fields deactivate FStructureService FederationService -> Encoder: transform the list to specific structure activate Encoder Encoder -> FederationService: transformed list deactivate Encoder FederationService -> FRestController: transformed list of modules (structure) deactivate FederationService FRestController -> NodeDestination: list of modules deactivate FRestController end box @enduml
@startuml skinparam responseMessageBelowArrow true hide footbox actor "Origin Node" as NodeOrigin box "Destination Node" #f7f7f7 participant "Federation Service" as FederationService database Store participant "Federation Structure Service" as FStructureService participant Decoder FederationService -> NodeOrigin: get structure for module activate FederationService activate NodeOrigin NodeOrigin -> FederationService: structure for module deactivate NodeOrigin note left : federated Module structure from Origin FederationService -> Decoder: structure for module in specific format activate Decoder Decoder -> FederationService: structure for module deactivate Decoder FederationService -> FStructureService: structure for module and fields activate FStructureService FStructureService -> FederationService: remapped module with fields deactivate FStructureService FederationService -> Store: write federated modules and fields info FederationService -> Store: structure sync OK deactivate FederationService end box @enduml
- Destination node requests changed structures
-
Origin node provides a set of endpoints that the destination node can use to fetch shared structures.
| $TOKEN_B is the token that was generated during the handshake and is used to authenticate the user on the Origin node (the one who shares the data) by the Destination node. |
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node
$NODE_ID
# Node B auth token
$TOKEN_B
curl -X GET "$BASE_URL/federation/exposed/modules" \
-H "Authorization: Bearer $TOKEN_B";
{
"response": {
"filter": {
"query": "after=1600109447",
"page": 1,
"perPage": 20,
"count": 97,
},
"set": [
{
"type": "GET",
"rel": "Account",
"href": "$BASE_URL/federation/exposed/modules/$MODULE_ID?after=1600109447"
},
{
"type": "GET",
"rel": "Contact",
"href": "$BASE_URL/federation/exposed/modules/$MODULE_ID?after=1600109447"
}
]
}
}
- Destination node fetches updated structures
-
The destination node requests structure definitions based on the above provided set of endpoints. The origin node provides the structure definition with respect to the fields that the destination node is allowed to access.
| $TOKEN_B is the token that was generated during the handshake and is used to authenticate the user on the Origin node (the one who shares the data) by the Destination node. |
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the origin node
$NODE_ID
# Node B auth token
$TOKEN_B
curl -X GET "$BASE_URL/federation/exposed/modules/$MODULE_ID" \
-H "Authorization: Bearer $TOKEN_B";
{
"response": {
"moduleID": "122709113267335170",
"handle": "Account",
"name": "Account",
"createdAt": "2019-12-18T17:45:15Z",
"updatedAt": "2020-05-26T13:29:36Z",
"fields": [
{
"kind": "Url",
"name": "LinkedIn",
"label": "LinkedIn",
"isMulti": false,
},
{
"kind": "String",
"name": "Phone",
"label": "Phone",
"isMulti": false,
}
]
}
}
- Destination node updates its store
-
Once the structure sync is finished, the destination node writes the structures to the store and updates the nodes status.
|
This step doesn’t create any system resources, such as records, on the destination node. This is performed later in the data sync. |
Store
Shared modules
| Column | Type | Description |
|---|---|---|
ID |
uint64 |
federation module id |
handle |
varchar(200) |
Module handle |
name |
varchar(64) |
Module name |
rel_node |
uint64 |
node id (source node id - who is sharing with us) |
xref_module |
uint64 |
federation module id on source node (id in federation_module_exposed) |
fields |
json |
list of fields |
Field mapping
Field mapping allows the destination node to determine what fields from the origin node should map into what fields on the destination node. This allows some flexibility when it comes down to datamodel definitions on both origin and destination nodes. This can be performed via the Corteza Low-Code administration panel, or directly via the API.
API
Set field mapping for a module
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node
$NODE_ID
# Federation module id
$MODULE_ID
curl -X PUT "$BASE_URL/federation/nodes/$NODE_ID/modules/$MODULE_ID/mapped" \
-H "Authorization: Bearer $JWT"
-H "Content-Type: application/json" \
--data "[{
\"origin\":{
\"name\":\"LinkedIn\",
\"kind\":\"Url\",
\"is_multi\":0
},
\"destination\":{
\"name\":\"Social\",
\"kind\":\"String\",
\"is_multi\":0
}
}]";
{
"response": {
"moduleID": "122709113267335170",
"handle": "Account",
"name": "Account",
"createdAt": "2019-12-18T17:45:15Z",
"updatedAt": "2020-05-26T13:29:36Z",
"mapping": [
{
"origin": {
"name": "LinkedIn",
"kind": "Url",
"is_multi": 0
},
"destination": {
"name": "Social",
"kind": "String",
"is_multi": 0
}
}
]
}
}
Get field mapping for a module
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node
$NODE_ID
# Federation module id
$MODULE_ID
curl -X GET "$BASE_URL/federation/nodes/$NODE_ID/modules/$MODULE_ID/mapped" \
-H "Authorization: Bearer $JWT";
{
"response": {
"moduleID": "122709113267335170",
"handle": "Account",
"name": "Account",
"createdAt": "2019-12-18T17:45:15Z",
"updatedAt": "2020-05-26T13:29:36Z",
"mapping": [
{
"origin": {
"name": "LinkedIn",
"kind": "Url",
"is_multi": 0
},
"destination": {
"name": "Social",
"kind": "String",
"is_multi": 0
}
}
]
}
}
Remove field mapping for a module
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node (?exposed) or the origin node (?shared)
$NODE_ID
# Federation module id
$MODULE_ID
curl -X DELETE "$BASE_URL/federation/nodes/$NODE_ID/modules/$MODULE_ID" \
-H "authorization: Bearer $JWT";
Store
Module mapping
| Column | Type | Description |
|---|---|---|
federation_module_id |
uint64 |
id from federation_module_exposed |
compose_module_id |
uint64 |
existing module |
field_mapping |
json |
json field mappings, ex: [{ source: 'node_A_module_field_7', dest: 'node_B_module_field_2', transform: 'string' }] |
Example
Phase I - on Origin node
First phase is exposing the desired modules for a specific node (to the Destination), so the structure mapping on the Destination and then the data sync can be done.
compose_module
| id | handle | name |
|---|---|---|
161250629010849793 |
Account |
Account |
compose_module_field
| id | kind | name | label | is_multi |
|---|---|---|---|---|
161250629061509121 |
String |
Phone |
Phone |
0 |
161250629027758081 |
Url |
0 |
||
161250629044666369 |
String |
Description |
Description |
0 |
federation_node
| id | name |
|---|---|
1 |
Origin server |
2 |
Destination server |
federation_module_exposed
| id | rel_node | rel_compose_module | field_mapping |
|---|---|---|---|
11 |
2 |
161250629010849793 |
[{"name":"Phone","kind":"String","is_multi":0}] |
Phase II - on Destination node
There are 2 phases in the phase II. First the module info from Origin is saved. After that we can do the mapping. The modules on the Destination need to be created beforehand.
compose_module
| id | handle | name |
|---|---|---|
261250629010849755 |
Account_federated |
Account (federated from Origin) |
compose_module_field
| id | kind | name | label | is_multi |
|---|---|---|---|---|
161250629061509121 |
String |
Mobile |
Mobile |
0 |
161250629044666369 |
String |
Desc |
Description |
0 |
federation_node
| id | name |
|---|---|
1 |
Our server |
2 |
Misc server (some other server) |
3 |
Origin server in this example (from phase I) |
1. Fetch and save the module info
federation_module_shared
| id | handle | name | rel_node | xref_module | field_mapping |
|---|---|---|---|---|---|
22 |
Account |
Account |
3 |
11 |
[{"name":"Phone","kind":"String","is_multi":0}] |
2. Mapping finished, modules created
The sharing of modules info from Origin is added, that is enough information for us to handle mapping from UI. We can now pick the fields from the field_mapping column that we need and store it into the mapping table.
federation_module_mapping
| federation_module_id | compose_module_id | fields |
|---|---|---|
22 |
261250629010849755 |
[{"origin":{"name":"Phone","kind":"String","is_multi":0},"destination":{"name":"Mobile","kind":"String","is_multi":0}}] |
Syncing data
The process of updating data on the destination node, based on the updated data source. The destination node has the ability to determine what field from the origin node maps into what field in the destination node.
Data sync operates directly on compose services and storage layers, but is designed to be decoupled and moved away from the main corteza service.
|
The two nodes must be paired prior to this. See Pairing nodes. |
Syncing data
After the two nodes are paired and the structure syncing has finished, we can proceed with the data sync.
|
Data syncing uses already established compose services along with it’s storage layer. This removes the need for any additional storage layer modifications. |
@startuml skinparam responseMessageBelowArrow true actor "Origin Node" as NodeOrigin box "Destination Node" #f7f7f7 participant "Federation Service" as FederationService participant "Compose Record Service" as ComposeRS database Store participant "Federation Data Service" as FDataService participant Decoder activate FederationService FederationService -> NodeOrigin: get federated records for module activate NodeOrigin NodeOrigin -> FederationService: list of federated records deactivate NodeOrigin FederationService -> Store: get federation info on module and mapped fields activate Store Store -> FederationService: federation mappings deactivate Store FederationService -> Decoder: list of records in specific format note right: this is the transport structure\n coming from Origin Node activate Decoder Decoder -> FederationService: list of records deactivate Decoder FederationService -> FDataService: records + federation mappings activate FDataService FDataService -> FederationService: list of compose Records deactivate FDataService FederationService -> ComposeRS: list of records with appropriate fields activate ComposeRS ComposeRS -> Store: write records to compose storage activate Store Store -> ComposeRS: status deactivate Store ComposeRS -> FederationService: status deactivate ComposeRS FederationService -> Store: set sync status deactivate FederationService end box @enduml
@startuml skinparam responseMessageBelowArrow true actor "Destination Node" as NodeDestination box "Origin Node" #f7f7f7 participant "Federation Rest Controller" as FRestController participant "Federation Service" as FederationService participant "Compose Record Service" as ComposeRS database Store participant "Federation Data Service" as FDataService participant Encoder NodeDestination -> FRestController: get data note left : only the data\nthat was updated after\nlast successful sync activate FRestController FRestController -> FederationService: get all records for a specific module activate FederationService FederationService -> Store: get federation info on module and mapped fields activate Store Store -> FederationService: federation mappings deactivate Store FederationService -> ComposeRS: get filtered compose Records for Module activate ComposeRS ComposeRS -> Store: get filtered records activate Store Store -> ComposeRS: list of compose Records deactivate Store ComposeRS -> FederationService: list of compose Records deactivate ComposeRS FederationService -> FDataService: prepare the record list with specific fields note left : omit the fields we do not wish to share activate FDataService FDataService -> FederationService: list with specific fields deactivate FDataService FederationService -> Encoder: transform the list to specific structure activate Encoder Encoder -> FederationService: transformed list deactivate Encoder FederationService -> FRestController: transformed list of data deactivate FederationService FRestController -> NodeDestination: list of records deactivate FRestController end box @enduml
- Destination node requests the information about any data changes
-
Origin node provides a set of endpoints that the destination node can use to fetch newly created, updated, and deleted data. The destination node provides some additional filtering parameters; such as last sync timestamp; to exclude any unchanged data.
|
$TOKEN_B is the token that was generated during the handshake and is used to authenticate the user on the Origin node (the one who shares the data) by the Destination node. |
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node (?exposed) or the origin node (?shared)
$NODE_ID
# Federation module id
$MODULE_ID
# Node B auth token
$TOKEN_B
curl -X GET "$BASE_URL/federation/exposed/records?after=$AFTER_TIMESTAMP" \
-H "Authorization: Bearer $TOKEN_B";
{
"response": {
"filter": {
"query": "after=1600109447",
"page": 1,
"perPage": 20,
"count": 97,
"deleted": 0
},
"set": [
{
"type": "GET",
"rel": "Lead",
"href": "$BASE_URL/federation/exposed/records/$MODULE_ID?after=1600109447"
},
{
"type": "GET",
"rel": "Contact",
"href": "$BASE_URL/federation/exposed/records/$MODULE_ID?after=1600109447"
}
]
}
}
- Destination node requests changed data
-
The destination node fetches the data on per-module basis from the above provided set of endpoints.
|
$TOKEN_B is the token that was generated during the handshake and is used to authenticate the user on the Origin node (the one who shares the data) by the Destination node. |
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node (?exposed) or the origin node (?shared)
$NODE_ID
# Federation module id
$MODULE_ID
# Node B auth token
$TOKEN_B
curl -X GET "$BASE_URL/exposed/modules/$MODULE_ID/records?after=$AFTER_TIMESTAMP" \
-H "Authorization: Bearer $TOKEN_B";
{
"response": {
"filter": {
"moduleID": "132954639472525355",
"query": "",
"sort": "createdAt DESC",
"page": 1,
"perPage": 20,
"count": 97,
"deleted": 0
},
"set": [
{
"recordID": "161135411379307175",
"moduleID": "132954639472525355",
"values": [
{
"name": "name",
"value": "John"
},
{
"name": "surname",
"value": "Doe"
}
],
"createdAt": "2020-09-08T19:56:14Z",
"updatedAt": "2020-09-09T18:05:33Z"
},
{
"recordID": "161134990657061543",
"moduleID": "132954639472525355",
"values": [
{
"name": "name",
"value": "Walter"
},
{
"name": "surname",
"value": "White"
}
],
"createdAt": "2020-09-08T19:52:03Z",
"updatedAt": "2020-09-11T19:44:03Z"
}
]
}
}
- Origin node provides changed data for the requested structure
-
The origin node provides a set of changes based on base filter parameters such as timestamp, and requested structure to determine what data the destination node would like to receive. The filtered data, along with federated structure definitions are then passed into internal data manipulation systems to transform the data into the desired output, such as Activity Pub, and JSON (this also removes any fields that are not exposed by the origin node). The response also includes any additional filtering and pagination related parameters so the data can be fetched in chunks, or re-fetched if it any issues occurred.
- Destination node transforms the provided data into internal resource structures
-
The destination node uses data manipulation systems, along with module mapping definitions (see Field mapping) to transform the provided data set into internal resource structures. These are then used to update the destination node’s storage.
- Destination node updates the nodes status
-
The node’s status is updated to indicate when the last successful data sync has occurred.
API
Node pair
Create a federated node from a series of parameters
# Base URL of node A api
$API_A_BASE
# Main administrator JWT for node A
$MAIN_JWT_A
# Node A domain
$DOMAIN_A
# Node B domain
$DOMAIN_B
# Node name
$NODE_NAME
# Node A nodeID
$NODE_ID_A
# Node B nodeID
$NODE_ID_B
# Node URI
$NODE_URI
curl -X POST "$API_A_BASE/federation/nodes" \
-H "authorization: Bearer $MAIN_JWT_A" \
--header "Content-Type: application/json" \
--data "{
\"myDomain\": \"$DOMAIN_A\",
\"domain\": \"$DOMAIN_B\",
\"name\": \"$NODE_NAME\"
}";
{
"response": {
"nodeID": "$NODE_ID_A",
"sharedNodeID": "$NODE_ID_B",
"name": "\"$NODE_NAME\"",
"domain": "\"$DOMAIN_B\"",
"status": "\"pending\"",
"nodeURI": "\"$NODE_URI\""
}
}
Create a federated node from a node URI
# Base URL of node B api
$API_B_BASE
# Main administrator JWT for node B
$MAIN_JWT_B
# Node B domain
$DOMAIN_B
# Node A domain
$DOMAIN_A
# Node name
$NODE_NAME
# Node B nodeID
$NODE_ID_B
# Node A nodeID
$NODE_ID_A
# Node URI
$NODE_URI
curl -X POST "$API_B_BASE/federation/nodes" \
-H "authorization: Bearer $MAIN_JWT_B" \
--header "Content-Type: application/json" \
--data "{
\"myDomain\": \"$DOMAIN_B\",
\"nodeURI\": \"$NODE_URI\"
}";
{
"response": {
"nodeID": "$NODE_ID_B",
"sharedNodeID": "$NODE_ID_A",
"name": "\"$NODE_NAME\"",
"domain": "\"$DOMAIN_A\"",
"status": "\"pending\"",
"nodeURI": "\"$NODE_URI\""
}
}
Initialize the handshake
# Base URL of node B api
$API_B_BASE
# Main administrator JWT for node B
$MAIN_JWT_B
# Node B nodeID
$NODE_ID_B
curl -X POST "$API_B_BASE/federation/nodes/$NODE_ID_B/pair" \
-H "authorization: Bearer $MAIN_JWT_B" \
--header "Content-Type: application/json";
{}
Request the handshake with node A
# Base URL of node A api
$API_A_BASE
# Node A nodeID
$NODE_ID_A
# Node URI
$NODE_URI
# Node B auth token
$TOKEN_B
# Node B nodeID
$NODE_ID_B
curl -X POST "$API_A_BASE/federation/nodes/$NODE_ID_A/handshake" \
--header "Content-Type: application/json" \
--data "{
\"nodeURI\": \"$NODE_URI\",
\"token\": \"$TOKEN_B\",
\"nodeIDB\": \"$NODE_ID_B\"
}";
{}
Confirm the requested handshake
# Base URL of node A api
$API_A_BASE
# Node A nodeID
$NODE_ID_A
# Main administrator JWT for node A
$MAIN_JWT_A
curl -X POST "$API_A_BASE/federation/nodes/$NODE_ID_A/handshake-confirm" \
-H "authorization: Bearer $MAIN_JWT_A" \
--header "Content-Type: application/json";
{}
Complete the handshake
# Base URL of node B api
$API_B_BASE
# Node B nodeID
$NODE_ID_B
# Node B auth token
$TOKEN_B
# Node A auth token
$TOKEN_A
curl -X POST "$API_B_BASE/federation/nodes/$NODE_ID_B/handshake-complete" \
-H "authorization: Bearer $TOKEN_B" \
--header "Content-Type: application/json" \
--data "{
\"token\": \"$TOKEN_A\"
}";
{}
Origin structures
Add module to federation
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node
$NODE_ID
# Federation module id
$MODULE_ID
curl -X PUT "$BASE_URL/federation/nodes/$NODE_ID/modules/$MODULE_ID" \
-H "Authorization: Bearer $JWT"
-H "Content-Type: application/json" \
--data "[{
\"name\":\"LinkedIn\",
\"label\":\"LinkedIn Url\",
\"kind\":\"Url\",
\"is_multi\":0
}]";
{
"response": {
"moduleID": "122709113267335170",
"handle": "Account",
"name": "Account",
"createdAt": "2019-12-18T17:45:15Z",
"updatedAt": "2020-05-26T13:29:36Z",
"fields": [
{
"name": "LinkedIn",
"label": "LinkedIn Url",
"kind": "Url",
"is_multi": 0
}
]
}
}
Change sharing fields
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node
$NODE_ID
# Federation module id
$MODULE_ID
curl -X PUT "$BASE_URL/federation/nodes/$NODE_ID/modules/$MODULE_ID/exposed" \
-H "Authorization: Bearer $JWT"
-H "Content-Type: application/json" \
--data "[{
\"name\":\"LinkedIn\",
\"label\":\"LinkedIn\",
\"kind\":\"Url\",
\"is_multi\":1
}, {
\"name\":\"Phone\",
\"label\":\"Phone\",
\"kind\":\"String\",
\"is_multi\":0
}]";
{
"response": {
"moduleID": "122709113267335170",
"handle": "Account",
"name": "Account",
"createdAt": "2019-12-18T17:45:15Z",
"updatedAt": "2020-05-26T13:29:36Z",
"fields": [
{
"kind": "Url",
"name": "LinkedIn",
"label": "LinkedIn",
"isMulti": 0,
},
{
"kind": "String",
"name": "Phone",
"label": "Phone",
"isMulti": 0,
}
]
}
}
Info about exposed federated module
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node
$NODE_ID
curl -X GET "$BASE_URL/federation/nodes/$NODE_ID/modules/$MODULE_ID/exposed" \
-H "Authorization: Bearer $JWT";
{
"response": {
"moduleID": "122709113267335170",
"handle": "Account",
"name": "Account",
"createdAt": "2019-12-18T17:45:15Z",
"updatedAt": "2020-05-26T13:29:36Z",
"fields": [
{
"kind": "Url",
"name": "LinkedIn",
"label": "LinkedIn",
"isMulti": false,
},
{
"kind": "String",
"name": "Phone",
"label": "Phone",
"isMulti": false,
}
]
}
}
Remove module from federation
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node (?exposed) or the origin node (?shared)
$NODE_ID
# Federation module id
$MODULE_ID
curl -X DELETE "$BASE_URL/federation/nodes/$NODE_ID/modules/$MODULE_ID" \
-H "authorization: Bearer $JWT";
Destination structures
Info about shared federated module
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the origin node
$NODE_ID
curl -X GET "$BASE_URL/federation/nodes/$NODE_ID/modules/$MODULE_ID/shared" \
-H "Authorization: Bearer $JWT";
{
"response": {
"moduleID": "122709113267335170",
"handle": "Account",
"name": "Account",
"createdAt": "2019-12-18T17:45:15Z",
"updatedAt": "2020-05-26T13:29:36Z",
"fields": [
{
"kind": "Url",
"name": "LinkedIn",
"label": "LinkedIn",
"isMulti": false,
},
{
"kind": "String",
"name": "Phone",
"label": "Phone",
"isMulti": false,
}
]
}
}
List shared modules
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node (?exposed) or the origin node (?shared)
$NODE_ID
curl -X GET "$BASE_URL/federation/nodes/$NODE_ID/modules" \
-H "Authorization: Bearer $JWT";
{
"response": {
"filter": {
"query": "",
"handle": "",
"name": "",
"sort": "name ASC",
"count": 1
},
"set": [
{
"moduleID": "122709113267335170",
"handle": "Account",
"name": "Account",
"createdAt": "2019-12-18T17:45:15Z",
"updatedAt": "2020-05-26T13:29:36Z",
"fields": [
{
"kind": "Url",
"name": "LinkedIn",
"label": "LinkedIn",
"isMulti": false,
},
{
"kind": "String",
"name": "Phone",
"label": "Phone",
"isMulti": false,
}
]
}
]
}
}
Set module mapping for a module
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node
$NODE_ID
# Federation module id
$MODULE_ID
curl -X PUT "$BASE_URL/federation/nodes/$NODE_ID/modules/$MODULE_ID/mapped" \
-H "Authorization: Bearer $JWT"
-H "Content-Type: application/json" \
--data "[{
\"origin\":{
\"name\":\"LinkedIn\",
\"kind\":\"Url\",
\"is_multi\":0
},
\"destination\":{
\"name\":\"Social\",
\"kind\":\"String\",
\"is_multi\":0
}
}]";
{
"response": {
"moduleID": "122709113267335170",
"handle": "Account",
"name": "Account",
"createdAt": "2019-12-18T17:45:15Z",
"updatedAt": "2020-05-26T13:29:36Z",
"mapping": [
{
"origin": {
"name": "LinkedIn",
"kind": "Url",
"is_multi": 0
},
"destination": {
"name": "Social",
"kind": "String",
"is_multi": 0
}
}
]
}
}
Get module mapping for a module
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node
$NODE_ID
# Federation module id
$MODULE_ID
curl -X GET "$BASE_URL/federation/nodes/$NODE_ID/modules/$MODULE_ID/mapped" \
-H "Authorization: Bearer $JWT";
{
"response": {
"moduleID": "122709113267335170",
"handle": "Account",
"name": "Account",
"createdAt": "2019-12-18T17:45:15Z",
"updatedAt": "2020-05-26T13:29:36Z",
"mapping": [
{
"origin": {
"name": "LinkedIn",
"kind": "Url",
"is_multi": 0
},
"destination": {
"name": "Social",
"kind": "String",
"is_multi": 0
}
}
]
}
}
Remove module mapping from federation
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node (?exposed) or the origin node (?shared)
$NODE_ID
# Federation module id
$MODULE_ID
curl -X DELETE "$BASE_URL/federation/nodes/$NODE_ID/modules/$MODULE_ID" \
-H "authorization: Bearer $JWT";
Structure sync
Get master changes
| $TOKEN_B is the token that was generated during the handshake and is used to authenticate the user on the Origin node (the one who shares the data) by the Destination node. |
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node
$NODE_ID
# Node B auth token
$TOKEN_B
curl -X GET "$BASE_URL/federation/exposed/modules" \
-H "Authorization: Bearer $TOKEN_B";
{
"response": {
"filter": {
"query": "after=1600109447",
"page": 1,
"perPage": 20,
"count": 97,
},
"set": [
{
"type": "GET",
"rel": "Account",
"href": "$BASE_URL/federation/exposed/modules/$MODULE_ID?after=1600109447"
},
{
"type": "GET",
"rel": "Contact",
"href": "$BASE_URL/federation/exposed/modules/$MODULE_ID?after=1600109447"
}
]
}
}
Sync shared module structure
| $TOKEN_B is the token that was generated during the handshake and is used to authenticate the user on the Origin node (the one who shares the data) by the Destination node. |
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the origin node
$NODE_ID
# Node B auth token
$TOKEN_B
curl -X GET "$BASE_URL/federation/exposed/modules/$MODULE_ID" \
-H "Authorization: Bearer $TOKEN_B";
{
"response": {
"moduleID": "122709113267335170",
"handle": "Account",
"name": "Account",
"createdAt": "2019-12-18T17:45:15Z",
"updatedAt": "2020-05-26T13:29:36Z",
"fields": [
{
"kind": "Url",
"name": "LinkedIn",
"label": "LinkedIn",
"isMulti": false,
},
{
"kind": "String",
"name": "Phone",
"label": "Phone",
"isMulti": false,
}
]
}
}
Fetch exposed data changes
|
$TOKEN_B is the token that was generated during the handshake and is used to authenticate the user on the Origin node (the one who shares the data) by the Destination node. |
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node (?exposed) or the origin node (?shared)
$NODE_ID
# Federation module id
$MODULE_ID
# Node B auth token
$TOKEN_B
curl -X GET "$BASE_URL/federation/exposed/records?after=$AFTER_TIMESTAMP" \
-H "Authorization: Bearer $TOKEN_B";
{
"response": {
"filter": {
"query": "after=1600109447",
"page": 1,
"perPage": 20,
"count": 97,
"deleted": 0
},
"set": [
{
"type": "GET",
"rel": "Lead",
"href": "$BASE_URL/federation/exposed/records/$MODULE_ID?after=1600109447"
},
{
"type": "GET",
"rel": "Contact",
"href": "$BASE_URL/federation/exposed/records/$MODULE_ID?after=1600109447"
}
]
}
}
Fetch exposed data
|
$TOKEN_B is the token that was generated during the handshake and is used to authenticate the user on the Origin node (the one who shares the data) by the Destination node. |
# Base url for the federation api
$BASE_URL
# JWT of the user
$JWT
# Node id of the destination node (?exposed) or the origin node (?shared)
$NODE_ID
# Federation module id
$MODULE_ID
# Node B auth token
$TOKEN_B
curl -X GET "$BASE_URL/exposed/modules/$MODULE_ID/records?after=$AFTER_TIMESTAMP" \
-H "Authorization: Bearer $TOKEN_B";
{
"response": {
"filter": {
"moduleID": "132954639472525355",
"query": "",
"sort": "createdAt DESC",
"page": 1,
"perPage": 20,
"count": 97,
"deleted": 0
},
"set": [
{
"recordID": "161135411379307175",
"moduleID": "132954639472525355",
"values": [
{
"name": "name",
"value": "John"
},
{
"name": "surname",
"value": "Doe"
}
],
"createdAt": "2020-09-08T19:56:14Z",
"updatedAt": "2020-09-09T18:05:33Z"
},
{
"recordID": "161134990657061543",
"moduleID": "132954639472525355",
"values": [
{
"name": "name",
"value": "Walter"
},
{
"name": "surname",
"value": "White"
}
],
"createdAt": "2020-09-08T19:52:03Z",
"updatedAt": "2020-09-11T19:44:03Z"
}
]
}
}
Store
Federation node
| Column | Type | Description |
|---|---|---|
ID |
uint64 |
Node ID |
status |
string |
Node status |
structure_status |
string |
Structure sync status |
structure_synced_at |
Timestamp |
Last structure sync |
data_status |
string |
Data sync status |
data_synced_at |
Timestamp |
Last data sync |
Exposed federation module
| Column | Type | Description |
|---|---|---|
ID |
uint64 |
federation module id |
rel_node |
uint64 |
node id (destination node id - who are we sharing to) |
rel_compose_module |
uint64 |
module id on source node |
fields |
json |
list of fields |
Shared federation module
| Column | Type | Description |
|---|---|---|
ID |
uint64 |
federation module id |
handle |
varchar(200) |
Module handle |
name |
varchar(64) |
Module name |
rel_node |
uint64 |
node id (source node id - who is sharing with us) |
xref_module |
uint64 |
federation module id on source node (id in federation_module_exposed) |
fields |
json |
list of fields |
Federation module mapping
| Column | Type | Description |
|---|---|---|
federation_module_id |
uint64 |
id from federation_module_exposed |
compose_module_id |
uint64 |
existing module |
field_mapping |
json |
json field mappings, ex: [{ source: 'node_A_module_field_7', dest: 'node_B_module_field_2', transform: 'string' }] |