Skip to content

Commit d5b0775

Browse files
feat(server): Add CLI task to list organizations
In preparation of #8352, I needed to bootstrap the `tasks` cli group and investigate if it's doable to perform operation without relying on a running backend
1 parent 56fe027 commit d5b0775

7 files changed

Lines changed: 148 additions & 1 deletion

File tree

server/parsec/cli/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from parsec.cli.sequester_create import create_service, generate_service_certificate
1919
from parsec.cli.sequester_list import list_services
2020
from parsec.cli.sequester_revoke import generate_service_revocation_certificate, revoke_service
21+
from parsec.cli.tasks import server_tasks_cmd_group
2122
from parsec.cli.testbed import TESTBED_AVAILABLE, testbed_cmd
2223

2324
__all__ = ("cli",)
@@ -53,6 +54,8 @@ def cli() -> None:
5354
cli.add_command(list_deletable_realms, "list_deletable_realms")
5455
cli.add_command(delete_realm, "delete_realm")
5556
cli.add_command(server_sequester_cmd, "sequester")
57+
cli.add_command(server_tasks_cmd_group)
58+
5659
if TESTBED_AVAILABLE:
5760
cli.add_command(testbed_cmd, "testbed")
5861
# Since `render_email` is only for debugging purpose, we don't expose it
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import click
2+
3+
from parsec.cli.options import version_option
4+
5+
from . import list_organization
6+
7+
8+
@click.group(name="tasks", short_help="Server tasks collections")
9+
@version_option
10+
def server_tasks_cmd_group() -> None:
11+
pass
12+
13+
14+
server_tasks_cmd_group.add_command(list_organization.cmd)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import dataclasses as dt
2+
import json
3+
4+
import click
5+
6+
from parsec._parsec import OrganizationID
7+
from parsec.cli.options import asyncio_run, db_server_options, logging_config_options
8+
from parsec.components.organization import OrganizationDump
9+
from parsec.components.postgresql.handler import asyncpg_pool_factory
10+
from parsec.components.postgresql.organization import PGOrganizationComponent
11+
from parsec.config import BaseDatabaseConfig, LogLevel, PostgreSQLDatabaseConfig
12+
from parsec.webhooks import MockedWebhooksComponent
13+
14+
15+
@click.command(name="list-organization", short_help="List organization present on the server")
16+
@db_server_options
17+
@logging_config_options(default_log_level="INFO")
18+
@asyncio_run
19+
async def cmd(
20+
db: BaseDatabaseConfig,
21+
db_min_connections: int,
22+
db_max_connections: int,
23+
log_level: LogLevel,
24+
log_format: str,
25+
log_file: str | None,
26+
):
27+
orgs = await list_organization(db)
28+
29+
click.echo_via_pager(json.dumps(orgs, default=dt.asdict, indent=4))
30+
31+
32+
async def list_organization(db: BaseDatabaseConfig) -> dict[OrganizationID, OrganizationDump]:
33+
# When mocked the organization are stored in volatile memory of the other process (server)
34+
# We will not be able to access it, so no organization
35+
if db.is_mocked():
36+
return {}
37+
38+
assert isinstance(db, PostgreSQLDatabaseConfig)
39+
async with asyncpg_pool_factory(
40+
url=db.url, min_connections=db.min_connections, max_connections=db.max_connections
41+
) as pool:
42+
org_cmp = PGOrganizationComponent(
43+
pool=pool,
44+
webhooks=MockedWebhooksComponent(),
45+
config=None, # pyright: ignore [reportArgumentType]
46+
)
47+
return await org_cmp.list_organizations()

server/parsec/components/postgresql/organization.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ async def get_tos(
355355

356356
@override
357357
@no_transaction
358-
async def test_dump_organizations(
358+
async def list_organizations(
359359
self, conn: AsyncpgConnection, skip_templates: bool = True
360360
) -> dict[OrganizationID, OrganizationDump]:
361361
return await organization_test_dump_organizations(conn, skip_templates)

server/parsec/webhooks.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,55 @@ async def sequester_service_on_realm_rotate_key(
236236
return SequesterServiceUnavailable(
237237
service_id=service_id,
238238
)
239+
240+
241+
class MockedWebhooksComponent(WebhooksComponent):
242+
"""
243+
Webhooks that does nothing,
244+
245+
Used when creating components that require a webhook class but we do not except that component to use webhook (useful for tasks)"""
246+
247+
async def __init__(self) -> None:
248+
pass
249+
250+
async def on_organization_bootstrap(
251+
self,
252+
organization_id: OrganizationID,
253+
device_id: DeviceID,
254+
device_label: DeviceLabel,
255+
human_email: EmailAddress,
256+
human_label: str,
257+
) -> None:
258+
return None
259+
260+
async def sequester_service_on_vlob_create_or_update(
261+
self,
262+
webhook_url: str,
263+
service_id: SequesterServiceID,
264+
organization_id: OrganizationID,
265+
author: DeviceID,
266+
realm_id: VlobID,
267+
vlob_id: VlobID,
268+
key_index: int,
269+
version: int,
270+
timestamp: DateTime,
271+
blob: bytes,
272+
) -> SequesterServiceUnavailable | RejectedBySequesterService | None:
273+
return None
274+
275+
async def sequester_service_on_realm_rotate_key(
276+
self,
277+
webhook_url: str,
278+
service_id: SequesterServiceID,
279+
organization_id: OrganizationID,
280+
keys_bundle: bytes,
281+
keys_bundle_access: bytes,
282+
author: DeviceID,
283+
timestamp: DateTime,
284+
realm_id: VlobID,
285+
key_index: int,
286+
encryption_algorithm: SecretKeyAlgorithm,
287+
hash_algorithm: HashAlgorithm,
288+
key_canary: bytes,
289+
) -> SequesterServiceUnavailable | RejectedBySequesterService | None:
290+
return None
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from click.testing import CliRunner
2+
3+
from parsec.cli.tasks import list_organization
4+
from parsec.config import BaseDatabaseConfig, PostgreSQLDatabaseConfig
5+
6+
7+
def test_list_organization(db_config: BaseDatabaseConfig, db_args: list[str]):
8+
runner = CliRunner()
9+
args = db_args
10+
use_pg = isinstance(db_config, PostgreSQLDatabaseConfig)
11+
result = runner.invoke(list_organization.cmd, args, catch_exceptions=True)
12+
assert result.exception is None, result.stderr
13+
assert result.exit_code == 0
14+
assert result.stderr_bytes == b""
15+
if not use_pg:
16+
assert result.stdout == "{}\n"
17+
else:
18+
assert result.stdout.startswith("{\n")
19+
assert result.stdout.endswith("}\n")

server/tests/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,18 @@ async def db_config(request: pytest.FixtureRequest) -> BaseDatabaseConfig:
279279
return MockedDatabaseConfig()
280280

281281

282+
@pytest.fixture
283+
async def db_args(db_config: BaseDatabaseConfig) -> list[str]:
284+
if isinstance(db_config, PostgreSQLDatabaseConfig):
285+
return [
286+
f"--db={db_config.url}",
287+
f"--db-min-connections={db_config.min_connections}",
288+
f"--db-max-connections={db_config.max_connections}",
289+
]
290+
else:
291+
return ["--db=MOCKED"]
292+
293+
282294
@pytest.fixture
283295
def blockstore_config(db_config: BaseDatabaseConfig) -> BaseBlockStoreConfig:
284296
# TODO: allow to test against swift ?

0 commit comments

Comments
 (0)