Sharing data between many data owners
In some applications, you have groups of collaborating doctors each sharing the data of their patients with all their colleagues.
A basic implementation for this scenario could have each doctor sharing each patient and piece of medical data they create with all their colleagues. However, every time a piece of data is shared between two users, the Cardinal SDK adds some metadata to the shared entity. As long as there are few doctors involved in these collaborating groups, or as long as only a few documents require full sharing between all the doctors, the space overhead used by the sharing metadata should be negligible. In other cases, however, the overhead may start to become significant.
An alternative solution to cover these cases would be instead to use hierarchical data owners.
Hierarchical data owners​
Healthcare parties can be organized hierarchically using a tree structure, where each healthcare party can have a parent. You can use these hierarchies to group the healthcare parties. For example, you can have an HCP representing the hospital at the root. Then you have HCPs representing different departments such as cardiology or radiology as children to the hospital HCP. Finally, you have the actual doctors that are children to the HCP representing their department.
This hierarchy could be used to represent the organization of a hospital, but this is not its main purpose. Instead, the main purpose of healthcare party hierarchies is to provide more flexibility in the entity sharing processes and access control when working in complex scenarios. Normally, healthcare parties have access only to the data that was created by or directly shared with them. If you enable hierarchical data owners for your application, however, the healthcare parties will also gain access to all data that was created by or shared with any of their ancestors.
This allows more easily sharing data with large groups of doctors: if a doctor needs to share data with its entire department instead of sharing it with each other doctor in the department, they only need to share it with the HCP for their department.
Currently, only healthcare parties can be part of data owner hierarchies.
However, the general concepts for data owner hierarchies could apply also to other types of data owners if implemented in the future.
Set up hierarchical data owners​
Setting up hierarchical data owners for your application, you need to:
- Configure the hierarchy in the data owners entity
- Assign the hierarchical data owner roles to the affected users
- Configure the SDK initialization to use the hierarchical data owners
- Have key pairs for the parent data owners and distribute them to the children
Configuring the data owners hierarchy​
You will have to decide a hierarchy that is suitable for your use case and apply it to any existing healthcare party,
using a user with the appropriate permissions.
When working with the default roles only admins and users with the HCP_USER_MANAGER role can do this.
You will also have to make sure that you properly set the parent id when creating the new users.
Assigning roles​
The HCPs need special permissions to be allowed to retrieve data shared with their parents.
When using default roles, you can provide these permissions through the HIERARCHICAL_DATA_OWNER role.
We recommend that you use the cockpit to configure the default roles for healthcare parties in your group to include the
HIERARCHICAL_DATA_OWNER role (🚧).
Configure the SDK​
When initializing the SDK you will have to say that you want to use hierarchical data owners, to have it load and use all the keypairs for parent data owners.
This is done by setting useHierarchicalDataOwners to true in the SdkOptions:
- Kotlin
- Typescript
- Python
import com.icure.cardinal.sdk.CardinalSdk
import com.icure.cardinal.sdk.auth.UsernamePassword
import com.icure.cardinal.sdk.options.AuthenticationMethod
import com.icure.cardinal.sdk.options.EncryptedFieldsConfiguration
import com.icure.cardinal.sdk.options.SdkOptions
import com.icure.cardinal.sdk.storage.impl.FileStorageFacade
suspend fun initializeMySdk(username: String, password: String) =
CardinalSdk.initialize(
"com.mycompany.mycardinalapp",
"https://api.icure.cloud",
AuthenticationMethod.UsingCredentials(UsernamePassword(username, password)),
FileStorageFacade("/path/to/storage/directory"),
SdkOptions(
useHierarchicalDataOwners = true
)
)
import {AuthenticationMethod, CardinalSdk, StorageFacade} from "@icure/cardinal-sdk";
function initializeMySdk(username: string, password: string): Promise<CardinalSdk> {
return CardinalSdk.initialize(
"com.mycompany.mycardinalapp",
"https://api.icure.cloud",
new AuthenticationMethod.UsingCredentials.UsernamePassword(username, password),
StorageFacade.usingFileSystem("/path/to/storage/directory"),
{
useHierarchicalDataOwners: true
}
)
}
from cardinal_sdk import CardinalSdk
from cardinal_sdk.authentication import UsernamePassword
from cardinal_sdk.storage import FileSystemStorage
from cardinal_sdk.options import SdkOptions, EncryptedFieldsConfiguration
sdk = CardinalSdk(
"com.mycompany.mycardinalapp",
"https://api.icure.cloud",
UsernamePassword("username", "password"),
FileSystemStorage("/path/to/storage/directory"),
SdkOptions(
use_hierarchical_data_owners=True
)
)
Configure keypairs for the parent data owners​
Healthcare parties can't create keypairs for their parents: the initial keypair of a parent data owner has to be created by an administrator of the organization.
Once the parent has a keypair, however, a child data owner whose SDK already has access to it can distribute it to the
other children of the same parent, without any further intervention of the administrator.
Since SDK 2.3.0, this is done using the createRecoveryInfoForAvailableParentKeyPairs method of the recovery API.
This method works like createRecoveryInfoForAvailableKeyPairs (see
keys backup representation): it stores the keypairs of the parent
encrypted on the Cardinal backend and returns the recovery key that decrypts them.
The difference is that the recovery data is created for the parent data owner instead of the current data owner, so
any other child of parentId can use the recovery key to get the parent keypairs.
- Only the parent keypairs that are available to the SDK of the current user and that are verified (or created on the
current device) are included in the recovery data. The SDK must be initialized with hierarchical data owners, and
parentIdmust be a (direct or indirect) parent of the current data owner, otherwise the method fails with anIllegalArgumentException. The method also fails if none of the available keypairs can be included. - If
includeAncestorKeysistrue, the recovery data also includes the available keypairs of the parents ofparentId. - As for any recovery data, you can limit its lifetime with
lifetimeSecondsand choose the size of the recovery key (or provide a pre-generated key) withrecoveryKeyOptions. - The user creating the recovery data needs the
RecoveryDataManagement.ExtendedCreate.ForParentpermission (or a stronger one), and the users that will use the recovery key need theRecoveryDataManagement.ExtendedRead.ForParentpermission.
The recovery key gives access to the private keys of the parent data owner: it must be kept secret, and shared only with other children of the same parent.
- Kotlin
- Typescript
- Python
import com.icure.cardinal.sdk.CardinalSdk
import com.icure.cardinal.sdk.crypto.entities.RecoveryKeyOptions
import com.icure.cardinal.sdk.crypto.entities.RecoveryKeySize
suspend fun createParentKeysRecoveryKey(
sdk: CardinalSdk,
parentId: String
): String {
val recoveryKey = sdk.recovery.createRecoveryInfoForAvailableParentKeyPairs(
parentId = parentId,
// The recovery data is deleted automatically after 1 week
lifetimeSeconds = 7 * 24 * 60 * 60,
recoveryKeyOptions = RecoveryKeyOptions.Generate(RecoveryKeySize.Bytes32),
)
// Share this value only with other children of the parent
return recoveryKey.asBase32()
}
import {
CardinalSdk,
RecoveryKeyOptions,
RecoveryKeySize
} from "@icure/cardinal-sdk";
async function createParentKeysRecoveryKey(sdk: CardinalSdk, parentId: string): Promise<string> {
const recoveryKey = await sdk.recovery.createRecoveryInfoForAvailableParentKeyPairs(parentId, {
// The recovery data is deleted automatically after 1 week
lifetimeSeconds: 7 * 24 * 60 * 60,
recoveryKeyOptions: new RecoveryKeyOptions.Generate({
recoveryKeySize: RecoveryKeySize.Bytes32,
}),
})
// Share this value only with other children of the parent
return recoveryKey.asBase32()
}
from cardinal_sdk import CardinalSdk
from cardinal_sdk.model import RecoveryKeyOptionsGenerate, RecoveryKeySize
def create_parent_keys_recovery_key(sdk: CardinalSdk, parent_id: str) -> str:
recovery_key = sdk.recovery.create_recovery_info_for_available_parent_key_pairs_blocking(
parent_id=parent_id,
# The recovery data is deleted automatically after 1 week
lifetime_seconds=7 * 24 * 60 * 60,
recovery_key_options=RecoveryKeyOptionsGenerate(RecoveryKeySize.Bytes32),
)
# Share this value only with other children of the parent
return recovery_key.as_base_32()
The other children of the parent use the recovery key in the same way as a recovery key for their own keys: when their
SDK is initialized with hierarchical data owners and the keys of the parent are missing, the SDK asks the crypto
strategies to recover them, and you can pass the recovery key to keyPairRecoverer.recoverWithRecoveryKey, as shown in
keys backup representation.
The recovered keypairs are then stored by the SDK like any other recovered key.
If multiple children have to use the same recovery key, make sure to pass autoDelete = false to
recoverWithRecoveryKey, otherwise the recovery data is deleted after the first successful use.
In this case, you should give the recovery data a limited lifetime or delete it with purgeRecoveryInfo once it is no
longer needed.
Alternatively, you can create a different recovery key for each child.
Sharing data with parents and siblings​
Sharing data with other healthcare parties in a hierarchical environment is done using in the same way as in any other environment. The only thing you should consider is that when a user is sharing data with another data owner, they're implicitly sharing data also with all the children of that data owner.
Therefore, if a doctor wants to share data with all other doctors in the hospital they can share the data with the hospital HCP. If, instead, the doctor only wants to share data with a specific colleague they will have to share the data directly only with that colleague.