Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/cc_modules/cc_tabletsession.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

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

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

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

15 (at your option) any later version. 

16 

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

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

19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

20 GNU General Public License for more details. 

21 

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

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

24 

25=============================================================================== 

26 

27**Session information for client devices (tablets etc.).** 

28 

29""" 

30 

31import logging 

32from typing import Optional, Set, TYPE_CHECKING 

33 

34from cardinal_pythonlib.logs import BraceStyleAdapter 

35from cardinal_pythonlib.reprfunc import simple_repr 

36from pyramid.exceptions import HTTPBadRequest 

37 

38from camcops_server.cc_modules.cc_client_api_core import ( 

39 fail_user_error, 

40 TabletParam, 

41) 

42from camcops_server.cc_modules.cc_constants import ( 

43 DEVICE_NAME_FOR_SERVER, 

44 USER_NAME_FOR_SYSTEM, 

45) 

46from camcops_server.cc_modules.cc_device import Device 

47from camcops_server.cc_modules.cc_pyramid import RequestMethod 

48from camcops_server.cc_modules.cc_validators import ( 

49 validate_anything, 

50 validate_device_name, 

51 validate_username, 

52) 

53from camcops_server.cc_modules.cc_version import ( 

54 FIRST_CPP_TABLET_VER, 

55 FIRST_TABLET_VER_WITH_SEPARATE_IDNUM_TABLE, 

56 FIRST_TABLET_VER_WITHOUT_IDDESC_IN_PT_TABLE, 

57 FIRST_TABLET_VER_WITH_EXPLICIT_PKNAME_IN_UPLOAD_TABLE, 

58 make_version, 

59 MINIMUM_TABLET_VERSION, 

60) 

61 

62if TYPE_CHECKING: 

63 from camcops_server.cc_modules.cc_request import CamcopsRequest 

64 

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

66 

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

68NO_UPLOAD_GROUP_SET = "No upload group set for user " 

69 

70 

71class TabletSession(object): 

72 """ 

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

74 calls and do not bother with cookies. 

75 """ 

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

77 # Check the basics 

78 if req.method != RequestMethod.POST: 

79 raise HTTPBadRequest("Must use POST method") 

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

81 

82 # Read key things 

83 self.req = req 

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

85 try: 

86 self.device_name = req.get_str_param( 

87 TabletParam.DEVICE, validator=validate_device_name) 

88 self.username = req.get_str_param( 

89 TabletParam.USER, validator=validate_username) 

90 except ValueError as e: 

91 fail_user_error(str(e)) 

92 self.password = req.get_str_param( 

93 TabletParam.PASSWORD, validator=validate_anything) 

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

95 self.session_token = req.get_str_param( 

96 TabletParam.SESSION_TOKEN, validator=validate_anything) 

97 self.tablet_version_str = req.get_str_param( 

98 TabletParam.CAMCOPS_VERSION, validator=validate_anything) 

99 try: 

100 self.tablet_version_ver = make_version(self.tablet_version_str) 

101 except ValueError: 

102 fail_user_error( 

103 f"CamCOPS tablet version nonsensical: " 

104 f"{self.tablet_version_str!r}") 

105 

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

107 if self.device_name == DEVICE_NAME_FOR_SERVER: 

108 fail_user_error( 

109 f"Tablets cannot use the device name " 

110 f"{DEVICE_NAME_FOR_SERVER!r}") 

111 if self.username == USER_NAME_FOR_SYSTEM: 

112 fail_user_error( 

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

114 

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

116 

117 # Ensure table version is OK 

118 if self.tablet_version_ver < MINIMUM_TABLET_VERSION: 

119 fail_user_error( 

120 f"Tablet CamCOPS version too old: is " 

121 f"{self.tablet_version_str}, need {MINIMUM_TABLET_VERSION}") 

122 # Other version things are done via properties 

123 

124 # Upload efficiency 

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

126 

127 # Report 

128 log.info("Incoming client API connection from IP={i}, port={p}, " 

129 "device_name={dn!r}, " 

130 # "device_id={di}, " 

131 "camcops_version={v}, " 

132 "username={u}, operation={o}", 

133 i=req.remote_addr, 

134 p=req.remote_port, 

135 dn=self.device_name, 

136 # di=self.device_id, 

137 v=self.tablet_version_str, 

138 u=self.username, 

139 o=self.operation) 

140 

141 def __repr__(self) -> str: 

142 return simple_repr( 

143 self, 

144 ["session_id", "session_token", "device_name", "username", 

145 "operation"], 

146 with_addr=True 

147 ) 

148 

149 # ------------------------------------------------------------------------- 

150 # Database objects, accessed on demand 

151 # ------------------------------------------------------------------------- 

152 

153 @property 

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

155 """ 

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

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

