Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions docs/hosting/administration/erase_organization.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
.. Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS

.. _doc_hosting_erase_organization:

Erase an organization
=====================

Where & how the data are stored
-------------------------------
Comment on lines +8 to +9

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Where & how the data are stored
-------------------------------
Where is the organization's data stored?
----------------------------------------


For any given organization, data are split as follow:

- A PostgreSQL database containing the certificates (e.g. user/devices/workspaces) and
encrypted workspaces metadata (e.g. content of folder, list of blocks for each file version).
- A blockstore (e.g. S3) containing the blocks (i.e. encrypted pieces of data that compose the files).
- On top of that, each Parsec client having access to the organization has an encrypted local database
containing a copy of the certificates, metadata for the workspaces it has access to, and a subset
of the blocks (depending on local cache configuration).
Comment on lines +13 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- A PostgreSQL database containing the certificates (e.g. user/devices/workspaces) and
encrypted workspaces metadata (e.g. content of folder, list of blocks for each file version).
- A blockstore (e.g. S3) containing the blocks (i.e. encrypted pieces of data that compose the files).
- On top of that, each Parsec client having access to the organization has an encrypted local database
containing a copy of the certificates, metadata for the workspaces it has access to, and a subset
of the blocks (depending on local cache configuration).
- A PostgreSQL database containing certificates (e.g. users, devices, workspaces) and
encrypted workspace metadata (e.g. folder contents, block list for each file version).
- An S3 blockstore containing the encrypted file blocks.
- On each Parsec client having access to the organization: An encrypted local database
containing a copy of the certificates, workspace metadata it has access to, and a subset
of the encrypted file blocks (depending on local cache configuration).


Erasing data
------------

When no longer in use (or for legal reason) an organization can be erased from the Parsec server.

In practice this means:
Comment on lines +23 to +25

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
When no longer in use (or for legal reason) an organization can be erased from the Parsec server.
In practice this means:
When no longer in use (or for legal reason) an organization can be erased from the Parsec server
following these steps:


1. Removing everything (certificates & metadata) related to the organization in the PostgreSQL database.
2. Removing the blocks from the blockstore.
3. Removing the remaining data from the clients.
Comment on lines +27 to +29

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd use "delete" (or erase) since "remove" may indicate persistence.

Suggested change
1. Removing everything (certificates & metadata) related to the organization in the PostgreSQL database.
2. Removing the blocks from the blockstore.
3. Removing the remaining data from the clients.
1. Delete certificates & metadata from the PostgreSQL database.
2. Delete the encrypted file blocks from the blockstore.
3. Delete remaining local data from Parsec clients.


Step 1: remove from PostgreSQL
------------------------------
Comment on lines +31 to +32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd make these steps (1, 2, 3) subsections of "Erasing data"

Suggested change
Step 1: remove from PostgreSQL
------------------------------
Suggested change
Step 1: remove from PostgreSQL
------------------------------
Step 1: Delete certificates from PostgreSQL
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^


Erasing an organization from Parsec is both an uncommon and (obviously !) a destructive operation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Erasing an organization from Parsec is both an uncommon and (obviously !) a destructive operation.
Erasing an organization from Parsec is both an uncommon and (obviously !) destructive operation.

As such it is not available from the Administration API but instead must be triggered from the server CLI directly.
Comment thread
FirelightFlagboy marked this conversation as resolved.

.. code-block:: bash

# On Parsec server
parsec erase_organization --organization <OrgName> --db <database_url>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
parsec erase_organization --organization <OrgName> --db <database_url>
parsec erase_organization --organization <organization_name> --db <database_url>


.. warning::

This operation cannot be undone. Make sure you have a backup of any data you may need before

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe add the command to make a backup in the warning message ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, the administrator is responsible for the backup strategy, so we cannot provide a one-size-fits-all command here (typically "backup any data" may refer to do a PostgreSQL snapshot and storing it on cold storage, or it may refer to simply have a user start a Parsec client and copy/paste the files still needed)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already a section describing how to make a backup, we should link to it. See my next comment/suggestion.

proceeding.
Comment on lines +44 to +45

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This operation cannot be undone. Make sure you have a backup of any data you may need before
proceeding.
This operation cannot be undone. Make sure you have a backup before proceeding.
See the :ref:`Backup and Restore section <doc_hosting_maintenance_backup_restore>`.


