Coverage for cc_modules/cc_serversettings.py: 90%

1"""

2 camcops_server/cc_modules/cc_serversettings.py

3

4===============================================================================

5

6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

8

9 This file is part of CamCOPS.

10

11 CamCOPS is free software: you can redistribute it and/or modify

12 it under the terms of the GNU General Public License as published by

13 the Free Software Foundation, either version 3 of the License, or

14 (at your option) any later version.

15

16 CamCOPS is distributed in the hope that it will be useful,

17 but WITHOUT ANY WARRANTY; without even the implied warranty of

18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

19 GNU General Public License for more details.

20

21 You should have received a copy of the GNU General Public License

22 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.

23

24===============================================================================

25

26**Represents server-wide configuration settings.**

27

28 Previously, we had a key/value pair system, both for device stored variables

29(table "storedvars") and server ones ("_server_storedvars"). We used a "type"

30 column to indicate type, and then columns named "valueInteger", "valueText",

31"valueReal" for the actual values.

32

33 Subsequently

34

35- There's no need for devices to upload their settings here, so that table

36 goes.

37

38- The server stored vars stored

39

40.. code-block:: none

41

42 idDescription1 - idDescription8 } now have their own table

43 idShortDescription1 - idShortDescription8 }

44

45 idPolicyUpload } now part of Group definition

46 idPolicyFinalize }

47

48 lastAnalyticsSentAt now unused

49

50 serverCamcopsVersion unnecessary (is in code)

51

52 databaseTitle still needed somehow

53

54 So, two options:

55 https://stackoverflow.com/questions/2300356/using-a-single-row-configuration-table-in-sql-server-database-bad-idea

56

57 Let's use a single row, based on a fixed PK (of 1).

58

59 On some databases, you can constrain the PK value to enforce "one row only";

60 MySQL isn't one of those.

61

62- https://docs.sqlalchemy.org/en/latest/core/constraints.html#check-constraint

63

64- https://stackoverflow.com/questions/3967372/sql-server-how-to-constrain-a-table-to-contain-a-single-row

65

66""" # noqa

67

68 from datetime import datetime

69 import logging

70 from typing import Optional, TYPE_CHECKING

71

72 from cardinal_pythonlib.logs import BraceStyleAdapter

73 import pendulum

74 from pendulum import DateTime as Pendulum

75 from sqlalchemy.orm import Mapped, mapped_column

76 from sqlalchemy.sql.schema import Column, MetaData, Table

77 from sqlalchemy.sql.sqltypes import (

78 Float,

79 Integer,

80 String,

81 UnicodeText,

82)

83

84 from camcops_server.cc_modules.cc_sqla_coltypes import DatabaseTitleColType

85 from camcops_server.cc_modules.cc_sqlalchemy import Base

86

87 if TYPE_CHECKING:

88 from camcops_server.cc_modules.cc_request import CamcopsRequest

89

90 log = BraceStyleAdapter(logging.getLogger(__name__))

91

92

93# =============================================================================

94# ServerStoredVars - defunct, but maintained for database imports

95# =============================================================================

96

97

98 class StoredVarTypesDefunct(object):

99 """

100 Variable types for the ServerStoredVars system.

101

102 Defunct, but maintained for database imports.

103 """

104

105 # values for the "type" column

106 TYPE_INTEGER = "integer"

107 TYPE_TEXT = "text"

108 TYPE_REAL = "real"

109

110

111 class ServerStoredVarNamesDefunct(object):

112 """

113 Variable names for the ServerStoredVars system.

114

115 Defunct, but maintained for database imports.

116 """

117

118 # values for the "name" column

119 ID_POLICY_UPLOAD = "idPolicyUpload" # text

120 ID_POLICY_FINALIZE = "idPolicyFinalize" # text

121 SERVER_CAMCOPS_VERSION = "serverCamcopsVersion" # text

122 DATABASE_TITLE = "databaseTitle" # text

123 LAST_ANALYTICS_SENT_AT = "lastAnalyticsSentAt" # text

124 ID_DESCRIPTION_PREFIX = "idDescription" # text; apply suffixes 1-8

125 ID_SHORT_DESCRIPTION_PREFIX = (

126 "idShortDescription" # text; apply suffixes 1-8

127 )