158 """ 

159 if self._device_obj is None: 

160 dbsession = self.req.dbsession 

161 self._device_obj = Device.get_device_by_name(dbsession, 

162 self.device_name) 

163 return self._device_obj 

164 

165 # ------------------------------------------------------------------------- 

166 # Permissions and similar 

167 # ------------------------------------------------------------------------- 

168 

169 @property 

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

171 """ 

172 Returns the integer device ID, if known. 

173 """ 

174 device = self.device 

175 if not device: 

176 return None 

177 return device.id 

178 

179 @property 

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

181 """ 

182 Returns the integer user ID, if known. 

183 """ 

184 user = self.req.user 

185 if not user: 

186 return None 

187 return user.id 

188 

189 def is_device_registered(self) -> bool: 

190 """ 

191 Is the device registered with our server? 

192 """ 

193 return self.device is not None 

194 

195 def reload_device(self): 

196 """ 

197 Re-fetch the device information from the database. 

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

199 """ 

200 self._device_obj = None 

201 

202 def ensure_device_registered(self) -> None: 

203 """ 

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

205 on failure. 

206 """ 

207 if not self.is_device_registered(): 

208 fail_user_error("Unregistered device") 

209 

210 def ensure_valid_device_and_user_for_uploading(self) -> None: 

211 """ 

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

213 Raises :exc:`UserErrorException` on failure. 

214 """ 

215 user = self.req.user 

216 if not user: 

217 fail_user_error(INVALID_USERNAME_PASSWORD) 

218 if user.upload_group_id is None: 

219 fail_user_error(NO_UPLOAD_GROUP_SET + user.username) 

220 if not user.may_upload: 

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

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

223 self.ensure_device_registered() 

224 

225 def ensure_valid_user_for_device_registration(self) -> None: 

226 """ 

227 Ensure the username/password combination is valid for device 

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

229 """ 

230 user = self.req.user 

231 if not user: 

232 fail_user_error(INVALID_USERNAME_PASSWORD) 

233 if user.upload_group_id is None: 

234 fail_user_error(NO_UPLOAD_GROUP_SET + user.username) 

235 if not user.may_register_devices: 

236 fail_user_error("User not authorized to register devices for " 

237 "selected group") 

238 

239 def set_session_id_token(self, session_id: int, 

240 session_token: str) -> None: 

241 """ 

242 Sets the session ID and token. 

243 Typical situation: 

244 

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

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

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

248 translates that into a server-side session 

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

250 the values here. 

251 """ 

252 self.session_id = session_id 

253 self.session_token = session_token 

254 

255 # ------------------------------------------------------------------------- 

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

257 # ------------------------------------------------------------------------- 

258 

259 @property 

260 def cope_with_deleted_patient_descriptors(self) -> bool: 

261 """ 

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

263 in the Patient table? 

264 """ 

265 return (self.tablet_version_ver < 

266 FIRST_TABLET_VER_WITHOUT_IDDESC_IN_PT_TABLE) 

267 

268 @property 

269 def cope_with_old_idnums(self) -> bool: 

270 """ 

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

272 Patient table? 

273 """ 

274 return (self.tablet_version_ver < 

275 FIRST_TABLET_VER_WITH_SEPARATE_IDNUM_TABLE) 

276 

277 @property 

278 def explicit_pkname_for_upload_table(self) -> bool: 

279 """ 

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

281 primary key when uploading tables? 

282 """ 

283 return (self.tablet_version_ver >= 

284 FIRST_TABLET_VER_WITH_EXPLICIT_PKNAME_IN_UPLOAD_TABLE) 

285 

286 @property 

287 def pkname_in_upload_table_neither_first_nor_explicit(self): 

288 """ 

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

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

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

292 explicitly name its PK either? 

293 

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

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

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

297 """ 

298 return (self.tablet_version_ver >= FIRST_CPP_TABLET_VER and 

299 not self.explicit_pkname_for_upload_table)