Collection Sharing
note
In the future we will have more ways to share collections among users (e.g: PAKE), but until we do, the only way to share is using public-key cryptography which is what this section is about.
This section assumes you have already read the collections section and are familiar with collections.
Collections can be shared among users for easy collaboration. This can be used for sharing files between users, editing a shared document, or giving others access to parts of your account (e.g. a calendar).
Getting your public key
An important part of using public key cryptography is making sure the other side of the interaction is whoever you think it is. This can be achieved by verifying each-other's public key. Your public key, as the name implies, is public, so feel free to share it with others.
Getting your public key:
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
const etebase = await Etebase.Account.login("username", "password");
const invitationManager = etebase.getInvitationManager();
// You can now share your pubkey
const myPubkey = invitationManager.pubkey;
etebase = Account.login(...)
invitation_manager = etebase.get_invitation_manager()
# You can now share your pubkey
invitation_manager.pubkey
Account etebase = Account.login(...);
CollectionInvitationManager invitationManager = etebase.getInvitationManager();
// You can now share your pubkey
byte[] myPubkey = invitationManager.getPubkey();
val etebase = Account.login(...)
val invitationManager = etebase.invitationManager
// You can now share your pubkey
val myPubkey = invitationManager.pubkey
EtebaseAccount *etebase = etebase_account_login(...);
EtebaseCollectionInvitationManager *invitation_manager = etebase_account_get_invitation_manager(etebase);
// You can now share your pubkey
const char *my_pubkey = etebase_invitation_manager_get_pubkey(invitation_manager);
uintptr_t pubkey_size = etebase_invitation_manager_get_pubkey_size(invitation_manager);
etebase_invitation_manager_destroy(invitation_manager);
let etebase = Account::login(...)?;
let invitation_manager = etebase.invitation_manager()?;
// You can now share your pubkey
let my_pubkey = invitation_manager.pubkey();
Pretty-print the public key
You would almost always want users to compare public keys in a secure manner (usually phone, in-person, or secure chat). The following function formats the pubkey for you in a way that's easy for users to compare, so you don't have to figure it out yourself.
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
// The delimiter parameter is optional and defaults to " "
const delimiter = " ";
const prettyFingerprint = Etebase.getPrettyFingerprint(myPubkey, delimiter);
console.log(prettyFingerprint);
/* Output:
45680 71497 88570 93128
19189 84243 25687 20837
47924 46071 54113 18789
*/
from etebase import pretty_fingerprint
fingerprint = pretty_fingerprint(my_pubkey)
print(fingerprint)
# Output:
# 45680 71497 88570 93128
# 19189 84243 25687 20837
# 47924 46071 54113 18789
String prettyFingerprint = Utils.getPrettyFingerprint(myPubkey);
/* prettyFingerprint:
45680 71497 88570 93128
19189 84243 25687 20837
47924 46071 54113 18789
*/
val prettyFingerprint = Utils.getPrettyFingerprint(myPubkey)
/* prettyFingerprint:
45680 71497 88570 93128
19189 84243 25687 20837
47924 46071 54113 18789
*/
char pretty[ETEBASE_UTILS_PRETTY_FINGERPRINT_SIZE];
etebase_utils_pretty_fingerprint(my_pubkey, my_pubkey_size, pretty);
/* prettyFingerprint:
45680 71497 88570 93128
19189 84243 25687 20837
47924 46071 54113 18789
*/
let pretty_fingerprint = utils::pretty_fingerprint(my_pubkey);
/* pretty_fingerprint:
45680 71497 88570 93128
19189 84243 25687 20837
47924 46071 54113 18789
*/
The format we use is similar to the one used by Signal. Please refer to their post for more information about it.
Sending invitations
The first step towards sharing collections with other users is sending them an invitation. Only a collection admin can send an invitation, so make sure you are one before trying to invite users.
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
const invitationManager = etebase.getInvitationManager();
// Fetch their public key
const user2 = await invitationManager.fetchUserProfile("username2");
// Verify user2.pubkey is indeed the pubkey you expect.
// This is done in a secure channel (e.g. encrypted chat or in person)
// Assuming the pubkey is as expected, send the invitation
await invitationManager.invite(collection, "username2", user2.pubkey,
Etebase.CollectionAccessLevel.ReadOnly);
invitation_manager = etebase.get_invitation_manager()
# Fetch their public key
user2 = invitation_manager.fetch_user_profile("username2");
# Verify user2.pubkey is indeed the pubkey you expect.
# This is done in a secure channel (e.g. encrypted chat or in person)
# Assuming the pubkey is as expected, send the invitation
invitation_manager.invite(collection, "username2", user2.pubkey, CollectionAccessLevel.ReadOnly)
CollectionInvitationManager invitationManager = etebase.getInvitationManager();
// Fetch their public key
UserProfile user2 = invitationManager.fetchUserProfile("username2");
// Verify user2.pubkey is indeed the pubkey you expect.
// This is done in a secure channel (e.g. encrypted chat or in person)
// Assuming the pubkey is as expected, send the invitation
invitationManager.invite(collection, "username2", user2.getPubkey(), CollectionAccessLevel.ReadOnly);
val invitationManager = etebase.invitationManager
// Fetch their public key
val user2 = invitationManager.fetchUserProfile("username2")
// Verify user2.pubkey is indeed the pubkey you expect.
// This is done in a secure channel (e.g. encrypted chat or in person)
// Assuming the pubkey is as expected, send the invitation
invitationManager.invite(collection, "username2", user2.pubkey, CollectionAccessLevel.ReadOnly)
EtebaseCollectionInvitationManager *invitation_manager = etebase_account_get_invitation_manager(etebase);
// Fetch their public key
EtebaseUserProfile *user2 = etebase_invitation_manager_fetch_user_profile(invitation_manager, "username2");
// Verify user2.pubkey is indeed the pubkey you expect.
// This is done in a secure channel (e.g. encrypted chat or in person)
// Assuming the pubkey is as expected, send the invitation
etebase_invitation_manager_invite(invitation_manager, col, "username2",
etebase_user_profile_get_pubkey(user2), etebase_user_profile_get_pubkey_size(user2),
ETEBASE_COLLECTION_ACCESS_LEVEL_READ_ONLY);
etebase_user_profile_destroy(user2);
etebase_invitation_manager_destroy(invitation_manager);
let invitation_manager = etebase.invitation_manager()?;
// Fetch their public key
let user2 = invitation_manager.fetch_user_profile("username2")?;
// Verify user2.pubkey is indeed the pubkey you expect.
// This is done in a secure channel (e.g. encrypted chat or in person)
// Assuming the pubkey is as expected, send the invitation
invitation_manager.invite(collection, "username2", user2.pubkey(), CollectionAccessLevel::ReadOnly)?;
As you can see from the example above, when inviting a user you also set the wanted access level. Allowed values are: Admin
, ReadWrite
, and ReadOnly
.
Responding to invitations
In the previous example we sent an access invitation to user2
. In this section we will see how to respond to these invitations.
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
// login as user2
const etebase = await Etebase.Account.login("username2", "password");
const invitationManager = etebase.getInvitationManager();
// List pending invitations
const invitations = await invitationManager.listIncoming();
const invitation = invitations.data[0]; // get first invitation
// Verify the sender's pubkey is indeed the pubkey you expect.
// This is done in a secure channel (e.g. encrypted chat or in person)
const fromPubkey = invitation.fromPubkey;
// We can now either accept
await invitationManager.accept(invitation);
// or reject the invitation:
await invitationManager.reject(invitation);
# login as user2
etebase = Account.login(client, "username2", "password")
invitation_manager = etebase.get_invitation_manager()
# List pending invitations
invitations = invitation_manager.list_incoming()
invitation = list(invitations.data)[0] # get first invitation
# Verify the sender's pubkey is indeed the pubkey you expect.
# This is done in a secure channel (e.g. encrypted chat or in person)
from_pubkey = invitation.from_pubkey
const fromPubkey = invitation.fromPubkey;
# We can now either accept
invitation_manager.accept(invitation)
# or reject the invitation:
invitation_manager.reject(invitation)
// login as user2
Account etebase = Account.login("username2", "password");
CollectionInvitationManager invitationManager = etebase.getInvitationManager();
// List pending invitations
InvitationListResponse invitations = invitationManager.listIncoming();
SignedInvitation invitation = invitations.getData()[0]; // get first invitation
// Verify the sender's pubkey is indeed the pubkey you expect.
// This is done in a secure channel (e.g. encrypted chat or in person)
byte[] fromPubkey = invitation.getFromPubkey();
// We can now either accept
invitationManager.accept(invitation);
// or reject the invitation:
invitationManager.reject(invitation);
// login as user2
val etebase = Account.login("username2", "password")
val invitationManager = etebase.invitationManager
// List pending invitations
val invitations = invitationManager.listIncoming()
val invitation = invitations.data[0] // get first invitation
// Verify the sender's pubkey is indeed the pubkey you expect.
// This is done in a secure channel (e.g. encrypted chat or in person)
val fromPubkey = invitation.fromPubkey
// We can now either accept
invitationManager.accept(invitation)
// or reject the invitation:
invitationManager.reject(invitation)
// login as user2
EtebaseAccount *etebase = etebase_account_login(client, "username2", "password");
EtebaseCollectionInvitationManager *invitation_manager = etebase_account_get_invitation_manager(etebase);
// List pending invitations
EtebaseInvitationListResponse *invitations = etebase_invitation_manager_list_incoming(invitation_manager, NULL);
uintptr_t data_len = etebase_invitation_list_response_get_data_length(invitations);
const EtebaseSignedInvitation *data[data_len];
etebase_invitation_list_response_get_data(invitations, data);
const EtebaseSignedInvitation *invitation = data[0];
// Verify the sender's pubkey is indeed the pubkey you expect.
// This is done in a secure channel (e.g. encrypted chat or in person)
const void *from_pubkey = etebase_signed_invitation_get_from_pubkey(invitation);
// We can now either accept
etebase_invitation_manager_accept(invitation_manager, invitation);
// or reject the invitation:
etebase_invitation_manager_reject(invitation_manager, invitation);
etebase_invitation_list_response_destroy(invitations);
etebase_invitation_manager_destroy(invitation_manager);
// login as user2
let etebase = Account::login(client, "username2", "password")?;
let invitation_manager = etebase.invitation_manager()?;
// List pending invitations
let invitations = invitation_manager.list_incoming(None)?;
let invitation = &invitations.data()[0]; // get first invitation
// Verify the sender's pubkey is indeed the pubkey you expect.
// This is done in a secure channel (e.g. encrypted chat or in person)
let from_pubkey = invitation.from_pubkey();
// We can now either accept
invitation_manager.accept(&invitation)?;
// or reject the invitation:
invitation_manager.reject(&invitation)?;
Leaving collections
Sometimes a user may want to leave a collection he has been invited to. This can be done as follows:
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
const memberManager = collectionManager.getMemberManager(collection);
await memberManager.leave();
member_manager = col_mgr.get_member_manager(collection)
member_manager.leave()
CollectionMemberManager memberManager = colMgr.getMemberManager(collection);
memberManager.leave();
val memberManager = colMgr.getMemberManager(collection)
memberManager.leave()
EtebaseCollectionMemberManager *member_manager = etebase_collection_manager_get_member_manager(col_mgr, col);
etebase_collection_member_manager_leave(member_manager);
etebase_collection_member_manager_destroy(member_manager);
let member_manager = collection_manager.member_manager(collection)?;
member_manager.leave()?;
Controlling access
Like with invitations, only collection admins can see who has access to a collection, modify access levels and revoke access altogether.
Listing collection members
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
const memberManager = collectionManager.getMemberManager(collection);
const members = await memberManager.list();
// Print the users and their access levels
for (const member of members.data) {
console.log(member.username, member.accessLevel);
}
member_manager = col_mgr.get_member_manager(collection)
members = member_manager.list()
# Print the users and their access levels
for member in members.data:
print(member.username, member.access_level)
CollectionMemberManager memberManager = colMgr.getMemberManager(collection);
MemberListResponse members = memberManager.list();
// Print the users and their access levels
for (CollectionMember member: members.getData()) {
System.out.println(member.getUsername(), member.getAccessLevel());
}
val memberManager = colMgr.getMemberManager(collection)
val members = memberManager.list()
// Print the users and their access levels
for (member in members.data) {
System.out.println(member.username, member.accessLevel)
}
EtebaseCollectionMemberManager *member_manager = etebase_collection_manager_get_member_manager(col_mgr, col);
EtebaseMemberListResponse *members = etebase_collection_member_manager_list(member_manager, NULL);
uintptr_t data_len = etebase_member_list_response_get_data_length(members);
const EtebaseCollectionMember *data[data_len];
etebase_member_list_response_get_data(members, data);
// Print the users and their access levels
for (int i = 0 ; i < data_len ; i++) {
const EtebaseCollectionMember *member = data[i];
printf("%s\n", etebase_collection_member_get_username(member));
}
etebase_member_list_response_destroy(members);
etebase_collection_member_manager_destroy(member_manager);
let member_manager = collection_manager.member_manager(collection)?;
let members = member_manager.list(None)?;
// Print the users and their access levels
for member in members.data() {
println!("{} {:?}", member.username(), member.access_level());
}
Modifying access level
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
const memberManager = collectionManager.getMemberManager(collection);
const newAccessLevel = Etebase.CollectionAccessLevel.ReadWrite;
await memberManager.modifyAccessLevel("username2", newAccessLevel);
member_manager = col_mgr.get_member_manager(collection)
new_access_level = CollectionAccessLevel.ReadWrite
member_manager.modify_access_level("username2", new_access_level)
CollectionMemberManager memberManager = colMgr.getMemberManager(collection);
CollectionAccessLevel newAccessLevel = CollectionAccessLevel.ReadWrite;
memberManager.modifyAccessLevel("username2", newAccessLevel);
val memberManager = colMgr.getMemberManager(collection)
val newAccessLevel = CollectionAccessLevel.ReadWrite
memberManager.modifyAccessLevel("username2", newAccessLevel)
EtebaseCollectionMemberManager *member_manager = etebase_collection_manager_get_member_manager(col_mgr, col);
etebase_collection_member_manager_modify_access_level(member_manager, "username2", ETEBASE_COLLECTION_ACCESS_LEVEL_READ_WRITE);
etebase_collection_member_manager_destroy(member_manager);
let member_manager = collection_manager.member_manager(collection)?;
let new_access_level = CollectionAccessLevel::ReadWrite;
member_manager.modify_access_level("username2", new_access_level)?;
Revoking access
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
const memberManager = collectionManager.getMemberManager(collection);
await memberManager.remove("username2");
member_manager = col_mgr.get_member_manager(collection)
member_manager.remove("username2")
CollectionMemberManager memberManager = colMgr.getMemberManager(collection);
memberManager.remove("username2");
val memberManager = colMgr.getMemberManager(collection)
memberManager.remove("username2")
EtebaseCollectionMemberManager *member_manager = etebase_collection_manager_get_member_manager(col_mgr, col);
etebase_collection_member_manager_remove(member_manager, "username2");
etebase_collection_member_manager_destroy(member_manager);
let member_manager = collection_manager.member_manager(collection)?;
member_manager.remove("username2")?;
Iterating through responses
Like with the rest of the Etebase API, list responses can be iterated through in smaller chunks. Here is how it's done for outgoing invitations, but the same can be done for incoming invitations and collection members.
- JavaScript
- Python
- Java
- Kotlin
- C/C++
- Rust
const invitationManager = etebase.getInvitationManager();
let iterator = null;
while (true) {
const items = await invitationManager.listOutgoing({ iterator, limit: 30 });
iterator = items.iterator;
processOutgoingInvitations(items.data);
if (items.done) {
break;
}
}
invitation_manager = etebase.get_invitation_manager()
iterator = None
done = False
while not done:
fetch_options = FetchOptions().iterator(iterator).limit(30)
invitations = invitation_manager.list_outgoing(fetch_options)
iterator = invitations.iterator
done = invitations.done
process_outgoing_invitations(invitations.data)
String iterator = null;
boolean done = false;
while (!done) {
InvitationListResponse invitations = itemManager.listOutgoing(new FetchOptions().iterator(iterator).limit(30));
iterator = invitations.getIterator();
done = invitations.isDone();
processOutgoingInvitations(invitations.getData());
}
var iterator: String? = null
var done = false
while (!done) {
val invitations = itemManager.listOutgoing(item, FetchOptions().iterator(iterator).limit(30))
iterator = invitations.iterator
done = invitations.isDone
processOutgoingInvitations(invitations.data)
}
char *iterator = NULL;
bool done = 0;
while (!done) {
EtebaseFetchOptions *fetch_options = etebase_fetch_options_new();
etebase_fetch_options_set_iterator(fetch_options, iterator);
etebase_fetch_options_set_limit(fetch_options, 30);
EtebaseInvitationListResponse *invitations = etebase_invitation_manager_list_outgoing(invitation_manager, fetch_options);
if (iterator) {
free(iterator);
}
iterator = strdup(etebase_invitation_list_response_get_iterator(invitations));
done = etebase_invitation_list_response_is_done(invitations);
uintptr_t data_len =
etebase_invitation_list_response_get_data_length(invitations);
const EtebaseSignedInvitation *data[data_len];
etebase_invitation_list_response_get_data(invitations, data);
process_revisions(data, data_len);
etebase_fetch_options_destroy(fetch_options);
etebase_invitation_list_response_destroy(invitations);
}
if (iterator) {
free(iterator);
}
let mut iterator: Option<&str> = None;
let mut invitations: IteratorListResponse<SignedInvitation>;
loop {
invitations = invitation_manager.list_outgoing(
Some(&FetchOptions::new().iterator(iterator).limit(30))
)?;
iterator = invitations.iterator();
process_outgoing_invitations(invitations.data());
if invitations.done() {
break
}
}