Skip to content
Draft
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
2 changes: 2 additions & 0 deletions server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ name = "parsec"
crate-type = ["cdylib"]

[features]
default = ["pydantic-support"]
pydantic-support = []
# Remember kid: RustCrypto is used if `use-libsodium` is not set !
use-libsodium = ["libparsec_crypto/use-libsodium"]
vendored-openssl = ["libparsec_crypto/vendored-openssl"]
Expand Down
3 changes: 3 additions & 0 deletions server/parsec/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from parsec.cli.sequester_create import create_service, generate_service_certificate
from parsec.cli.sequester_list import list_services
from parsec.cli.sequester_revoke import generate_service_revocation_certificate, revoke_service
from parsec.cli.tasks import server_tasks_cmd_group
from parsec.cli.testbed import TESTBED_AVAILABLE, testbed_cmd

__all__ = ("cli",)
Expand Down Expand Up @@ -53,6 +54,8 @@ def cli() -> None:
cli.add_command(list_deletable_realms, "list_deletable_realms")
cli.add_command(delete_realm, "delete_realm")
cli.add_command(server_sequester_cmd, "sequester")
cli.add_command(server_tasks_cmd_group)

if TESTBED_AVAILABLE:
cli.add_command(testbed_cmd, "testbed")
# Since `render_email` is only for debugging purpose, we don't expose it
Expand Down
16 changes: 16 additions & 0 deletions server/parsec/cli/tasks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS

import click

from parsec.cli.options import version_option

from . import list_organization


@click.group(name="tasks", short_help="Server tasks collections")
@version_option
def server_tasks_cmd_group() -> None:
pass


server_tasks_cmd_group.add_command(list_organization.cmd)
133 changes: 133 additions & 0 deletions server/parsec/cli/tasks/list_organization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS
from __future__ import annotations

from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager

import click
import pydantic

from parsec._parsec import (
ActiveUsersLimit,
DateTime,
EmailAddress,
OrganizationID,
ParsecAddr,
SecretKey,
)
from parsec.cli.options import asyncio_run, db_server_options, logging_config_options
from parsec.components.memory.datamodel import MemoryDatamodel
from parsec.components.memory.events import event_bus_factory
from parsec.components.memory.organization import MemoryOrganizationComponent
from parsec.components.organization import (
BaseOrganizationComponent,
OrganizationDump,
TermsOfService,
)
from parsec.components.postgresql.handler import asyncpg_pool_factory
from parsec.components.postgresql.organization import PGOrganizationComponent
from parsec.config import (
BackendConfig,
BaseDatabaseConfig,
LogLevel,
MockedBlockStoreConfig,
MockedEmailConfig,
PostgreSQLDatabaseConfig,
)
from parsec.webhooks import MockedWebhooksComponent


@pydantic.dataclasses.dataclass()
class OrganizationInfo:
id: OrganizationID
created_on: DateTime
is_bootstrapped: bool
bootstrapped_on: DateTime | None
is_expired: bool
expired_on: DateTime | None
active_users_limit: ActiveUsersLimit
user_profile_outsider_allowed: bool
realm_minimum_archiving_period_before_deletion: int
tos: TermsOfService | None

@classmethod
def from_dump(cls, dump: OrganizationDump) -> OrganizationInfo:
return OrganizationInfo(
id=dump.organization_id,
created_on=dump.created_on,
bootstrapped_on=dump.bootstrapped_on,
is_bootstrapped=dump.is_bootstrapped,
expired_on=dump.expired_on,
is_expired=dump.is_expired,
user_profile_outsider_allowed=dump.user_profile_outsider_allowed,
active_users_limit=dump.active_users_limit,
realm_minimum_archiving_period_before_deletion=dump.realm_minimum_archiving_period_before_deletion,
tos=dump.tos,
)


@click.command(name="list-organizations", short_help="List organization present on the server")
@db_server_options
@logging_config_options(default_log_level="INFO")
@asyncio_run
async def cmd(
db: BaseDatabaseConfig,
db_min_connections: int,
db_max_connections: int,
log_level: LogLevel,
log_format: str,
log_file: str | None,
):
backend_config = BackendConfig(
debug=False,
db_config=db,
blockstore_config=MockedBlockStoreConfig(),
email_config=MockedEmailConfig(sender=EmailAddress("tasks@parsec.local")),
server_addr=ParsecAddr("tasks.parsec.local", None, True),
administration_token="",
fake_account_password_algorithm_seed=SecretKey.generate(),
)
async with organization_component_factory(backend_config) as component:
orgs = await list_organizations(component)

