terms.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. # Copyright 2019 The Matrix.org Foundation C.I.C.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import logging
  15. from typing import TYPE_CHECKING, Dict, List, Mapping, Optional, Set, Union
  16. import yaml
  17. from typing_extensions import TypedDict
  18. if TYPE_CHECKING:
  19. from sydent.sydent import Sydent
  20. logger = logging.getLogger(__name__)
  21. class TermConfig(TypedDict):
  22. master_version: str
  23. docs: Mapping[str, "Policy"]
  24. class Policy(TypedDict):
  25. version: str
  26. langs: Mapping[str, "LocalisedPolicy"]
  27. class LocalisedPolicy(TypedDict):
  28. name: str
  29. url: str
  30. VersionOrLang = Union[str, LocalisedPolicy]
  31. class Terms:
  32. def __init__(self, yamlObj: Optional[TermConfig]) -> None:
  33. """
  34. :param yamlObj: The parsed YAML.
  35. """
  36. self._rawTerms = yamlObj
  37. def getMasterVersion(self) -> Optional[str]:
  38. """
  39. :return: The global (master) version of the terms, or None if there
  40. are no terms of service for this server.
  41. """
  42. if self._rawTerms is None:
  43. return None
  44. return self._rawTerms["master_version"]
  45. def getForClient(self) -> Dict[str, Dict[str, Dict[str, VersionOrLang]]]:
  46. # Examples:
  47. # "policy" -> "terms_of_service", "version" -> "1.2.3"
  48. # "policy" -> "terms_of_service", "en" -> LocalisedPolicy
  49. """
  50. :return: A dict which value for the "policies" key is a dict which contains the
  51. "docs" part of the terms' YAML. That nested dict is empty if no terms.
  52. """
  53. policies: Dict[str, Dict[str, VersionOrLang]] = {}
  54. if self._rawTerms is not None:
  55. for docName, doc in self._rawTerms["docs"].items():
  56. policies[docName] = {
  57. "version": doc["version"],
  58. }
  59. policies[docName].update(doc["langs"])
  60. return {"policies": policies}
  61. def getUrlSet(self) -> Set[str]:
  62. """
  63. :return: All the URLs for the terms in a set. Empty set if no terms.
  64. """
  65. urls = set()
  66. if self._rawTerms is not None:
  67. for docName, doc in self._rawTerms["docs"].items():
  68. for langName, lang in doc["langs"].items():
  69. url = lang["url"]
  70. urls.add(url)
  71. return urls
  72. def urlListIsSufficient(self, urls: List[str]) -> bool:
  73. """
  74. Checks whether the provided list of URLs (which represents the list of terms
  75. accepted by the user) is enough to allow the creation of the user's account.
  76. :param urls: The list of URLs of terms the user has accepted.
  77. :return: Whether the list is sufficient to allow the creation of the user's
  78. account.
  79. """
  80. agreed = set()
  81. urlset = set(urls)
  82. if self._rawTerms is None:
  83. if urls:
  84. raise ValueError("No configured terms, but user accepted some terms")
  85. else:
  86. return True
  87. else:
  88. for docName, doc in self._rawTerms["docs"].items():
  89. for lang in doc["langs"].values():
  90. if lang["url"] in urlset:
  91. agreed.add(docName)
  92. break
  93. required = set(self._rawTerms["docs"].keys())
  94. return agreed == required
  95. def get_terms(sydent: "Sydent") -> Terms:
  96. """Read and parse terms as specified in the config.
  97. Errors in reading, parsing and validating the config
  98. are raised as exceptions."""
  99. # TODO - move some of this to parse_config
  100. termsPath = sydent.config.general.terms_path
  101. if termsPath == "":
  102. return Terms(None)
  103. with open(termsPath) as fp:
  104. termsYaml = yaml.safe_load(fp)
  105. # TODO use something like jsonschema instead of this handwritten code.
  106. if "master_version" not in termsYaml:
  107. raise Exception("No master version")
  108. elif not isinstance(termsYaml["master_version"], str):
  109. raise TypeError(
  110. f"master_version should be a string, not {termsYaml['master_version']!r}"
  111. )
  112. if "docs" not in termsYaml:
  113. raise Exception("No 'docs' key in terms")
  114. for docName, doc in termsYaml["docs"].items():
  115. if "version" not in doc:
  116. raise Exception("'%s' has no version" % (docName,))
  117. if "langs" not in doc:
  118. raise Exception("'%s' has no langs" % (docName,))
  119. for langKey, lang in doc["langs"].items():
  120. if "name" not in lang:
  121. raise Exception("lang '%s' of doc %s has no name" % (langKey, docName))
  122. if "url" not in lang:
  123. raise Exception("lang '%s' of doc %s has no url" % (langKey, docName))
  124. return Terms(termsYaml)