Coverage for cc_modules/cc_tabletsession.py: 80%

1"""

2 camcops_server/cc_modules/cc_tabletsession.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**Session information for client devices (tablets etc.).**

27

28"""

29

30 import logging

31 from typing import Optional, Set, TYPE_CHECKING

32

33 from cardinal_pythonlib.httpconst import HttpMethod

34 from cardinal_pythonlib.logs import BraceStyleAdapter

35 from cardinal_pythonlib.reprfunc import simple_repr

36 from pyramid.exceptions import HTTPBadRequest

37

38 from camcops_server.cc_modules.cc_client_api_core import (

39 fail_user_error,

40 TabletParam,

41)

42 from camcops_server.cc_modules.cc_constants import (

43 DEVICE_NAME_FOR_SERVER,

44 USER_NAME_FOR_SYSTEM,

45)

46 from camcops_server.cc_modules.cc_device import Device

47 from camcops_server.cc_modules.cc_validators import (

48 validate_anything,

49 validate_device_name,

50 validate_username,

51)

52 from camcops_server.cc_modules.cc_version import (

53 FIRST_CPP_TABLET_VER,

54 FIRST_TABLET_VER_WITH_SEPARATE_IDNUM_TABLE,

55 FIRST_TABLET_VER_WITHOUT_IDDESC_IN_PT_TABLE,

56 FIRST_TABLET_VER_WITH_EXPLICIT_PKNAME_IN_UPLOAD_TABLE,

57 make_version,

58 MINIMUM_TABLET_VERSION,

59)

60

61 if TYPE_CHECKING:

62 from camcops_server.cc_modules.cc_request import CamcopsRequest

63

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

65

66 INVALID_USERNAME_PASSWORD = (

67 "Invalid username/password (or user not authorized)"

68)

69 NO_UPLOAD_GROUP_SET = "No upload group set for user "

70

71

72 class TabletSession(object):

73 """

74 Represents session information for client devices. They use HTTPS POST

75 calls and do not bother with cookies.

76 """

77

78 def __init__(self, req: "CamcopsRequest") -> None:

79 # Check the basics

80 if req.method != HttpMethod.POST:

81 raise HTTPBadRequest("Must use POST method")

82 # ... this is for humans to view, so it has a pretty error

83

84 # Read key things

85 self.req = req

86 self.operation = req.get_str_param(TabletParam.OPERATION)

87 try:

88 self.device_name = req.get_str_param(

89 TabletParam.DEVICE, validator=validate_device_name

90 )

91 self.username = req.get_str_param(

92 TabletParam.USER, validator=validate_username

93 )

94 except ValueError as e:

95 fail_user_error(str(e))

96 self.password = req.get_str_param(

97 TabletParam.PASSWORD, validator=validate_anything

98 )

99 self.session_id = req.get_int_param(TabletParam.SESSION_ID)

100 self.session_token = req.get_str_param(

101 TabletParam.SESSION_TOKEN, validator=validate_anything

102 )

103 self.tablet_version_str = req.get_str_param(

104 TabletParam.CAMCOPS_VERSION, validator=validate_anything

105 )

106 try:

107 self.tablet_version_ver = make_version(self.tablet_version_str)

108 except ValueError:

109 fail_user_error(

110 f"CamCOPS tablet version nonsensical: "

111 f"{self.tablet_version_str!r}"

112 )

113

114 # Basic security check: no pretending to be the server

115 if self.device_name == DEVICE_NAME_FOR_SERVER:

116 fail_user_error(

117 f"Tablets cannot use the device name "

118 f"{DEVICE_NAME_FOR_SERVER!r}"

119 )

120 if self.username == USER_NAME_FOR_SYSTEM:

121 fail_user_error(

122 f"Tablets cannot use the username {USER_NAME_FOR_SYSTEM!r}"

123 )

124

125 self._device_obj = None # type: Optional[Device]

126

127 # Ensure table version is OK

128 if self.tablet_version_ver < MINIMUM_TABLET_VERSION:

129 fail_user_error(

130 f"Tablet CamCOPS version too old: is "

131 f"{self.tablet_version_str}, need {MINIMUM_TABLET_VERSION}"

132 )

133 # Other version things are done via properties

134

135 # Upload efficiency

136 self._dirty_table_names = set() # type: Set[str]

137

138 # Report

139 log.info(

140 "Incoming client API connection from IP={i}, port={p}, "

141 "device_name={dn!r}, "

142 # "device_id={di}, "

143 "camcops_version={v}, " "username={u}, operation={o}",

144 i=req.remote_addr,

145 p=req.remote_port,

146 dn=self.device_name,

147 # di=self.device_id,

148 v=self.tablet_version_str,

149 u=self.username,

150 o=self.operation,

151 )

152

153 def __repr__(self) -> str:

154 return simple_repr(

155 self,

156 [

157 "session_id",

158 "session_token",

159 "device_name",

160 "username",

161 "operation",

162 ],

163 with_addr=True,

164 )

165

166 # -------------------------------------------------------------------------

