This plugin implement data policy parsing and an algorithm to calculate a score based on them. rel 460
742 lines
24 KiB
Python
742 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# Libervia plugin for handling stateless file sharing encryption
|
|
# Copyright (C) 2009-2025 Jérôme Poisson (goffi@goffi.org)
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
from enum import Enum, StrEnum, auto
|
|
import enum
|
|
from typing import TYPE_CHECKING, Self, cast
|
|
from typing import get_type_hints
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, computed_field
|
|
from twisted.internet import defer
|
|
from twisted.words.protocols.jabber import jid
|
|
from wokkel import data_form
|
|
|
|
from libervia.backend.core import exceptions
|
|
from libervia.backend.core.constants import Const as C
|
|
from libervia.backend.core.core_types import SatXMPPEntity
|
|
from libervia.backend.core.i18n import D_, _
|
|
from libervia.backend.core.log import getLogger
|
|
|
|
if TYPE_CHECKING:
|
|
from libervia.backend.core.main import LiberviaBackend
|
|
|
|
log = getLogger(__name__)
|
|
IMPORT_NAME = "DATA-POLICY"
|
|
|
|
PLUGIN_INFO = {
|
|
C.PI_NAME: "Data Policy",
|
|
C.PI_IMPORT_NAME: IMPORT_NAME,
|
|
C.PI_TYPE: C.PLUG_TYPE_EXP,
|
|
C.PI_PROTOCOLS: [],
|
|
C.PI_DEPENDENCIES: [],
|
|
C.PI_MAIN: "DATA_POLICY",
|
|
C.PI_HANDLER: "no",
|
|
}
|
|
|
|
NS_DATA_POLICY_BASE = "urn:xmpp:data-policy"
|
|
NS_DATA_POLICY = f"{NS_DATA_POLICY_BASE}:0"
|
|
NS_DATA_POLICY_ID_PREFIX = f"{NS_DATA_POLICY_BASE}:identity:"
|
|
NS_DATA_POLICY_ID_SUFFIX = ":0"
|
|
NS_DATA_POLICY_ID_TPL = (
|
|
f"{NS_DATA_POLICY_ID_PREFIX}{{category}}:{{type}}{NS_DATA_POLICY_ID_SUFFIX}"
|
|
)
|
|
|
|
|
|
class IndividualScore(BaseModel):
|
|
score: int
|
|
description: str
|
|
|
|
|
|
class Score(BaseModel):
|
|
score: int
|
|
minimum: int
|
|
maximum: int
|
|
detail: list[IndividualScore]
|
|
|
|
|
|
class ScoredStrEnum(StrEnum):
|
|
_score_map = enum.nonmember({})
|
|
_min_score = enum.nonmember(0)
|
|
_max_score = enum.nonmember(0)
|
|
|
|
@classmethod
|
|
def get_score(cls, value: str) -> Score:
|
|
score, description = cls._score_map[value]
|
|
return Score(
|
|
score=score,
|
|
minimum=cls._min_score,
|
|
maximum=cls._max_score,
|
|
detail=[IndividualScore(score=score, description=description)],
|
|
)
|
|
|
|
def __init_subclass__(cls) -> None:
|
|
try:
|
|
score_map = cls._score_map
|
|
except AttributeError:
|
|
raise exceptions.InternalError('"_score_map" must be set.')
|
|
if not score_map:
|
|
raise exceptions.InternalError("ScoredEnum must set _score_map.")
|
|
|
|
if set(score_map.keys()) != set(cls):
|
|
raise exceptions.InternalError(
|
|
"All enum members must be present in _score_map."
|
|
)
|
|
|
|
all_scores = [score for score, _ in score_map.values()]
|
|
cls._min_score = min(all_scores)
|
|
cls._max_score = max(all_scores)
|
|
|
|
|
|
class AuthMechanism(ScoredStrEnum):
|
|
NO_AUTH = auto()
|
|
PLAIN = auto()
|
|
HIDDEN = auto()
|
|
RESTRICTED = auto()
|
|
|
|
_score_map = enum.nonmember(
|
|
{
|
|
NO_AUTH: (20, D_("No authentication is needed.")),
|
|
PLAIN: (-20, D_("Your login data are transmitted to this service.")),
|
|
HIDDEN: (
|
|
0,
|
|
D_("This service logs to your account, but doesn't get logging data."),
|
|
),
|
|
RESTRICTED: (
|
|
15,
|
|
D_("This service logs to your account in a restricted way."),
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
class DataTransmission(ScoredStrEnum):
|
|
PLAIN = auto()
|
|
ENCRYPTED = auto()
|
|
E2E = auto()
|
|
GRE = auto()
|
|
|
|
_score_map = enum.nonmember(
|
|
{
|
|
PLAIN: (
|
|
-20,
|
|
D_(
|
|
"Data is transmitted without encryption. This is highly insecure and "
|
|
"risks data interception."
|
|
),
|
|
),
|
|
ENCRYPTED: (
|
|
0,
|
|
D_(
|
|
"Data is encrypted during transmission but not end-to-end. The "
|
|
"service can view the data."
|
|
),
|
|
),
|
|
E2E: (
|
|
10,
|
|
D_(
|
|
"Data is end-to-end encrypted from the service. Only the service and "
|
|
"the recipient(s) can view the data."
|
|
),
|
|
),
|
|
GRE: (
|
|
30,
|
|
D_(
|
|
"Data uses Gateway Relayed Encryption, ensuring end-to-end security, "
|
|
"only your and your recipient(s) can view the data. Highly secure."
|
|
),
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
class AccessPolicy(ScoredStrEnum):
|
|
ADMINS = auto()
|
|
MODERATORS = auto()
|
|
ORGANIZATION_MEMBER = auto()
|
|
GOVERNMENT = auto()
|
|
ADVERTISERS = auto()
|
|
PARTNERS = auto()
|
|
NONE = auto()
|
|
|
|
_score_map = enum.nonmember(
|
|
{
|
|
ADMINS: (
|
|
-5,
|
|
D_(
|
|
"Service administrators can access user data for operational "
|
|
"purposes."
|
|
),
|
|
),
|
|
MODERATORS: (
|
|
-10,
|
|
D_("Moderators can access user data within their moderation scope."),
|
|
),
|
|
ORGANIZATION_MEMBER: (
|
|
-15,
|
|
D_("Any organization member can access user data."),
|
|
),
|
|
GOVERNMENT: (
|
|
-10,
|
|
D_(
|
|
"Government authorities can access user data under legal "
|
|
"requirements."
|
|
),
|
|
),
|
|
ADVERTISERS: (
|
|
-30,
|
|
D_("Third-party advertisers can access user data for targeted ads."),
|
|
),
|
|
PARTNERS: (
|
|
-20,
|
|
D_("Business partners can access user data under agreements."),
|
|
),
|
|
NONE: (20, D_("No entity other than the user can access user data.")),
|
|
}
|
|
)
|
|
|
|
|
|
class DataPolicy(BaseModel):
|
|
"""Represents a data policy form as defined in Data Policy XEP.
|
|
|
|
Fields correspond to the data policy specification and may be None when not provided.
|
|
"""
|
|
|
|
model_config = ConfigDict(use_enum_values=True)
|
|
|
|
auth_data: AuthMechanism | None = None
|
|
data_transmission: DataTransmission | None = None
|
|
encryption_algorithm: str | None = None
|
|
data_retention: str | None = None
|
|
data_deletion: bool | None = None
|
|
encryption_at_rest: bool | None = None
|
|
tos: str | None = None
|
|
data_export: bool | None = None
|
|
access_policy: set[AccessPolicy] | None = None
|
|
full_erasure: bool | None = None
|
|
backup_frequency: str | None = None
|
|
backup_retention: str | None = None
|
|
extra_info: str | None = None
|
|
|
|
@computed_field
|
|
@property
|
|
def score(self) -> Score:
|
|
"""Calculate a score based on the filled fields.
|
|
|
|
This score helps assess the quality of the data policy at a glance.
|
|
"""
|
|
total_score = 0
|
|
overall_min = 0
|
|
overall_max = 0
|
|
detail = []
|
|
|
|
fields_names = set(self.__class__.model_fields.keys())
|
|
|
|
for field_name in fields_names:
|
|
score = getattr(self, f"_{field_name}_score")
|
|
if score is not None:
|
|
total_score += score.score
|
|
overall_min += score.minimum
|
|
overall_max += score.maximum
|
|
detail.extend(score.detail)
|
|
|
|
return Score(
|
|
score=total_score, minimum=overall_min, maximum=overall_max, detail=detail
|
|
)
|
|
|
|
@property
|
|
def _auth_data_score(self) -> Score | None:
|
|
if self.auth_data is None:
|
|
return None
|
|
|
|
return AuthMechanism.get_score(self.auth_data)
|
|
|
|
@property
|
|
def _data_transmission_score(self) -> Score | None:
|
|
if self.data_transmission is None:
|
|
return None
|
|
|
|
return DataTransmission.get_score(self.data_transmission)
|
|
|
|
@property
|
|
def _encryption_algorithm_score(self) -> Score | None:
|
|
ENCRYPTION_ALGORITHM_SET = (10, D_("The encryption algorithm is {}."))
|
|
ENCRYPTION_ALGORITHM_UNSET = (
|
|
-10,
|
|
D_("The encryption algorithm is not specified."),
|
|
)
|
|
ALL = (ENCRYPTION_ALGORITHM_SET, ENCRYPTION_ALGORITHM_UNSET)
|
|
if not self.data_transmission or self.data_transmission not in (
|
|
DataTransmission.E2E,
|
|
DataTransmission.ENCRYPTED,
|
|
):
|
|
return None
|
|
|
|
if self.encryption_algorithm:
|
|
score, desc = ENCRYPTION_ALGORITHM_SET
|
|
desc = desc.format(self.encryption_algorithm)
|
|
else:
|
|
score, desc = ENCRYPTION_ALGORITHM_UNSET
|
|
|
|
return Score(
|
|
score=0,
|
|
minimum=min(score for score, _ in ALL),
|
|
maximum=max(score for score, _ in ALL),
|
|
detail=[IndividualScore(score=score, description=desc)],
|
|
)
|
|
|
|
@property
|
|
def _data_retention_score(self) -> Score | None:
|
|
if self.data_retention is None:
|
|
return None
|
|
|
|
DATA_RETENTION_0 = (10, D_("The service does not store data."))
|
|
DATA_RETENTION_INFINITE = (
|
|
-15,
|
|
D_("Data is stored indefinitely, which may pose privacy risks."),
|
|
)
|
|
DATA_RETENTION_UNKNOWN = (-10, D_("Data retention policy is unknown."))
|
|
DATA_RETENTION_DEFAULT = (0, D_("Data is stored for {days:.02f} day(s)."))
|
|
DATA_RETENTION_INVALID = (-20, D_("Invalid data retention policy ({value!r})."))
|
|
ALL = [
|
|
DATA_RETENTION_0,
|
|
DATA_RETENTION_INFINITE,
|
|
DATA_RETENTION_UNKNOWN,
|
|
DATA_RETENTION_DEFAULT,
|
|
DATA_RETENTION_INVALID,
|
|
]
|
|
|
|
value = self.data_retention
|
|
if value == "0":
|
|
score, desc = DATA_RETENTION_0
|
|
elif value == "infinite":
|
|
score, desc = DATA_RETENTION_INFINITE
|
|
elif value == "unknown":
|
|
score, desc = DATA_RETENTION_UNKNOWN
|
|
else:
|
|
try:
|
|
hours = int(value)
|
|
days = hours / 24
|
|
desc = DATA_RETENTION_DEFAULT[1].format(days=days)
|
|
score = DATA_RETENTION_DEFAULT[0]
|
|
except ValueError:
|
|
score, desc = DATA_RETENTION_INVALID
|
|
|
|
return Score(
|
|
score=score,
|
|
minimum=min(score for score, _ in ALL),
|
|
maximum=max(score for score, _ in ALL),
|
|
detail=[IndividualScore(score=score, description=desc)],
|
|
)
|
|
|
|
@property
|
|
def _data_deletion_score(self) -> Score | None:
|
|
if self.data_deletion is None:
|
|
return None
|
|
|
|
DATA_DELETION_TRUE = (20, D_("Users can delete data on this service."))
|
|
DATA_DELETION_FALSE = (-10, D_("Users cannot delete data on this service."))
|
|
ALL = [DATA_DELETION_TRUE, DATA_DELETION_FALSE]
|
|
|
|
score, desc = DATA_DELETION_TRUE if self.data_deletion else DATA_DELETION_FALSE
|
|
|
|
return Score(
|
|
score=score,
|
|
minimum=min(score for score, _ in ALL),
|
|
maximum=max(score for score, _ in ALL),
|
|
detail=[IndividualScore(score=score, description=desc)],
|
|
)
|
|
|
|
@property
|
|
def _encryption_at_rest_score(self) -> Score | None:
|
|
if self.encryption_at_rest is None:
|
|
return None
|
|
|
|
ENCRYPTION_AT_REST_TRUE = (10, D_("Data is encrypted at rest."))
|
|
ENCRYPTION_AT_REST_FALSE = (-5, D_("Data is not encrypted at rest."))
|
|
ALL = [ENCRYPTION_AT_REST_TRUE, ENCRYPTION_AT_REST_FALSE]
|
|
|
|
score, desc = (
|
|
ENCRYPTION_AT_REST_TRUE
|
|
if self.encryption_at_rest
|
|
else ENCRYPTION_AT_REST_FALSE
|
|
)
|
|
|
|
return Score(
|
|
score=score,
|
|
minimum=min(score for score, _ in ALL),
|
|
maximum=max(score for score, _ in ALL),
|
|
detail=[IndividualScore(score=score, description=desc)],
|
|
)
|
|
|
|
@property
|
|
def _tos_score(self) -> Score:
|
|
TOS_SET = (5, D_("Terms of Service are linked."))
|
|
TOS_UNSET = (-5, D_("Terms of Service are not linked."))
|
|
ALL = [TOS_SET, TOS_UNSET]
|
|
score, desc = TOS_SET if self.tos else TOS_UNSET
|
|
|
|
return Score(
|
|
score=score,
|
|
minimum=min(score for score, _ in ALL),
|
|
maximum=max(score for score, _ in ALL),
|
|
detail=[IndividualScore(score=score, description=desc)],
|
|
)
|
|
|
|
@property
|
|
def _data_export_score(self) -> Score | None:
|
|
if self.data_export is None:
|
|
return None
|
|
|
|
DATA_EXPORT_TRUE = (15, D_("Users can export their data."))
|
|
DATA_EXPORT_FALSE = (-10, D_("Users cannot export their data."))
|
|
ALL = [DATA_EXPORT_TRUE, DATA_EXPORT_FALSE]
|
|
|
|
score, desc = DATA_EXPORT_TRUE if self.data_export else DATA_EXPORT_FALSE
|
|
|
|
return Score(
|
|
score=score,
|
|
minimum=min(score for score, _ in ALL),
|
|
maximum=max(score for score, _ in ALL),
|
|
detail=[IndividualScore(score=score, description=desc)],
|
|
)
|
|
|
|
@property
|
|
def _full_erasure_score(self) -> Score | None:
|
|
if self.full_erasure is None:
|
|
return None
|
|
|
|
FULL_ERASURE_TRUE = (20, D_("Users can fully erase their account and data."))
|
|
FULL_ERASURE_FALSE = (-20, D_("Users cannot fully erase their account and data."))
|
|
ALL = [FULL_ERASURE_TRUE, FULL_ERASURE_FALSE]
|
|
|
|
score, desc = FULL_ERASURE_TRUE if self.full_erasure else FULL_ERASURE_FALSE
|
|
|
|
return Score(
|
|
score=score,
|
|
minimum=min(score for score, _ in ALL),
|
|
maximum=max(score for score, _ in ALL),
|
|
detail=[IndividualScore(score=score, description=desc)],
|
|
)
|
|
|
|
@property
|
|
def _backup_frequency_score(self) -> Score | None:
|
|
if self.backup_frequency is None:
|
|
return None
|
|
|
|
BACKUP_FREQUENCY_0 = (0, D_("The service does not do backups."))
|
|
BACKUP_FREQUENCY_DEFAULT = (5, D_("Backups are done every {days:.02f} day(s)."))
|
|
BACKUP_FREQUENCY_INVALID = (-20, D_("Invalid backup frequency {value!r}."))
|
|
ALL = [
|
|
BACKUP_FREQUENCY_0,
|
|
BACKUP_FREQUENCY_DEFAULT,
|
|
BACKUP_FREQUENCY_INVALID,
|
|
]
|
|
|
|
value = self.backup_frequency
|
|
if value == "0":
|
|
score, desc = BACKUP_FREQUENCY_0
|
|
else:
|
|
try:
|
|
hours = int(value)
|
|
days = hours / 24
|
|
desc = BACKUP_FREQUENCY_DEFAULT[1].format(days=days)
|
|
score = BACKUP_FREQUENCY_DEFAULT[0]
|
|
except ValueError:
|
|
score, desc = BACKUP_FREQUENCY_INVALID
|
|
|
|
return Score(
|
|
score=score,
|
|
minimum=min(score for score, _ in ALL),
|
|
maximum=max(score for score, _ in ALL),
|
|
detail=[IndividualScore(score=score, description=desc)],
|
|
)
|
|
|
|
@property
|
|
def _backup_retention_score(self) -> Score | None:
|
|
if self.backup_retention is None:
|
|
return None
|
|
|
|
BACKUP_RETENTION_0 = (0, D_("No backups are done."))
|
|
BACKUP_RETENTION_INFINITE = (-10, D_("Backups are stored indefinitely."))
|
|
BACKUP_RETENTION_UNKNOWN = (-5, D_("Backup retention policy is unknown."))
|
|
BACKUP_RETENTION_DEFAULT = (0, D_("Backups are kept for {days:.02f} day(s)."))
|
|
BACKUP_RETENTION_INVALID = (-20, D_("Invalid backup retention {value!r}."))
|
|
ALL = [
|
|
BACKUP_RETENTION_0,
|
|
BACKUP_RETENTION_INFINITE,
|
|
BACKUP_RETENTION_UNKNOWN,
|
|
BACKUP_RETENTION_DEFAULT,
|
|
BACKUP_RETENTION_INVALID,
|
|
]
|
|
|
|
value = self.backup_retention
|
|
if value == "0":
|
|
score, desc = BACKUP_RETENTION_0
|
|
elif value == "infinite":
|
|
score, desc = BACKUP_RETENTION_INFINITE
|
|
elif value == "unknown":
|
|
score, desc = BACKUP_RETENTION_UNKNOWN
|
|
else:
|
|
try:
|
|
hours = int(value)
|
|
days = hours / 24
|
|
desc = BACKUP_RETENTION_DEFAULT[1].format(days=days)
|
|
score = BACKUP_RETENTION_DEFAULT[0]
|
|
except ValueError:
|
|
score, desc = BACKUP_RETENTION_INVALID
|
|
|
|
return Score(
|
|
score=score,
|
|
minimum=min(score for score, _ in ALL),
|
|
maximum=max(score for score, _ in ALL),
|
|
detail=[IndividualScore(score=score, description=desc)],
|
|
)
|
|
|
|
@property
|
|
def _access_policy_score(self) -> Score | None:
|
|
if self.access_policy is None:
|
|
return None
|
|
|
|
total_score = 0
|
|
overall_min = 0
|
|
overall_max = 0
|
|
details = []
|
|
|
|
for policy in self.access_policy:
|
|
policy_score = AccessPolicy.get_score(policy)
|
|
total_score += policy_score.score
|
|
overall_min += policy_score.minimum
|
|
overall_max += policy_score.maximum
|
|
details.extend(policy_score.detail)
|
|
|
|
return Score(
|
|
score=total_score, minimum=overall_min, maximum=overall_max, detail=details
|
|
)
|
|
|
|
@property
|
|
def _extra_info_score(self) -> Score | None:
|
|
if self.extra_info is None:
|
|
return None
|
|
|
|
return Score(score=0, minimum=0, maximum=0, detail=[])
|
|
|
|
@classmethod
|
|
def from_data_form(cls, form: data_form.Form) -> "DataPolicy | None":
|
|
"""Create a DataPolicy instance from a Wokkel Data Form.
|
|
|
|
@param form: The data form to parse.
|
|
@return: Parsed DataPolicy instance or None if form type doesn't match.
|
|
"""
|
|
if not form.formNamespace or not form.formNamespace.startswith(
|
|
NS_DATA_POLICY_BASE
|
|
):
|
|
return None
|
|
|
|
fields = cls.model_fields.keys()
|
|
kwargs = {}
|
|
|
|
for name in fields:
|
|
if name not in form:
|
|
continue
|
|
|
|
value = form.get(name)
|
|
|
|
match name:
|
|
case "access_policy":
|
|
if not value:
|
|
continue
|
|
|
|
policy_set = set()
|
|
policies = [value] if not isinstance(value, list) else value
|
|
for policy in policies:
|
|
if policy:
|
|
policy_set.add(AccessPolicy(policy))
|
|
if policy_set:
|
|
kwargs[name] = policy_set
|
|
|
|
case "auth_data":
|
|
if value:
|
|
kwargs[name] = AuthMechanism(value)
|
|
|
|
case "data_transmission":
|
|
if value:
|
|
kwargs[name] = DataTransmission(value)
|
|
|
|
case "extra_info":
|
|
if isinstance(value, list):
|
|
kwargs[name] = "\n".join(str(line) for line in value)
|
|
else:
|
|
kwargs[name] = str(value)
|
|
|
|
case _:
|
|
kwargs[name] = value
|
|
|
|
return cls(**kwargs)
|
|
|
|
def to_data_form(
|
|
self, category: str | None = None, type_: str | None = None
|
|
) -> data_form.Form:
|
|
"""Convert this model to a Wokkel Data Form.
|
|
|
|
@return: Form with type='result' containing all non-None fields.
|
|
"""
|
|
if category is not None:
|
|
if type_ is not None:
|
|
raise exceptions.InternalError(
|
|
'If "category" is set, "type_" must be set too.'
|
|
)
|
|
form_ns = NS_DATA_POLICY_ID_TPL.format(category=category, type=type_)
|
|
else:
|
|
form_ns = NS_DATA_POLICY
|
|
form_fields = []
|
|
|
|
for name in self.__class__.model_fields.keys():
|
|
value = getattr(self, name)
|
|
if value is None:
|
|
continue
|
|
|
|
match name:
|
|
case "auth_data" | "data_transmission":
|
|
form_fields.append(
|
|
data_form.Field(
|
|
fieldType="list-single",
|
|
var=name,
|
|
value=value.value,
|
|
)
|
|
)
|
|
case (
|
|
"data_deletion"
|
|
| "encryption_at_rest"
|
|
| "data_export"
|
|
| "full_erasure"
|
|
):
|
|
form_fields.append(
|
|
data_form.Field(
|
|
fieldType="boolean",
|
|
var=name,
|
|
value=value,
|
|
)
|
|
)
|
|
case "access_policy":
|
|
field_values = [policy.value for policy in value]
|
|
form_fields.append(
|
|
data_form.Field(
|
|
fieldType="list-multi",
|
|
var=name,
|
|
values=field_values,
|
|
)
|
|
)
|
|
case "extra_info":
|
|
lines = value.split("\n")
|
|
form_fields.append(
|
|
data_form.Field(
|
|
fieldType="text-multi",
|
|
var=name,
|
|
values=lines,
|
|
)
|
|
)
|
|
case _:
|
|
form_fields.append(
|
|
data_form.Field(
|
|
fieldType="text-single",
|
|
var=name,
|
|
value=str(value),
|
|
)
|
|
)
|
|
|
|
return data_form.Form(
|
|
formType="result",
|
|
formNamespace=form_ns,
|
|
fields=form_fields,
|
|
)
|
|
|
|
|
|
class DataPolicies(BaseModel):
|
|
main: DataPolicy
|
|
services: dict[str, DataPolicy] = Field(
|
|
default=dict(),
|
|
description=(
|
|
"Identity to data policy map. Identity is used as key with the "
|
|
'template "{identity}:{type}".'
|
|
),
|
|
)
|
|
|
|
|
|
class DATA_POLICY:
|
|
namespace = NS_DATA_POLICY
|
|
|
|
def __init__(self, host: "LiberviaBackend") -> None:
|
|
log.info(f"plugin {PLUGIN_INFO[C.PI_NAME]!r} initialization")
|
|
self.host = host
|
|
host.register_namespace("data-policy", NS_DATA_POLICY)
|
|
host.bridge.add_method(
|
|
"data_policy_get",
|
|
".plugin",
|
|
in_sign="ss",
|
|
out_sign="s",
|
|
method=self._data_policy_get,
|
|
async_=True,
|
|
)
|
|
|
|
def _data_policy_get(self, target: str, profile_key: str) -> defer.Deferred[str]:
|
|
client = self.host.get_client(profile_key)
|
|
d = defer.ensureDeferred(self.get_data_policy(client, jid.JID(target)))
|
|
d.addCallback(
|
|
lambda data_policies: (
|
|
"" if data_policies is None else data_policies.model_dump_json()
|
|
)
|
|
)
|
|
d = cast(defer.Deferred[str], d)
|
|
return d
|
|
|
|
async def get_data_policy(
|
|
self, client: SatXMPPEntity, target_jid: jid.JID
|
|
) -> DataPolicies | None:
|
|
infos = await self.host.memory.disco.get_infos(client, target_jid)
|
|
|
|
if NS_DATA_POLICY not in infos.extensions:
|
|
return None
|
|
|
|
# Main data policy.
|
|
data_policy = DataPolicy.from_data_form(infos.extensions[NS_DATA_POLICY])
|
|
if data_policy is None:
|
|
log.error(f"DataPolicy should be found at this point.")
|
|
return None
|
|
|
|
data_policies = DataPolicies(main=data_policy)
|
|
|
|
# Now we looks for identities data policies.
|
|
for namespace, form in infos.extensions.items():
|
|
if namespace.startswith(NS_DATA_POLICY_ID_PREFIX) and namespace.endswith(
|
|
NS_DATA_POLICY_ID_SUFFIX
|
|
):
|
|
identity_data = namespace[
|
|
len(NS_DATA_POLICY_ID_PREFIX) : -len(NS_DATA_POLICY_ID_SUFFIX)
|
|
]
|
|
try:
|
|
category, type_ = identity_data.split(":", 1)
|
|
except ValueError:
|
|
log.warning(
|
|
"Invalid namespace for identity data policy: " f"{namespace!r}"
|
|
)
|
|
else:
|
|
id_data_policy = DataPolicy.from_data_form(form)
|
|
if id_data_policy is not None:
|
|
data_policies.services[f"{category}:{type_}"] = id_data_policy
|
|
return data_policies
|