128

129

130 StoredVarNameColTypeDefunct = String(length=255)

131 StoredVarTypeColTypeDefunct = String(length=255)

132 _ssv_metadata = MetaData()

133

134

135 server_stored_var_table_defunct = Table(

136 "_server_storedvars", # table name

137 _ssv_metadata, # metadata separate from everything else

138 Column(

139 "name",

140 StoredVarNameColTypeDefunct,

141 primary_key=True,

142 index=True,

143 comment="Variable name",

144 ),

145 Column(

146 "type",

147 StoredVarTypeColTypeDefunct,

148 nullable=False,

149 comment="Variable type ('integer', 'real', 'text')",

150 ),

151 Column("valueInteger", Integer, comment="Value of an integer variable"),

152 Column("valueText", UnicodeText, comment="Value of a text variable"),

153 Column(

154 "valueReal", Float, comment="Value of a real (floating-point) variable"

155 ),

156)

157

158

159# =============================================================================

160# ServerSettings

161# =============================================================================

162

163 SERVER_SETTINGS_SINGLETON_PK = 1

164# CACHE_KEY_DATABASE_TITLE = "database_title"

165

166

167 class ServerSettings(Base):

168 """

169 Singleton SQLAlchemy object (i.e. there is just one row in the database

170 table) representing server settings.

171 """

172

173 __tablename__ = "_server_settings"

174

175 id: Mapped[int] = mapped_column(

176 primary_key=True,

177 autoincrement=True,

178 index=True,

179 comment=(

180 f"PK (arbitrary integer but only a value of "

181 f"{SERVER_SETTINGS_SINGLETON_PK} is ever used)"

182 ),

183 )

184 database_title: Mapped[Optional[str]] = mapped_column(

185 DatabaseTitleColType, comment="Database title"

186 )

187 last_dummy_login_failure_clearance_at_utc: Mapped[Optional[datetime]] = (

188 mapped_column(

189 comment="Date/time (in UTC) when login failure records were "

190 "cleared for nonexistent users (security feature)",

191 )

192 )

193

194 def get_last_dummy_login_failure_clearance_pendulum(

195 self,

196 ) -> Optional[Pendulum]:

197 """

198 Returns the time at which login failure records were cleared for

199 nonexistent users.

200

201 This is part of a security failure to prevent attackers discovering

202 usernames: since repeated attempts to hack a real account leads to an

203 account lockout, we arrange things so that attempts to hack nonexistent

204 accounts do likewise.

205

206 Specifically, this function returns an offset-aware (timezone-aware)

207 version of the raw UTC DATETIME from the database.

208 """

209 dt = self.last_dummy_login_failure_clearance_at_utc

210 if dt is None:

211 return None

212 return pendulum.instance(dt, tz=pendulum.UTC)

213

214

215 def get_server_settings(req: "CamcopsRequest") -> ServerSettings:

216 """

217 Gets the

218 :class:`camcops_server.cc_modules.cc_serversettings.ServerSettings` object

219 for the request.

220 """

221 dbsession = req.dbsession

222 server_settings = (

223 dbsession.query(ServerSettings)

224 .filter(ServerSettings.id == SERVER_SETTINGS_SINGLETON_PK)

225 .first()

226 )

227 if server_settings is None:

228 server_settings = ServerSettings()

229 server_settings.id = SERVER_SETTINGS_SINGLETON_PK

230 server_settings.database_title = "DATABASE_TITLE_UNSET"

231 dbsession.add(server_settings)

232 return server_settings

233

234

235# def get_database_title(req: "CamcopsRequest") -> str:

236# def creator() -> str:

237# server_settings = get_server_settings(req)

238# return server_settings.database_title or ""

239#

240# return cache_region_static.get_or_create(CACHE_KEY_DATABASE_TITLE, creator) # noqa

241

242

243# def clear_database_title_cache() -> None:

244# cache_region_static.delete(CACHE_KEY_DATABASE_TITLE)

245

246

247# def set_database_title(req: "CamcopsRequest", title: str) -> None:

248# server_settings = get_server_settings(req)

249# server_settings.database_title = title

250# clear_database_title_cache()