167 # Database objects, accessed on demand

168 # -------------------------------------------------------------------------

169

170 @property

171 def device(self) -> Optional[Device]:

172 """

173 Returns the :class:`camcops_server.cc_modules.cc_device.Device`

174 associated with this request/session, or ``None``.

175 """

176 if self._device_obj is None:

177 dbsession = self.req.dbsession

178 self._device_obj = Device.get_device_by_name(

179 dbsession, self.device_name

180 )

181 return self._device_obj

182

183 # -------------------------------------------------------------------------

184 # Permissions and similar

185 # -------------------------------------------------------------------------

186

187 @property

188 def device_id(self) -> Optional[int]:

189 """

190 Returns the integer device ID, if known.

191 """

192 device = self.device

193 if not device:

194 return None

195 return device.id

196

197 @property

198 def user_id(self) -> Optional[int]:

199 """

200 Returns the integer user ID, if known.

201 """

202 user = self.req.user

203 if not user:

204 return None

205 return user.id

206

207 def is_device_registered(self) -> bool:

208 """

209 Is the device registered with our server?

210 """

211 return self.device is not None

212

213 def reload_device(self) -> None:

214 """

215 Re-fetch the device information from the database.

216 (Or, at least, do so when it's next required.)

217 """

218 self._device_obj = None

219

220 def ensure_device_registered(self) -> None:

221 """

222 Ensure the device is registered. Raises :exc:`UserErrorException`

223 on failure.

224 """

225 if not self.is_device_registered():

226 fail_user_error("Unregistered device")

227

228 def ensure_valid_device_and_user_for_uploading(self) -> None:

229 """

230 Ensure the device/username/password combination is valid for uploading.

231 Raises :exc:`UserErrorException` on failure.

232 """

233 user = self.req.user

234 if not user:

235 fail_user_error(INVALID_USERNAME_PASSWORD)

236 if user.upload_group_id is None:

237 fail_user_error(NO_UPLOAD_GROUP_SET + user.username)

238 if not user.may_upload:

239 fail_user_error("User not authorized to upload to selected group")

240 # Username/password combination found and is valid. Now check device.

241 self.ensure_device_registered()

242

243 def ensure_valid_user_for_device_registration(self) -> None:

244 """

245 Ensure the username/password combination is valid for device

246 registration. Raises :exc:`UserErrorException` on failure.

247 """

248 user = self.req.user

249 if not user:

250 fail_user_error(INVALID_USERNAME_PASSWORD)

251 if user.upload_group_id is None:

252 fail_user_error(NO_UPLOAD_GROUP_SET + user.username)

253 if not user.may_register_devices:

254 fail_user_error(

255 "User not authorized to register devices for " "selected group"

256 )

257

258 def set_session_id_token(

259 self, session_id: int, session_token: str

260 ) -> None:

261 """

262 Sets the session ID and token.

263 Typical situation:

264

265 - :class:`camcops_server.cc_modules.cc_tabletsession.TabletSession`

266 created; may or may not have an ID/token as part of the POST request

267 - :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`

268 translates that into a server-side session

269 - If one wasn't found and needs to be created, we write back

270 the values here.

271 """

272 self.session_id = session_id

273 self.session_token = session_token

274

275 # -------------------------------------------------------------------------

276 # Version information (via property as not always needed)

277 # -------------------------------------------------------------------------

278

279 @property

280 def cope_with_deleted_patient_descriptors(self) -> bool:

281 """

282 Must we cope with an old client that had ID descriptors

283 in the Patient table?

284 """

285 return (

286 self.tablet_version_ver

287 < FIRST_TABLET_VER_WITHOUT_IDDESC_IN_PT_TABLE

288 )

289

290 @property

291 def cope_with_old_idnums(self) -> bool:

292 """

293 Must we cope with an old client that had ID numbers embedded in the

294 Patient table?

295 """

296 return (

297 self.tablet_version_ver

298 < FIRST_TABLET_VER_WITH_SEPARATE_IDNUM_TABLE

299 )

300

301 @property

302 def explicit_pkname_for_upload_table(self) -> bool:

303 """

304 Is the client a nice new one that explicitly names the

305 primary key when uploading tables?

306 """

307 return (

308 self.tablet_version_ver

309 >= FIRST_TABLET_VER_WITH_EXPLICIT_PKNAME_IN_UPLOAD_TABLE

310 )

311

312 @property

313 def pkname_in_upload_table_neither_first_nor_explicit(self) -> bool:

314 """

315 Is the client a particularly tricky old version that is a C++ client

316 (generally a good thing, but meaning that the primary key might not be

317 the first field in uploaded tables) but had a bug such that it did not

318 explicitly name its PK either?

319

320 See discussion of bug in ``NetworkManager::sendTableWhole`` (C++).

321 For these versions, the only safe thing is to take ``"id"`` as the

322 name of the (client-side) primary key.

323 """

324 return (

325 self.tablet_version_ver >= FIRST_CPP_TABLET_VER

326 and not self.explicit_pkname_for_upload_table

327 )