.. note::

After erasing an organization, it is possible to create a new organization with the same name

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the expected error on the client side if they haven't deleted their local data and a new organization with the same name is created ?

If their parsec client would just look offline that may be confusing if the same entity re creates an organization and expect to enroll the same people.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The client will get an error complaining the organization is not found on the server (even if a new organization with the same name is created, since they don't have the same root verify key)

I've added a word about this in the note 👍

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ I think we should move up this whole note from the step 1, maybe to the first or second section.

Otherwise one may think that it is OK to create an org with same name just after step 1 (postgresql), before step 2 (blockstore cleanup).

IMO, regardless of Parsec internals, the erasing procedure should be treated almost as a" transaction", step 1 to 3 should be performed as a whole before attempting to create a new organization with same name.

since no trace of the previous one remains.

Even if they share the same name, the erased and the new organization are strictly unrelated
since they have a different root verify key (i.e. root key used to verify all certificates
in the organization).

Typically this means a Parsec client trying to access the erased organization will complain
it doesn't exist on the server even if a new organization with the same name exist.
Comment on lines +52 to +57

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Even if they share the same name, the erased and the new organization are strictly unrelated
since they have a different root verify key (i.e. root key used to verify all certificates
in the organization).
Typically this means a Parsec client trying to access the erased organization will complain
it doesn't exist on the server even if a new organization with the same name exist.
Even if they share the same name, the organizations are strictly unrelated
since they have a different root verify key (used to verify the organization certificates).
If a Parsec client tries to access the erased organization it will complain
that it doesn't exist on the server even if a new organization with the same name exist.


Step 2: Blockstore cleanup
--------------------------
Comment on lines +59 to +60

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Step 2: Blockstore cleanup
--------------------------
Step 2: Delete blocks from the Blockstore
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^


Once step 1 done, the blocks' decryption keys has been lost. In other words, everything
stored in the blockstore and related to the organization is irrecoverable.
Hence removing those data from the blockstore should be seen as an optional step to reclaim
Comment on lines +63 to +64

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
stored in the blockstore and related to the organization is irrecoverable.
Hence removing those data from the blockstore should be seen as an optional step to reclaim
stored in the blockstore related to the organization is irrecoverable.
Hence removing those blocks can be seen as an optional step to reclaim

needlessly occupied space.

This can be done by manually removing from the blockstore the top level directory named after the organization.
For example, if the organization was named ``CoolOrg``, remove the ``CoolOrg/`` prefix from the bucket.

.. note::

Blockstores have their own backup strategy. Typically AWS S3 allows for a bucket to
have an history so that a data removal can be cancelled.
You should pay attention to this to ensure the blocks have actually been removed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we expect them to be removed from the history too ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal here is to permanently erase all data from a given organization, so we expect the administrator to remove the history.
However this is not Parsec responsibility since blockstore configuration is specific to the administrator needs (see #12172 (comment)).


Step 3: Clients cleanup
-----------------------
Comment on lines +76 to +77

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Step 3: Clients cleanup
-----------------------
Step 3: delete local data from Parsec clients
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^


Once the organization erased from the server, the Parsec client will display an error
about the organization not being found on the server.
However the client can still work in offline mode (if the server is not reachable, the
client cannot know the organization has been erased from the server!),

For this reason, an end-user is still able to use his Parsec client to work on the
organization using the local cache (e.g. creating a new file in a workspace or
reading an existing file that is in cache).
Comment on lines +84 to +86

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
For this reason, an end-user is still able to use his Parsec client to work on the
organization using the local cache (e.g. creating a new file in a workspace or
reading an existing file that is in cache).
For this reason, users are still able to use the Parsec client to work on the
organization for actions relying on the local cache such as importing a new file
or reading an existing file that is in cache.


To prevent the users from accessing the local cache, the local configuration and data should be manually removed:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by the user themselves ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can also be done by an administrator that has remote access to the machine. Here we don't specify how things should be done but what should be done.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rephrase it like this:

Suggested change
To prevent the users from accessing the local cache, the local configuration and data should be manually removed:
The local cache can be safely erased by manually deleting the local configuration and data for the device having access to the organization:

IMO we should add a note or warning to explain how to identify which device is linked to the organization (device id is displayed in the "My Devices" when logged-in... maybe we should consider displaying it in the Home page as tooltip or detail button ?)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, in other sections of the docs we suggest using the CLI for that, e.g.:

The <DEVICE_ID> mentioned below refer to the device used to access the
organization. You can find out which is your device ID by running:

.. code-block:: bash

parsec-cli device list


- Linux:
- config: ``$XDG_DATA_HOME or $HOME/.local/share/parsec3/<device_id>`` (e.g. ``/home/alice/.local/share/parsec3/e68b7131394749a4bbd279bd087e6ae6``)
- data: ``$XDG_CONFIG_HOME or $HOME/.config/parsec3/libparsec/devices/<device_id>`` (e.g. ``/home/alice/.config/parsec3/libparsec/devices/e68b7131394749a4bbd279bd087e6ae6``)
Comment on lines +91 to +92

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- config: ``$XDG_DATA_HOME or $HOME/.local/share/parsec3/<device_id>`` (e.g. ``/home/alice/.local/share/parsec3/e68b7131394749a4bbd279bd087e6ae6``)
- data: ``$XDG_CONFIG_HOME or $HOME/.config/parsec3/libparsec/devices/<device_id>`` (e.g. ``/home/alice/.config/parsec3/libparsec/devices/e68b7131394749a4bbd279bd087e6ae6``)
- config: ``$XDG_DATA_HOME`` or ``$HOME/.local/share/parsec3/<device_id>`` (e.g. ``/home/alice/.local/share/parsec3/e68b7131394749a4bbd279bd087e6ae6``)
- data: ``$XDG_CONFIG_HOME`` or ``$HOME/.config/parsec3/libparsec/devices/<device_id>`` (e.g. ``/home/alice/.config/parsec3/libparsec/devices/e68b7131394749a4bbd279bd087e6ae6``)

- macOS:
- config: ``$HOME/Library/Application Support/parsec3/<device_id>`` (e.g. ``/Users/Alice/Library/Application Support/parsec3/e68b7131394749a4bbd279bd087e6ae6``)
- data: ``$HOME/Library/Application Support/parsec3/libparsec/devices/<device_id>`` (e.g. ``/Users/Alice/Library/Application Support/parsec3/libparsec/devices/e68b7131394749a4bbd279bd087e6ae6``)
- Windows:
- config: ``{FOLDERID_RoamingAppData}\parsec3\<device_id>`` (e.g. ``C:\Users\Alice\AppData\Roaming\parsec3\e68b7131394749a4bbd279bd087e6ae6``)

Check warning on line 97 in docs/hosting/administration/erase_organization.rst

View workflow job for this annotation

GitHub Actions / spelling / cspell

Unknown word (FOLDERID) Suggestions: (folded, folder, FOLDER, folders, FOLDERS)
- data: ``{FOLDERID_RoamingAppData}\parsec3\libparsec\devices\<device_id>`` (e.g. ``C:\Users\Alice\AppData\Roaming/parsec3/libparsec/devices/e68b7131394749a4bbd279bd087e6ae6``)

Check warning on line 98 in docs/hosting/administration/erase_organization.rst

View workflow job for this annotation

GitHub Actions / spelling / cspell

Unknown word (FOLDERID) Suggestions: (folded, folder, FOLDER, folders, FOLDERS)
1 change: 1 addition & 0 deletions docs/hosting/administration/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ Server Administration
stats_organization
freeze_users
shared_recovery
erase_organization
3 changes: 0 additions & 3 deletions server/parsec/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,6 @@ async def test_customize_organization(
skip_events_offset=len(template.events),
)

async def test_drop_organization(self, id: OrganizationID) -> None:
await self.organization.test_drop_organization(id)

async def test_load_template(self, template: TestbedTemplateContent) -> OrganizationID:
org_id = OrganizationID(f"{template.id.title().replace('_', '')}OrgTemplate")
match await self.organization.create(
Expand Down
2 changes: 2 additions & 0 deletions server/parsec/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import click

from parsec.cli.erase_organization import erase_organization
from parsec.cli.export import export_realm
from parsec.cli.export_email import export_email
from parsec.cli.inspect import human_accesses
Expand Down Expand Up @@ -45,6 +46,7 @@ def cli() -> None:
pass


cli.add_command(erase_organization, "erase_organization")
cli.add_command(run_cmd, "run")
cli.add_command(migrate, "migrate")
cli.add_command(export_realm, "export_realm")
Expand Down
139 changes: 139 additions & 0 deletions server/parsec/cli/erase_organization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS
from __future__ import annotations

import asyncio
from typing import Any

import click

from parsec._parsec import (
OrganizationID,
)
from parsec.cli.options import (
db_server_options,
debug_config_options,
logging_config_options,
)
from parsec.cli.testbed import if_testbed_available
from parsec.cli.utils import cli_exception_handler, spinner, start_backend
from parsec.components.organization import OrganizationEraseBadOutcome
from parsec.config import (
BaseDatabaseConfig,
DisabledBlockStoreConfig,
LogLevel,
MockedBlockStoreConfig,
)


class DevOption(click.Option):
def handle_parse_result(
self, ctx: click.Context, opts: Any, args: list[str]
) -> tuple[Any, list[str]]:
value, args = super().handle_parse_result(ctx, opts, args)
if value:
for key, value in (
("debug", True),
("db", "MOCKED"),
("with_testbed", "coolorg"),
("organization", "CoolorgOrgTemplate"),
):
if key not in opts:
opts[key] = value

return value, args


@click.command(short_help="Erase an organization from the database")
@click.option("--organization", type=OrganizationID, help="Organization ID", required=True)
@click.option("--yes", is_flag=True, help="Don't ask for confirmation before proceeding")
@db_server_options
# Add --log-level/--log-format/--log-file
@logging_config_options(default_log_level="INFO")
# Add --debug & --version
@debug_config_options
@if_testbed_available(
click.option("--with-testbed", help="Start by populating with a testbed template")
)
@if_testbed_available(
click.option(
"--dev",
cls=DevOption,
is_flag=True,
is_eager=True,
help=(
"Equivalent to `--debug --db=MOCKED --with-testbed=coolorg --organization CoolorgOrgTemplate`"
),
)
)
def erase_organization(
Comment thread
FirelightFlagboy marked this conversation as resolved.
organization: OrganizationID,
db: BaseDatabaseConfig,
db_max_connections: int,
db_min_connections: int,
log_level: LogLevel,
log_format: str,
log_file: str | None,
yes: bool,
debug: bool,
with_testbed: str | None = None,
dev: bool = False,
) -> None:
with cli_exception_handler(debug):
asyncio.run(
_erase_organization(
yes=yes,
db_config=db,
debug=debug,
with_testbed=with_testbed,
organization_id=organization,
)
)


async def _erase_organization(
db_config: BaseDatabaseConfig,
yes: bool,
debug: bool,
with_testbed: str | None,
organization_id: OrganizationID,
) -> None:
# Can use a dummy blockstore config since we are not going to query it
if with_testbed is None:
blockstore_config = DisabledBlockStoreConfig()
else:
# Testbed template might need to create some blocks
blockstore_config = MockedBlockStoreConfig()

display_org = click.style(organization_id.str, fg="yellow")
click.echo(
f"You are about to entirely erase the {display_org} organization from the database, this action cannot be undone."
)

display_bucket_path = click.style(f"{organization_id.str}/", fg="yellow")
click.echo("Notes:")
click.echo(
"- No trace of the organization will remain, so it will be possible to re-create another organization with the same name."
)
click.echo(
f"- The organization's blocks won't be erased from the blockstore, you should manually remove the {display_bucket_path} top level directory from it."
)
click.echo("")

if not yes:
confirmation = click.prompt("To confirm, type the name of the organization")
if confirmation != organization_id.str:
raise RuntimeError("Organization name does not match, aborting")

async with start_backend(
db_config=db_config,
blockstore_config=blockstore_config,
debug=debug,
populate_with_template=with_testbed,
) as backend:
async with spinner("Removing from database..."):
outcome = await backend.organization.erase(id=organization_id)
match outcome:
case None:
pass
case OrganizationEraseBadOutcome.ORGANIZATION_NOT_FOUND:
raise RuntimeError("Organization doesn't exist")
13 changes: 6 additions & 7 deletions server/parsec/cli/testbed.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,10 @@ async def customize_organization(self, id: OrganizationID, customization: bytes)
cooked_customization = testbed.test_load_testbed_customization(template, customization) # pyright: ignore [reportPossiblyUnboundVariable]
await self.backend.test_customize_organization(id, template, cooked_customization)

async def drop_organization(self, id: OrganizationID) -> None:
await self.backend.test_drop_organization(id)
del self.template_per_org[id]
async def drop_organization_idempotent(self, id: OrganizationID) -> None:
# Ignore errors (in case the organization doesn't exist) to be idempotent
await self.backend.organization.erase(id)
self.template_per_org.pop(id, None)


testbed_router = APIRouter(tags=["testbed"])
Expand Down Expand Up @@ -244,8 +245,7 @@ async def test_new(template: str, request: Request, background_tasks: Background
async def _organization_garbage_collector():
await asyncio.sleep(orga_life_limit)
logger.info("Dropping testbed org due to time limit", organization=new_org_id.str)
# Dropping is idempotent, so no need for error handling
await testbed.backend.test_drop_organization(new_org_id)
await testbed.drop_organization_idempotent(new_org_id)

background_tasks.add_task(_organization_garbage_collector)

Expand Down Expand Up @@ -279,8 +279,7 @@ async def test_drop(raw_organization_id: str, request: Request) -> Response:
except ValueError:
return Response(status_code=400, content=b"")

# Dropping is idempotent, so no need for error handling
await testbed.drop_organization(organization_id)
await testbed.drop_organization_idempotent(organization_id)

return Response(status_code=200, content=b"")

Expand Down
15 changes: 11 additions & 4 deletions server/parsec/components/memory/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
OrganizationCreateBadOutcome,
OrganizationDump,
OrganizationDumpTopics,
OrganizationEraseBadOutcome,
OrganizationGetBadOutcome,
OrganizationGetTosBadOutcome,
OrganizationStats,
Expand Down Expand Up @@ -383,6 +384,16 @@ async def update(
if tos is not Unset:
await self._event_bus.send(EventOrganizationTosUpdated(organization_id=id))

@override
async def erase(
self,
id: OrganizationID,
) -> None | OrganizationEraseBadOutcome:
try:
del self._data.organizations[id]
except KeyError:
return OrganizationEraseBadOutcome.ORGANIZATION_NOT_FOUND

@override
async def get_tos(
self, id: OrganizationID
Expand Down Expand Up @@ -437,10 +448,6 @@ async def test_dump_topics(self, id: OrganizationID) -> OrganizationDumpTopics:
shamir_recovery=org.per_topic_last_timestamp.get("shamir_recovery"),
)

@override
async def test_drop_organization(self, id: OrganizationID) -> None:
self._data.organizations.pop(id, None)

@override
async def test_duplicate_organization(
self, source_id: OrganizationID, target_id: OrganizationID
Expand Down
13 changes: 10 additions & 3 deletions server/parsec/components/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ class OrganizationUpdateBadOutcome(BadOutcomeEnum):
ORGANIZATION_NOT_FOUND = auto()


class OrganizationEraseBadOutcome(BadOutcomeEnum):
ORGANIZATION_NOT_FOUND = auto()


@dataclass(slots=True)
class OrganizationDumpTopics:
common: DateTime
Expand Down Expand Up @@ -316,6 +320,12 @@ async def update(
) -> None | OrganizationUpdateBadOutcome:
raise NotImplementedError

async def erase(
self,
id: OrganizationID,
) -> None | OrganizationEraseBadOutcome:
raise NotImplementedError

async def get_tos(
self, id: OrganizationID
) -> TermsOfService | None | OrganizationGetTosBadOutcome:
Expand All @@ -329,9 +339,6 @@ async def test_dump_organizations(
async def test_dump_topics(self, id: OrganizationID) -> OrganizationDumpTopics:
raise NotImplementedError

async def test_drop_organization(self, id: OrganizationID) -> None:
raise NotImplementedError

async def test_duplicate_organization(
self, source_id: OrganizationID, target_id: OrganizationID
) -> None:
Expand Down
Loading
Loading