adapter = pydantic.TypeAdapter(
dict[
OrganizationID,
OrganizationInfo,
]
)
click.echo_via_pager(adapter.dump_json(orgs, indent=4).decode())


@asynccontextmanager
async def organization_component_factory(
config: BackendConfig,
) -> AsyncGenerator[BaseOrganizationComponent, None]:
if config.db_config.is_mocked():
data = MemoryDatamodel(
{} if config.backend_mocked_data is None else config.backend_mocked_data
)
async with event_bus_factory() as event_bus:
yield MemoryOrganizationComponent(data, event_bus, MockedWebhooksComponent(), config)

else:
assert isinstance(config.db_config, PostgreSQLDatabaseConfig)
async with asyncpg_pool_factory(
url=config.db_config.url,
min_connections=config.db_config.min_connections,
max_connections=config.db_config.max_connections,
) as pool:
yield PGOrganizationComponent(
pool=pool,
webhooks=MockedWebhooksComponent(),
config=None, # pyright: ignore [reportArgumentType]
)


async def list_organizations(
component: BaseOrganizationComponent,
) -> dict[OrganizationID, OrganizationInfo]:
return {
id: OrganizationInfo.from_dump(dump)
for id, dump in (await component.list_organizations()).items()
}
1 change: 1 addition & 0 deletions server/parsec/components/memory/datamodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ class MemoryOrganization:
sequester_authority_certificate: bytes | None = field(default=None, repr=False)
cooked_sequester_authority: SequesterAuthorityCertificate | None = None
root_verify_key: VerifyKey | None = field(default=None, repr=False)
expired_on: DateTime | None = None
is_expired: bool = False
realm_minimum_archiving_period_before_deletion: int = 2592000 # 30 days
tos: TermsOfService | None = None
Expand Down
7 changes: 6 additions & 1 deletion server/parsec/components/memory/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ async def get(self, id: OrganizationID) -> Organization | OrganizationGetBadOutc
organization_id=org.organization_id,
bootstrap_token=org.bootstrap_token,
is_expired=org.is_expired,
expired_on=org.expired_on,
created_on=org.created_on,
bootstrapped_on=org.bootstrapped_on,
root_verify_key=org.root_verify_key,
Expand Down Expand Up @@ -366,6 +367,7 @@ async def update(

if is_expired is not Unset:
org.is_expired = is_expired
org.expired_on = now if is_expired else None
if active_users_limit is not Unset:
org.active_users_limit = active_users_limit
if user_profile_outsider_allowed is not Unset:
Expand Down Expand Up @@ -402,7 +404,7 @@ async def get_tos(
return org.tos

@override
async def test_dump_organizations(
async def list_organizations(
self, skip_templates: bool = True
) -> dict[OrganizationID, OrganizationDump]:
items = {}
Expand All @@ -413,8 +415,11 @@ async def test_dump_organizations(
org.active_users_limit
items[org.organization_id] = OrganizationDump(
organization_id=org.organization_id,
created_on=org.created_on,
bootstrap_token=org.bootstrap_token,
bootstrapped_on=org.bootstrapped_on,
is_bootstrapped=org.is_bootstrapped,
expired_on=org.expired_on,
is_expired=org.is_expired,
active_users_limit=org.active_users_limit,
user_profile_outsider_allowed=org.user_profile_outsider_allowed,
Expand Down
6 changes: 5 additions & 1 deletion server/parsec/components/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,11 @@ class TermsOfService:
@dataclass(slots=True)
class OrganizationDump:
organization_id: OrganizationID
created_on: DateTime
bootstrap_token: AccessToken | None
bootstrapped_on: DateTime | None
is_bootstrapped: bool
expired_on: DateTime | None
is_expired: bool
active_users_limit: ActiveUsersLimit
user_profile_outsider_allowed: bool
Expand Down Expand Up @@ -171,6 +174,7 @@ class Organization:
organization_id: OrganizationID
bootstrap_token: AccessToken | None
is_expired: bool
expired_on: DateTime | None
created_on: DateTime
realm_minimum_archiving_period_before_deletion: int
bootstrapped_on: DateTime | None
Expand Down Expand Up @@ -321,7 +325,7 @@ async def get_tos(
) -> TermsOfService | OrganizationGetTosBadOutcome | None:
raise NotImplementedError

async def test_dump_organizations(
async def list_organizations(
self, skip_templates: bool = True
) -> dict[OrganizationID, OrganizationDump]:
raise NotImplementedError
Expand Down
12 changes: 7 additions & 5 deletions server/parsec/components/postgresql/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@
from parsec.components.postgresql.organization_bootstrap import organization_bootstrap
from parsec.components.postgresql.organization_create import organization_create
from parsec.components.postgresql.organization_get_tos import organization_get_tos
from parsec.components.postgresql.organization_list_organizations import (
organization_list_organizations,
)
from parsec.components.postgresql.organization_stats import (
organization_server_stats,
organization_stats,
)
from parsec.components.postgresql.organization_test_dump_organizations import (
organization_test_dump_organizations,
)
from parsec.components.postgresql.organization_test_dump_topics import organization_test_dump_topics
from parsec.components.postgresql.organization_update import organization_update
from parsec.components.postgresql.test_queries import (
Expand All @@ -69,6 +69,7 @@ def _make_q_get_organization(for_update: bool = False) -> Q:
bootstrap_token,
root_verify_key,
is_expired,
_expired_on AS expired_on,
_bootstrapped_on AS bootstrapped_on,
_created_on AS created_on,
active_users_limit,
Expand Down Expand Up @@ -247,6 +248,7 @@ async def _get(
bootstrap_token=bootstrap_token,
root_verify_key=rvk,
is_expired=row["is_expired"],
expired_on=row["expired_on"],
created_on=row["created_on"],
bootstrapped_on=row["bootstrapped_on"],
active_users_limit=ActiveUsersLimit.from_maybe_int(row["active_users_limit"]),
Expand Down Expand Up @@ -355,10 +357,10 @@ async def get_tos(

@override
@no_transaction
async def test_dump_organizations(
async def list_organizations(
self, conn: AsyncpgConnection, skip_templates: bool = True
) -> dict[OrganizationID, OrganizationDump]:
return await organization_test_dump_organizations(conn, skip_templates)
return await organization_list_organizations(conn, skip_templates)

@override
@no_transaction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@
_q_get_organizations = Q("""
SELECT
organization_id,
_created_on AS created_on,
bootstrap_token,
_bootstrapped_on AS bootstrapped_on,
is_expired,
_expired_on AS expired_on,
active_users_limit,
user_profile_outsider_allowed,
realm_minimum_archiving_period_before_deletion,
Expand All @@ -32,7 +35,7 @@
""")


async def organization_test_dump_organizations(
async def organization_list_organizations(
conn: AsyncpgConnection, skip_templates: bool = True
) -> dict[OrganizationID, OrganizationDump]:
items = {}
Expand All @@ -48,6 +51,12 @@ async def organization_test_dump_organizations(
if skip_templates and organization_id.str.endswith("Template"):
continue

match row["created_on"]:
case DateTime() as created_on:
pass
case _:
assert False, row

match row["bootstrap_token"]:
case str() as raw_bootstrap_token:
bootstrap_token = AccessToken.from_hex(raw_bootstrap_token)
Expand All @@ -62,12 +71,28 @@ async def organization_test_dump_organizations(
case _:
assert False, row

match row["bootstrapped_on"]:
case DateTime() as bootstrapped_on:
pass
case None:
bootstrapped_on = None
case _:
assert False, row

match row["is_expired"]:
case bool() as is_expired:
pass
case _:
assert False, row

match row["expired_on"]:
case DateTime() as expired_on:
pass
case None:
expired_on = None
case _:
assert False, row

match row["active_users_limit"]:
case None:
active_users_limit = ActiveUsersLimit.NO_LIMIT
Expand Down Expand Up @@ -103,9 +128,12 @@ async def organization_test_dump_organizations(

items[organization_id] = OrganizationDump(
organization_id=organization_id,
created_on=created_on,
bootstrap_token=bootstrap_token,
is_bootstrapped=is_bootstrapped,
bootstrapped_on=bootstrapped_on,
is_expired=is_expired,
expired_on=expired_on,
active_users_limit=active_users_limit,
user_profile_outsider_allowed=user_profile_outsider_allowed,
realm_minimum_archiving_period_before_deletion=realm_minimum_archiving_period_before_deletion,
Expand Down
Loading
Loading