async def test_create_certif_too_old(alice, alice_backend_sock): now = pendulum.now() # Generate a certificate realm_id = RealmID.from_hex("C0000000000000000000000000000000") certif = RealmRoleCertificateContent.build_realm_root_certif( author=alice.device_id, timestamp=now, realm_id=realm_id).dump_and_sign(alice.signing_key) # Create a realm a tiny bit too late later = now.add(seconds=BALLPARK_CLIENT_LATE_OFFSET) with freeze_time(later): rep = await realm_create(alice_backend_sock, certif) assert rep == { "status": "bad_timestamp", "backend_timestamp": later, "ballpark_client_early_offset": BALLPARK_CLIENT_EARLY_OFFSET, "ballpark_client_late_offset": BALLPARK_CLIENT_LATE_OFFSET, "client_timestamp": now, } # Create a realm late but right before the deadline later = now.add(seconds=BALLPARK_CLIENT_LATE_OFFSET, microseconds=-1) with freeze_time(later): rep = await realm_create(alice_backend_sock, certif) assert rep["status"] == "ok" # Generate a new certificate realm_id = RealmID.from_hex("C0000000000000000000000000000001") certif = RealmRoleCertificateContent.build_realm_root_certif( author=alice.device_id, timestamp=now, realm_id=realm_id).dump_and_sign(alice.signing_key) # Create a realm a tiny bit too soon sooner = now.subtract(seconds=BALLPARK_CLIENT_EARLY_OFFSET) with freeze_time(sooner): rep = await realm_create(alice_backend_sock, certif) assert rep == { "status": "bad_timestamp", "backend_timestamp": sooner, "ballpark_client_early_offset": BALLPARK_CLIENT_EARLY_OFFSET, "ballpark_client_late_offset": BALLPARK_CLIENT_LATE_OFFSET, "client_timestamp": now, } # Create a realm soon but after the limit sooner = now.subtract(seconds=BALLPARK_CLIENT_EARLY_OFFSET, microseconds=-1) with freeze_time(sooner): rep = await realm_create(alice_backend_sock, certif) assert rep["status"] == "ok"
async def test_remove_role_idempotent( alice, bob, alice_backend_sock, realm, start_with_existing_role, realm_generate_certif_and_update_roles_or_fail, ): if start_with_existing_role: with freeze_time("2000-01-03"): rep = await realm_generate_certif_and_update_roles_or_fail( alice_backend_sock, alice, realm, bob.user_id, RealmRole.MANAGER) assert rep == {"status": "ok"} with freeze_time("2000-01-04"): rep = await realm_generate_certif_and_update_roles_or_fail( alice_backend_sock, alice, realm, bob.user_id, None) if start_with_existing_role: assert rep == {"status": "ok"} else: assert rep == {"status": "already_granted"} with freeze_time("2000-01-05"): rep = await realm_generate_certif_and_update_roles_or_fail( alice_backend_sock, alice, realm, bob.user_id, None) assert rep == {"status": "already_granted"} certifs = await _realm_get_clear_role_certifs(alice_backend_sock, realm) expected_certifs = [ RealmRoleCertificateContent( author=alice.device_id, timestamp=datetime(2000, 1, 2), realm_id=realm, user_id=alice.user_id, role=RealmRole.OWNER, ) ] if start_with_existing_role: expected_certifs += [ RealmRoleCertificateContent( author=alice.device_id, timestamp=datetime(2000, 1, 3), realm_id=realm, user_id=bob.user_id, role=RealmRole.MANAGER, ), RealmRoleCertificateContent( author=alice.device_id, timestamp=datetime(2000, 1, 4), realm_id=realm, user_id=bob.user_id, role=None, ), ] assert certifs == expected_certifs
async def api_realm_update_roles(self, client_ctx, msg): msg = realm_update_roles_serializer.req_load(msg) try: data = RealmRoleCertificateContent.verify_and_load( msg["role_certificate"], author_verify_key=client_ctx.verify_key, expected_author=client_ctx.device_id, ) except DataError as exc: return { "status": "invalid_certification", "reason": f"Invalid certification data ({exc}).", } now = pendulum.now() if not timestamps_in_the_ballpark(data.timestamp, now): return { "status": "invalid_certification", "reason": f"Invalid timestamp in certification.", } granted_role = RealmGrantedRole( certificate=msg["role_certificate"], realm_id=data.realm_id, user_id=data.user_id, role=data.role, granted_by=data.author, granted_on=data.timestamp, ) if granted_role.granted_by.user_id == granted_role.user_id: return { "status": "invalid_data", "reason": f"Realm role certificate cannot be self-signed.", } try: await self.update_roles(client_ctx.organization_id, granted_role, msg["recipient_message"]) except RealmRoleAlreadyGranted: return realm_update_roles_serializer.rep_dump( {"status": "already_granted"}) except RealmAccessError: return realm_update_roles_serializer.rep_dump( {"status": "not_allowed"}) except RealmNotFoundError as exc: return realm_update_roles_serializer.rep_dump({ "status": "not_found", "reason": str(exc) }) except RealmInMaintenanceError: return realm_update_roles_serializer.rep_dump( {"status": "in_maintenance"}) return realm_update_roles_serializer.rep_dump({"status": "ok"})
async def test_realm_create_not_allowed_for_outsider(backend, alice, alice_backend_sock): realm_id = UUID("C0000000000000000000000000000000") certif = RealmRoleCertificateContent.build_realm_root_certif( author=alice.device_id, timestamp=pendulum.now(), realm_id=realm_id ).dump_and_sign(alice.signing_key) rep = await realm_create(alice_backend_sock, certif, check_rep=False) assert rep == {"status": "not_allowed", "reason": "Outsider user cannot create realm"}
async def _update_role_and_check_events(role): with backend.event_bus.listen() as spy: certif = RealmRoleCertificateContent( author=alice.device_id, timestamp=pendulum.now(), realm_id=realm, user_id=bob.user_id, role=role, ).dump_and_sign(alice.signing_key) rep = await realm_update_roles(alice_backend_sock, certif, check_rep=False) assert rep == {"status": "ok"} await spy.wait_with_timeout( "realm.roles_updated", { "organization_id": alice.organization_id, "author": alice.device_id, "realm_id": realm, "user": bob.user_id, "role": role, }, ) # Check events propagated to the client rep = await events_listen_nowait(bob_backend_sock) assert rep == { "status": "ok", "event": "realm.roles_updated", "realm_id": realm, "role": role, } rep = await events_listen_nowait(bob_backend_sock) assert rep == {"status": "no_events"}
async def test_status(backend, bob_backend_sock, alice_backend_sock, alice, bob, realm): rep = await realm_status(alice_backend_sock, realm) assert rep == { "status": "ok", "in_maintenance": False, "maintenance_type": None, "maintenance_started_by": None, "maintenance_started_on": None, "encryption_revision": 1, } # Cheap test on no access rep = await realm_status(bob_backend_sock, realm) assert rep == {"status": "not_allowed"} # Also test lesser role have access await realm_update_roles( alice_backend_sock, RealmRoleCertificateContent( author=alice.device_id, timestamp=pendulum_now(), realm_id=realm, user_id=bob.user_id, role=RealmRole.READER, ).dump_and_sign(alice.signing_key), ) rep = await realm_status(bob_backend_sock, realm) assert rep == { "status": "ok", "in_maintenance": False, "maintenance_type": None, "maintenance_started_by": None, "maintenance_started_on": None, "encryption_revision": 1, }
async def _realm_get_clear_role_certifs(sock, realm_id): rep = await realm_get_role_certificates(sock, realm_id) assert rep["status"] == "ok" cooked = [ RealmRoleCertificateContent.unsecure_load(certif) for certif in rep["certificates"] ] return [item for item in sorted(cooked, key=lambda x: x.timestamp)]
async def api_realm_create(self, client_ctx, msg): msg = realm_create_serializer.req_load(msg) try: data = RealmRoleCertificateContent.verify_and_load( msg["role_certificate"], author_verify_key=client_ctx.verify_key, expected_author=client_ctx.device_id, ) except DataError as exc: return { "status": "invalid_certification", "reason": f"Invalid certification data ({exc}).", } now = pendulum.now() if not timestamps_in_the_ballpark(data.timestamp, now): return { "status": "invalid_certification", "reason": f"Invalid timestamp in certification.", } granted_role = RealmGrantedRole( certificate=msg["role_certificate"], realm_id=data.realm_id, user_id=data.user_id, role=data.role, granted_by=data.author, granted_on=data.timestamp, ) if granted_role.granted_by.user_id != granted_role.user_id: return { "status": "invalid_data", "reason": f"Initial realm role certificate must be self-signed.", } if granted_role.role != RealmRole.OWNER: return { "status": "invalid_data", "reason": f"Initial realm role certificate must set OWNER role.", } try: await self.create(client_ctx.organization_id, granted_role) except RealmNotFoundError as exc: return realm_create_serializer.rep_dump({ "status": "not_found", "reason": str(exc) }) except RealmAlreadyExistsError: return realm_create_serializer.rep_dump( {"status": "already_exists"}) return realm_create_serializer.rep_dump({"status": "ok"})
async def test_create_invalid_certif(backend, alice, bob, alice_backend_sock): realm_id = UUID("C0000000000000000000000000000000") certif = RealmRoleCertificateContent.build_realm_root_certif( author=bob.device_id, timestamp=pendulum.now(), realm_id=realm_id ).dump_and_sign(bob.signing_key) rep = await realm_create(alice_backend_sock, certif) assert rep == { "status": "invalid_certification", "reason": "Invalid certification data (Signature was forged or corrupt).", }
async def _realm_generate_certif_and_update_roles_or_fail( backend_sock, author, realm_id, user_id, role, timestamp=None): certif = RealmRoleCertificateContent( author=author.device_id, timestamp=timestamp or next_timestamp(), realm_id=realm_id, user_id=user_id, role=role, ).dump_and_sign(author.signing_key) return await realm_update_roles(backend_sock, certif, check_rep=False)
async def test_realm_create(backend, alice, alice_backend_sock): await events_subscribe(alice_backend_sock) realm_id = UUID("C0000000000000000000000000000000") certif = RealmRoleCertificateContent.build_realm_root_certif( author=alice.device_id, timestamp=pendulum.now(), realm_id=realm_id ).dump_and_sign(alice.signing_key) with backend.event_bus.listen() as spy: await realm_create(alice_backend_sock, certif) await spy.wait_with_timeout("realm.roles_updated")
async def _test_create_ok(backend, device, device_backend_sock): await events_subscribe(device_backend_sock) realm_id = RealmID.from_hex("C0000000000000000000000000000000") certif = RealmRoleCertificateContent.build_realm_root_certif( author=device.device_id, timestamp=pendulum.now(), realm_id=realm_id).dump_and_sign(device.signing_key) with backend.event_bus.listen() as spy: rep = await realm_create(device_backend_sock, certif) assert rep == {"status": "ok"} await spy.wait_with_timeout(BackendEvent.REALM_ROLES_UPDATED)
async def test_create_certif_too_old(alice, alice_backend_sock): realm_id = UUID("C0000000000000000000000000000000") now = pendulum.now() certif = RealmRoleCertificateContent.build_realm_root_certif( author=alice.device_id, timestamp=now, realm_id=realm_id).dump_and_sign(alice.signing_key) with freeze_time(now.add(seconds=TIMESTAMP_MAX_DT)): rep = await realm_create(alice_backend_sock, certif) assert rep == { "status": "invalid_certification", "reason": "Invalid timestamp in certification.", }
async def test_create_certif_not_self_signed(alice, bob, alice_backend_sock): realm_id = UUID("C0000000000000000000000000000000") certif = RealmRoleCertificateContent( author=alice.device_id, timestamp=pendulum.now(), realm_id=realm_id, user_id=bob.user_id, role=RealmRole.OWNER, ).dump_and_sign(alice.signing_key) rep = await realm_create(alice_backend_sock, certif) assert rep == { "status": "invalid_data", "reason": "Initial realm role certificate must be self-signed.", }
async def test_create_certif_role_not_owner(alice, alice_backend_sock): realm_id = RealmID.from_hex("C0000000000000000000000000000000") certif = RealmRoleCertificateContent( author=alice.device_id, timestamp=pendulum.now(), realm_id=realm_id, user_id=alice.user_id, role=RealmRole.MANAGER, ).dump_and_sign(alice.signing_key) rep = await realm_create(alice_backend_sock, certif) assert rep == { "status": "invalid_data", "reason": "Initial realm role certificate must set OWNER role.", }
async def _backend_realm_generate_certif_and_update_roles(backend, author, realm_id, user_id, role): now = pendulum_now() certif = RealmRoleCertificateContent( author=author.device_id, timestamp=now, realm_id=realm_id, user_id=user_id, role=role ).dump_and_sign(author.signing_key) await backend.realm.update_roles( author.organization_id, RealmGrantedRole( certificate=certif, realm_id=realm_id, user_id=user_id, role=role, granted_by=author.device_id, granted_on=now, ), ) return certif
async def create_realm(self, realm_id: EntryID): """ Raises: FSError FSBackendOfflineError """ certif = RealmRoleCertificateContent.build_realm_root_certif( author=self.device.device_id, timestamp=pendulum_now(), realm_id=realm_id).dump_and_sign(self.device.signing_key) rep = await self._backend_cmds("realm_create", certif) if rep["status"] == "already_exists": # It's possible a previous attempt to create this realm # succeeded but we didn't receive the confirmation, hence # we play idempotent here. return elif rep["status"] != "ok": raise FSError(f"Cannot create realm {realm_id}: `{rep['status']}`")
async def _realm_factory(backend, author, realm_id=None, now=None): realm_id = realm_id or uuid4() now = now or pendulum_now() certif = RealmRoleCertificateContent.build_realm_root_certif( author=author.device_id, timestamp=now, realm_id=realm_id ).dump_and_sign(author.signing_key) with backend.event_bus.listen() as spy: await backend.realm.create( organization_id=author.organization_id, self_granted_role=RealmGrantedRole( realm_id=realm_id, user_id=author.user_id, certificate=certif, role=RealmRole.OWNER, granted_by=author.device_id, granted_on=now, ), ) await spy.wait_with_timeout("realm.roles_updated") return realm_id
async def _give_role(self, author_sock, author, recipient, role): author_sock = await self.get_sock(author) certif = RealmRoleCertificateContent( author=author.device_id, timestamp=next_timestamp(), realm_id=self.realm_id, user_id=recipient.user_id, role=role, ).dump_and_sign(author.signing_key) rep = await realm_update_roles(author_sock, certif, check_rep=False) if author.user_id == recipient.user_id: assert rep == { "status": "invalid_data", "reason": "Realm role certificate cannot be self-signed.", } else: owner_only = (RealmRole.OWNER, ) owner_or_manager = (RealmRole.OWNER, RealmRole.MANAGER) existing_recipient_role = self.current_roles[recipient.user_id] if existing_recipient_role in owner_or_manager or role in owner_or_manager: allowed_roles = owner_only else: allowed_roles = owner_or_manager if self.current_roles[author.user_id] in allowed_roles: # print(f"+ {author.user_id} -{role.value}-> {recipient.user_id}") if existing_recipient_role != role: assert rep == {"status": "ok"} self.current_roles[recipient.user_id] = role self.certifs.append(certif) else: assert rep == {"status": "already_granted"} else: # print(f"- {author.user_id} -{role.value}-> {recipient.user_id}") assert rep == {"status": "not_allowed"} return rep["status"] == "ok"
async def _update_role(self, author, user, role=RealmRole.MANAGER): now = pendulum_now() certif = RealmRoleCertificateContent( author=author.device_id, timestamp=now, realm_id=RealmID(self.wid.uuid), user_id=user.user_id, role=role, ).dump_and_sign(author.signing_key) await self.backend.realm.update_roles( author.organization_id, RealmGrantedRole( certificate=certif, realm_id=RealmID(self.wid.uuid), user_id=user.user_id, role=role, granted_by=author.device_id, granted_on=now, ), ) return certif
async def create_realm(self, realm_id: EntryID) -> None: """ Raises: FSError FSRemoteOperationError FSBackendOfflineError """ timestamp = self.device.timestamp() certif = RealmRoleCertificateContent.build_realm_root_certif( author=self.device.device_id, timestamp=timestamp, realm_id=RealmID(realm_id.uuid)).dump_and_sign( self.device.signing_key) with translate_backend_cmds_errors(): rep = await self.backend_cmds.realm_create(certif) if rep["status"] == "already_exists": # It's possible a previous attempt to create this realm # succeeded but we didn't receive the confirmation, hence # we play idempotent here. return elif rep["status"] != "ok": raise FSError(f"Cannot create realm {realm_id}: `{rep['status']}`")
async def api_realm_update_roles(self, client_ctx, msg): """ This API call, when successful, performs the writing of a new role certificate to the database. Before adding new entries, extra care should be taken in order to guarantee the consistency in the ordering of the different timestamps stored in the database. In particular, the backend server performs the following checks: - The certificate must have a timestamp strictly greater than the last certificate for the same user in the same realm. - If the certificate corresponds to a role without write rights, its timestamp should be strictly greater than the timestamp of the last vlob update performed by the corresponding user in the corresponding realm. - If the certificate corresponds to a role without management rights, its timestamp should be strictly greater than the timestamp of the last role certificate uploaded by the corresponding user in the corresponding realm. If one of those constraints is not satisfied, an error is returned with the status `require_greater_timestamp` indicating to the client that it should craft a new certificate with a timestamp strictly greater than the timestamp provided with the error. The `api_vlob_create` and `api_vlob_update` calls also perform similar checks. """ # An OUTSIDER is allowed to create a realm (given he needs to have one # to store it user manifest). However he cannot be MANAGER or OWNER in # a shared realm as well. # Hence the only way for him to be OWNER is to create a realm, and in # this case he cannot share this realm with anyone. # On top of that, we don't have to fetch the user profile from the # database before checking it given it cannot be updated. if client_ctx.profile == UserProfile.OUTSIDER: return { "status": "not_allowed", "reason": "Outsider user cannot share realm" } msg = realm_update_roles_serializer.req_load(msg) try: data = RealmRoleCertificateContent.verify_and_load( msg["role_certificate"], author_verify_key=client_ctx.verify_key, expected_author=client_ctx.device_id, ) except DataError as exc: return { "status": "invalid_certification", "reason": f"Invalid certification data ({exc}).", } now = pendulum.now() if not timestamps_in_the_ballpark(data.timestamp, now): return realm_update_roles_serializer.timestamp_out_of_ballpark_rep_dump( backend_timestamp=now, client_timestamp=data.timestamp) granted_role = RealmGrantedRole( certificate=msg["role_certificate"], realm_id=data.realm_id, user_id=data.user_id, role=data.role, granted_by=data.author, granted_on=data.timestamp, ) if granted_role.granted_by.user_id == granted_role.user_id: return { "status": "invalid_data", "reason": f"Realm role certificate cannot be self-signed.", } try: await self.update_roles(client_ctx.organization_id, granted_role, msg["recipient_message"]) except RealmRoleAlreadyGranted: return realm_update_roles_serializer.rep_dump( {"status": "already_granted"}) except RealmAccessError: return realm_update_roles_serializer.rep_dump( {"status": "not_allowed"}) except RealmRoleRequireGreaterTimestampError as exc: return realm_update_roles_serializer.require_greater_timestamp_rep_dump( exc.strictly_greater_than) except RealmIncompatibleProfileError as exc: return realm_update_roles_serializer.rep_dump({ "status": "incompatible_profile", "reason": str(exc) }) except RealmNotFoundError as exc: return realm_update_roles_serializer.rep_dump({ "status": "not_found", "reason": str(exc) }) except RealmInMaintenanceError: return realm_update_roles_serializer.rep_dump( {"status": "in_maintenance"}) return realm_update_roles_serializer.rep_dump({"status": "ok"})
async def api_realm_update_roles(self, client_ctx, msg): # An OUTSIDER is allowed to create a realm (given he needs to have one # to store it user manifest). However he cannot be MANAGER or OWNER in # a shared realm as well. # Hence the only way for him to be OWNER is to create a realm, and in # this case he cannot share this realm with anyone. # On top of that, we don't have to fetch the user profile from the # database before checking it given it cannot be updated. if client_ctx.profile == UserProfile.OUTSIDER: return { "status": "not_allowed", "reason": "Outsider user cannot share realm" } msg = realm_update_roles_serializer.req_load(msg) try: data = RealmRoleCertificateContent.verify_and_load( msg["role_certificate"], author_verify_key=client_ctx.verify_key, expected_author=client_ctx.device_id, ) except DataError as exc: return { "status": "invalid_certification", "reason": f"Invalid certification data ({exc}).", } now = pendulum.now() if not timestamps_in_the_ballpark(data.timestamp, now): return { "status": "invalid_certification", "reason": f"Invalid timestamp in certification.", } granted_role = RealmGrantedRole( certificate=msg["role_certificate"], realm_id=data.realm_id, user_id=data.user_id, role=data.role, granted_by=data.author, granted_on=data.timestamp, ) if granted_role.granted_by.user_id == granted_role.user_id: return { "status": "invalid_data", "reason": f"Realm role certificate cannot be self-signed.", } try: await self.update_roles(client_ctx.organization_id, granted_role, msg["recipient_message"]) except RealmRoleAlreadyGranted: return realm_update_roles_serializer.rep_dump( {"status": "already_granted"}) except RealmAccessError: return realm_update_roles_serializer.rep_dump( {"status": "not_allowed"}) except RealmIncompatibleProfileError as exc: return realm_update_roles_serializer.rep_dump({ "status": "incompatible_profile", "reason": str(exc) }) except RealmNotFoundError as exc: return realm_update_roles_serializer.rep_dump({ "status": "not_found", "reason": str(exc) }) except RealmInMaintenanceError: return realm_update_roles_serializer.rep_dump( {"status": "in_maintenance"}) return realm_update_roles_serializer.rep_dump({"status": "ok"})
async def _create_realm_and_first_vlob(self, device): manifest = initial_user_manifest_state.get_user_manifest_v1_for_backend(device) if manifest.author == device.device_id: author = device else: author = self.get_device(device.organization_id, manifest.author) realm_id = author.user_manifest_id with self.backend.event_bus.listen() as spy: await self.backend.realm.create( organization_id=author.organization_id, self_granted_role=RealmGrantedRole( realm_id=realm_id, user_id=author.user_id, certificate=RealmRoleCertificateContent( author=author.device_id, timestamp=manifest.timestamp, realm_id=realm_id, user_id=author.user_id, role=RealmRole.OWNER, ).dump_and_sign(author.signing_key), role=RealmRole.OWNER, granted_by=author.device_id, granted_on=manifest.timestamp, ), ) await self.backend.vlob.create( organization_id=author.organization_id, author=author.device_id, realm_id=realm_id, encryption_revision=1, vlob_id=author.user_manifest_id, timestamp=manifest.timestamp, blob=manifest.dump_sign_and_encrypt( author_signkey=author.signing_key, key=author.user_manifest_key ), ) # Avoid possible race condition in tests listening for events await spy.wait_multiple_with_timeout( [ ( BackendEvent.REALM_ROLES_UPDATED, { "organization_id": author.organization_id, "author": author.device_id, "realm_id": author.user_manifest_id, "user": author.user_id, "role": RealmRole.OWNER, }, ), ( BackendEvent.REALM_VLOBS_UPDATED, { "organization_id": author.organization_id, "author": author.device_id, "realm_id": author.user_manifest_id, "checkpoint": 1, "src_id": author.user_manifest_id, "src_version": 1, }, ), ] )
async def _outbound_sync_inner(self) -> bool: base_um = self.get_user_manifest() if not base_um.need_sync: return True # Make sure the corresponding realm has been created in the backend if base_um.is_placeholder: certif = RealmRoleCertificateContent.build_realm_root_certif( author=self.device.device_id, timestamp=pendulum_now(), realm_id=self.device.user_manifest_id, ).dump_and_sign(self.device.signing_key) try: rep = await self.backend_cmds.realm_create(certif) except BackendNotAvailable as exc: raise FSBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise FSError(f"Cannot create user manifest's realm in backend: {exc}") from exc if rep["status"] == "already_exists": # It's possible a previous attempt to create this realm # succeeded but we didn't receive the confirmation, hence # we play idempotent here. pass elif rep["status"] != "ok": raise FSError(f"Cannot create user manifest's realm in backend: {rep}") # Sync placeholders for w in base_um.workspaces: await self._workspace_minimal_sync(w) # Build vlob now = pendulum_now() to_sync_um = base_um.to_remote(author=self.device.device_id, timestamp=now) ciphered = to_sync_um.dump_sign_and_encrypt( author_signkey=self.device.signing_key, key=self.device.user_manifest_key ) # Sync the vlob with backend try: # Note encryption_revision is always 1 given we never reencrypt # the user manifest's realm if to_sync_um.version == 1: rep = await self.backend_cmds.vlob_create( self.user_manifest_id, 1, self.user_manifest_id, now, ciphered ) else: rep = await self.backend_cmds.vlob_update( 1, self.user_manifest_id, to_sync_um.version, now, ciphered ) except BackendNotAvailable as exc: raise FSBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise FSError(f"Cannot sync user manifest: {exc}") from exc if rep["status"] in ("already_exists", "bad_version"): # Concurrency error (handled by the caller) return False elif rep["status"] == "in_maintenance": raise FSWorkspaceInMaintenance( f"Cannot modify workspace data while it is in maintenance: {rep}" ) elif rep["status"] != "ok": raise FSError(f"Cannot sync user manifest: {rep}") # Merge back the manifest in local async with self._update_user_manifest_lock: diverged_um = self.get_user_manifest() # Final merge could have been achieved by a concurrent operation if to_sync_um.version > diverged_um.base_version: merged_um = merge_local_user_manifests(diverged_um, to_sync_um) await self.set_user_manifest(merged_um) self.event_bus.send("fs.entry.synced", id=self.user_manifest_id) return True
async def _load_realm_role_certificates(self, realm_id: Optional[EntryID] = None ): rep = await self._backend_cmds("realm_get_role_certificates", realm_id or self.workspace_id) if rep["status"] == "not_allowed": # Seems we lost the access to the realm raise FSWorkspaceNoReadAccess( "Cannot get workspace roles: no read access") elif rep["status"] != "ok": raise FSError( f"Cannot retrieve workspace roles: `{rep['status']}`") try: # Must read unverified certificates to access metadata unsecure_certifs = sorted( [(RealmRoleCertificateContent.unsecure_load(uv_role), uv_role) for uv_role in rep["certificates"]], key=lambda x: x[0].timestamp, ) current_roles = {} owner_only = (RealmRole.OWNER, ) owner_or_manager = (RealmRole.OWNER, RealmRole.MANAGER) # Now verify each certif for unsecure_certif, raw_certif in unsecure_certifs: author = await self.remote_device_manager.get_device( unsecure_certif.author) RealmRoleCertificateContent.verify_and_load( raw_certif, author_verify_key=author.verify_key, expected_author=author.device_id, ) # Make sure author had the right to do this existing_user_role = current_roles.get(unsecure_certif.user_id) if not current_roles and unsecure_certif.user_id == author.device_id.user_id: # First user is autosigned needed_roles = (None, ) elif (existing_user_role in owner_or_manager or unsecure_certif.role in owner_or_manager): needed_roles = owner_only else: needed_roles = owner_or_manager if current_roles.get( unsecure_certif.author.user_id) not in needed_roles: raise FSError( f"Invalid realm role certificates: " f"{unsecure_certif.author} has not right to give " f"{unsecure_certif.role} role to {unsecure_certif.user_id} " f"on {unsecure_certif.timestamp}") if unsecure_certif.role is None: current_roles.pop(unsecure_certif.user_id, None) else: current_roles[ unsecure_certif.user_id] = unsecure_certif.role # Decryption error except DataError as exc: raise FSError(f"Invalid realm role certificates: {exc}") from exc # Now unsecure_certifs is no longer unsecure we have valided it items return [c for c, _ in unsecure_certifs], current_roles
async def workspace_share( self, workspace_id: EntryID, recipient: UserID, role: Optional[WorkspaceRole] ) -> None: """ Raises: FSError FSWorkspaceNotFoundError FSBackendOfflineError FSSharingNotAllowedError """ if self.device.user_id == recipient: raise FSError("Cannot share to oneself") user_manifest = self.get_user_manifest() workspace_entry = user_manifest.get_workspace_entry(workspace_id) if not workspace_entry: raise FSWorkspaceNotFoundError(f"Unknown workspace `{workspace_id}`") # Make sure the workspace is not a placeholder await self._workspace_minimal_sync(workspace_entry) # Retrieve the user try: recipient_user, revoked_recipient_user = await self.remote_devices_manager.get_user( recipient ) except RemoteDevicesManagerBackendOfflineError as exc: raise FSBackendOfflineError(str(exc)) from exc except RemoteDevicesManagerError as exc: raise FSError(f"Cannot retreive recipient: {exc}") from exc if revoked_recipient_user: raise FSError(f"User {recipient} revoked") # Note we don't bother to check workspace's access roles given they # could be outdated (and backend will do the check anyway) now = pendulum_now() # Build the sharing message try: if role is not None: recipient_message = SharingGrantedMessageContent( author=self.device.device_id, timestamp=now, name=workspace_entry.name, id=workspace_entry.id, encryption_revision=workspace_entry.encryption_revision, encrypted_on=workspace_entry.encrypted_on, key=workspace_entry.key, ) else: recipient_message = SharingRevokedMessageContent( author=self.device.device_id, timestamp=now, id=workspace_entry.id ) ciphered_recipient_message = recipient_message.dump_sign_and_encrypt_for( author_signkey=self.device.signing_key, recipient_pubkey=recipient_user.public_key ) except DataError as exc: raise FSError(f"Cannot create sharing message for `{recipient}`: {exc}") from exc # Build role certificate role_certificate = RealmRoleCertificateContent( author=self.device.device_id, timestamp=now, realm_id=workspace_id, user_id=recipient, role=role, ).dump_and_sign(self.device.signing_key) # Actually send the command to the backend try: rep = await self.backend_cmds.realm_update_roles( role_certificate, ciphered_recipient_message ) except BackendNotAvailable as exc: raise FSBackendOfflineError(str(exc)) from exc except BackendConnectionError as exc: raise FSError(f"Error while trying to set vlob group roles in backend: {exc}") from exc if rep["status"] == "not_allowed": raise FSSharingNotAllowedError( f"Must be Owner or Manager on the workspace is mandatory to share it: {rep}" ) elif rep["status"] == "in_maintenance": raise FSWorkspaceInMaintenance( f"Cannot share workspace while it is in maintenance: {rep}" ) elif rep["status"] == "already_granted": # Stay idempotent return elif rep["status"] != "ok": raise FSError(f"Error while trying to set vlob group roles in backend: {rep}")
async def test_create_realm_already_exists(alice, alice_backend_sock, realm): certif = RealmRoleCertificateContent.build_realm_root_certif( author=alice.device_id, timestamp=pendulum.now(), realm_id=realm).dump_and_sign(alice.signing_key) rep = await realm_create(alice_backend_sock, certif) assert rep == {"status": "already_exists"}