Skip to content

Commit

Permalink
wip eula postgresql
Browse files Browse the repository at this point in the history
  • Loading branch information
touilleMan committed Sep 21, 2024
1 parent fc8eaeb commit 39ae05c
Show file tree
Hide file tree
Showing 18 changed files with 408 additions and 30 deletions.
9 changes: 9 additions & 0 deletions server/parsec/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,13 @@ def handle_parse_result(
default=True,
type=bool,
)
@click.option(
"--organization-initial-eula-url",
envvar="PARSEC_ORGANIZATION_INITIAL_EULA_URL",
show_envvar=True,
help="End User License Agreement used to configure newly created organizations (default: no EULA)",
default=None,
)
@click.option(
"--server-addr",
envvar="PARSEC_SERVER_ADDR",
Expand Down Expand Up @@ -324,6 +331,7 @@ def run_cmd(
organization_bootstrap_webhook: str | None,
organization_initial_active_users_limit: int | None,
organization_initial_user_profile_outsider_allowed: bool,
organization_initial_eula_url: str | None,
server_addr: ParsecAddr,
email_host: str,
email_port: int,
Expand Down Expand Up @@ -388,6 +396,7 @@ def run_cmd(
if organization_initial_active_users_limit is not None
else ActiveUsersLimit.NO_LIMIT,
organization_initial_user_profile_outsider_allowed=organization_initial_user_profile_outsider_allowed,
organization_initial_eula_url=organization_initial_eula_url,
)

click.echo(
Expand Down
3 changes: 2 additions & 1 deletion server/parsec/components/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,8 @@ async def authenticated_auth(
)
match outcome:
case AuthenticatedAuthInfo() as auth_info:
self._device_cache[(organization_id, token.device_id)] = auth_info
if not allow_not_accepted_eula:
self._device_cache[(organization_id, token.device_id)] = auth_info

case AuthAuthenticatedAuthBadOutcome.ORGANIZATION_NOT_FOUND:
# Cannot store cache as the organization might be created at anytime !
Expand Down
4 changes: 4 additions & 0 deletions server/parsec/components/memory/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ async def anonymous_auth(
user_profile_outsider_allowed=self._config.organization_initial_user_profile_outsider_allowed,
active_users_limit=self._config.organization_initial_active_users_limit,
minimum_archiving_period=self._config.organization_initial_minimum_archiving_period,
eula_url=self._config.organization_initial_eula_url,
eula_updated_on=None
if self._config.organization_initial_eula_url is None
else now,
created_on=now,
)
self._data.organizations[organization_id] = org
Expand Down
7 changes: 5 additions & 2 deletions server/parsec/components/memory/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,11 @@ async def create(
minimum_archiving_period = self._config.organization_initial_minimum_archiving_period
assert isinstance(minimum_archiving_period, int)
if eula is Unset:
eula_url = None
eula_updated_on = None
eula_url = self._config.organization_initial_eula_url
if eula_url is None:
eula_updated_on = None
else:
eula_updated_on = now
else:
eula_url = eula
eula_updated_on = now
Expand Down
63 changes: 47 additions & 16 deletions server/parsec/components/postgresql/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@
WITH my_organization AS (
SELECT
_id,
is_expired
is_expired,
eula_updated_on
FROM organization
WHERE
organization_id = $organization_id
Expand All @@ -91,28 +92,47 @@
WHERE
device.device_id = $device_id
LIMIT 1
),
my_user AS (
SELECT
user_id,
revoked_on,
frozen,
(
CASE WHEN user_.eula_accepted_on IS NULL
THEN
-- The user hasn't accepted the EULA, this is acceptable if there
-- is no EULA at all for this organization.
COALESCE(
(SELECT TRUE FROM my_organization WHERE eula_updated_on IS NOT NULL),
FALSE
)
ELSE
-- The user has accepted an EULA, we must make sure it corresponds to
-- the current one in the organization.
-- If the organization no longer has a EULA, then what the user has
-- accepted in the past is irrelevant.
COALESCE(
(user_.eula_accepted_on < (SELECT eula_updated_on FROM my_organization)),
FALSE
)
END
) AS user_must_accept_eula
FROM user_
INNER JOIN my_device ON user_._id = my_device.user_
LIMIT 1
)
SELECT
(SELECT _id FROM my_organization) as organization_internal_id,
(SELECT is_expired FROM my_organization) as organization_is_expired,
(SELECT _id FROM my_device) as device_internal_id,
(SELECT verify_key FROM my_device) as device_verify_key,
(
SELECT user_id
FROM user_
INNER JOIN my_device ON user_._id = my_device.user_
) as user_id,
(
SELECT revoked_on
FROM user_
INNER JOIN my_device ON user_._id = my_device.user_
) as user_revoked_on,
(
SELECT frozen
FROM user_
INNER JOIN my_device ON user_._id = my_device.user_
) as user_is_frozen
(SELECT user_id FROM my_user) as user_id,
(SELECT revoked_on FROM my_user) as user_revoked_on,
(SELECT frozen FROM my_user) as user_is_frozen,
(SELECT user_must_accept_eula FROM my_user)
"""
)

Expand Down Expand Up @@ -146,6 +166,7 @@ async def anonymous_auth(
active_users_limit=self._config.organization_initial_active_users_limit,
user_profile_outsider_allowed=self._config.organization_initial_user_profile_outsider_allowed,
minimum_archiving_period=self._config.organization_initial_minimum_archiving_period,
eula_url=self._config.organization_initial_eula_url,
bootstrap_token=None,
)
match outcome:
Expand Down Expand Up @@ -248,6 +269,7 @@ async def _get_authenticated_info(
conn: AsyncpgConnection,
organization_id: OrganizationID,
device_id: DeviceID,
allow_not_accepted_eula: bool,
) -> AuthenticatedAuthInfo | AuthAuthenticatedAuthBadOutcome:
row = await conn.fetchrow(
*_q_authenticated_get_info(organization_id=organization_id.str, device_id=device_id)
Expand Down Expand Up @@ -304,6 +326,15 @@ async def _get_authenticated_info(
case unknown:
assert False, repr(unknown)

if not allow_not_accepted_eula:
match row["user_must_accept_eula"]:
case False:
pass
case True:
return AuthAuthenticatedAuthBadOutcome.USER_MUST_ACCEPT_EULA
case unknown:
assert False, repr(unknown)

return AuthenticatedAuthInfo(
organization_id=organization_id,
user_id=user_id,
Expand Down
7 changes: 7 additions & 0 deletions server/parsec/components/postgresql/migrations/0006_eula.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS


ALTER TABLE organization ADD eula_updated_on TIMESTAMPTZ;
ALTER TABLE organization ADD eula_url TEXT;

ALTER TABLE user_ ADD eula_accepted_on TIMESTAMPTZ;
6 changes: 3 additions & 3 deletions server/parsec/components/postgresql/migrations/datamodel.sql
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ CREATE TABLE organization (
sequester_authority_verify_key_der BYTEA,
minimum_archiving_period INTEGER NOT NULL,
-- NULL if no End User License Agreement is set
eula_updated_on: TIMESTAMPTZ,
eula_updated_on TIMESTAMPTZ,
-- NULL if no End User License Agreement is set
eula_url: TEXT
eula_url TEXT
);

-------------------------------------------------------
Expand Down Expand Up @@ -106,7 +106,7 @@ CREATE TABLE user_ (
frozen BOOLEAN NOT NULL DEFAULT FALSE,
current_profile USER_PROFILE NOT NULL,
-- NULL if no End User License Agreement has been accepted
eula_accepted_on: TIMESTAMPTZ
eula_accepted_on TIMESTAMPTZ,

UNIQUE (organization, user_id)
);
Expand Down
13 changes: 12 additions & 1 deletion server/parsec/components/postgresql/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from parsec.components.postgresql import AsyncpgConnection, AsyncpgPool
from parsec.components.postgresql.organization_bootstrap import organization_bootstrap
from parsec.components.postgresql.organization_create import organization_create
from parsec.components.postgresql.organization_get_eula import organization_get_eula
from parsec.components.postgresql.organization_stats import (
organization_server_stats,
organization_stats,
Expand Down Expand Up @@ -123,6 +124,7 @@ async def create(
active_users_limit: Literal[UnsetType.Unset] | ActiveUsersLimit = Unset,
user_profile_outsider_allowed: Literal[UnsetType.Unset] | bool = Unset,
minimum_archiving_period: UnsetType | int = Unset,
eula: Literal[UnsetType.Unset] | str = Unset,
force_bootstrap_token: BootstrapToken | None = None,
) -> BootstrapToken | OrganizationCreateBadOutcome:
bootstrap_token = force_bootstrap_token or BootstrapToken.new()
Expand All @@ -134,6 +136,10 @@ async def create(
)
if minimum_archiving_period is Unset:
minimum_archiving_period = self._config.organization_initial_minimum_archiving_period
if eula is Unset:
eula_url = None
else:
eula_url = eula

outcome = await organization_create(
conn,
Expand All @@ -142,6 +148,7 @@ async def create(
active_users_limit,
user_profile_outsider_allowed,
minimum_archiving_period,
eula_url,
bootstrap_token,
)
match outcome:
Expand Down Expand Up @@ -273,28 +280,32 @@ async def server_stats(
async def update(
self,
conn: AsyncpgConnection,
now: DateTime,
id: OrganizationID,
is_expired: Literal[UnsetType.Unset] | bool = Unset,
active_users_limit: Literal[UnsetType.Unset] | ActiveUsersLimit = Unset,
user_profile_outsider_allowed: Literal[UnsetType.Unset] | bool = Unset,
minimum_archiving_period: Literal[UnsetType.Unset] | int = Unset,
eula: Literal[UnsetType.Unset] | None | str = Unset,
) -> None | OrganizationUpdateBadOutcome:
return await organization_update(
self.event_bus,
conn,
now,
id,
is_expired,
active_users_limit,
user_profile_outsider_allowed,
minimum_archiving_period,
eula,
)

@override
@no_transaction
async def get_eula(
self, conn: AsyncpgConnection, id: OrganizationID
) -> tuple[str, DateTime] | None | OrganizationGetEulaBadOutcome:
raise NotImplementedError
return await organization_get_eula(conn, id)

@override
@no_transaction
Expand Down
17 changes: 14 additions & 3 deletions server/parsec/components/postgresql/organization_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
_bootstrapped_on,
is_expired,
_expired_on,
minimum_archiving_period
minimum_archiving_period,
eula_updated_on,
eula_url
)
VALUES (
$organization_id,
Expand All @@ -38,7 +40,12 @@
NULL,
FALSE,
NULL,
$minimum_archiving_period
$minimum_archiving_period,
CASE WHEN $eula_url::TEXT IS NULL
THEN NULL::TIMESTAMPTZ
ELSE $created_on
END,
$eula_url
)
-- If the organization exists but hasn't been bootstrapped yet, we can
-- simply overwrite it.
Expand All @@ -50,7 +57,9 @@
_created_on = EXCLUDED._created_on,
is_expired = EXCLUDED.is_expired,
_expired_on = EXCLUDED._expired_on,
minimum_archiving_period = EXCLUDED.minimum_archiving_period
minimum_archiving_period = EXCLUDED.minimum_archiving_period,
eula_updated_on = EXCLUDED.eula_updated_on,
eula_url = EXCLUDED.eula_url
WHERE organization.root_verify_key IS NULL
RETURNING _id
),
Expand Down Expand Up @@ -81,6 +90,7 @@ async def organization_create(
active_users_limit: ActiveUsersLimit,
user_profile_outsider_allowed: bool,
minimum_archiving_period: int,
eula_url: str | None,
bootstrap_token: BootstrapToken | None,
) -> int | OrganizationCreateBadOutcome:
organization_internal_id = await conn.fetchval(
Expand All @@ -93,6 +103,7 @@ async def organization_create(
user_profile_outsider_allowed=user_profile_outsider_allowed,
created_on=now,
minimum_archiving_period=minimum_archiving_period,
eula_url=eula_url,
)
)
match organization_internal_id:
Expand Down
52 changes: 52 additions & 0 deletions server/parsec/components/postgresql/organization_get_eula.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS
from __future__ import annotations

from parsec._parsec import (
DateTime,
OrganizationID,
)
from parsec.components.organization import (
OrganizationGetEulaBadOutcome,
)
from parsec.components.postgresql import AsyncpgConnection
from parsec.components.postgresql.utils import (
Q,
)

_q_get_eula = Q("""
SELECT
is_expired,
eula_updated_on,
eula_url
FROM organization
WHERE organization_id = $organization_id
LIMIT 1
""")


async def organization_get_eula(
conn: AsyncpgConnection,
id: OrganizationID,
) -> tuple[str, DateTime] | None | OrganizationGetEulaBadOutcome:
row = await conn.fetchrow(*_q_get_eula(organization_id=id.str))

if not row:
return OrganizationGetEulaBadOutcome.ORGANIZATION_NOT_FOUND

match row["is_expired"]:
case True:
return OrganizationGetEulaBadOutcome.ORGANIZATION_EXPIRED
case False:
pass
case unknown:
assert False, unknown

match (row["eula_updated_on"], row["eula_url"]):
case (None, None):
eula = None
case (DateTime() as eula_updated_on, str() as eula_url):
eula = (eula_url, eula_updated_on)
case unknown:
assert False, unknown

return eula
Loading

0 comments on commit 39ae05c

Please sign in to comment.