Coverage for cc_modules/cc_tabletsession.py: 38%
104 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/cc_modules/cc_tabletsession.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
13 CamCOPS is free software: you can redistribute it and/or modify
14 it under the terms of the GNU General Public License as published by
15 the Free Software Foundation, either version 3 of the License, or
16 (at your option) any later version.
18 CamCOPS is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 GNU General Public License for more details.
23 You should have received a copy of the GNU General Public License
24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
26===============================================================================
28**Session information for client devices (tablets etc.).**
30"""
32import logging
33from typing import Optional, Set, TYPE_CHECKING
35from cardinal_pythonlib.httpconst import HttpMethod
36from cardinal_pythonlib.logs import BraceStyleAdapter
37from cardinal_pythonlib.reprfunc import simple_repr
38from pyramid.exceptions import HTTPBadRequest
40from camcops_server.cc_modules.cc_client_api_core import (
41 fail_user_error,
42 TabletParam,
43)
44from camcops_server.cc_modules.cc_constants import (
45 DEVICE_NAME_FOR_SERVER,
46 USER_NAME_FOR_SYSTEM,
47)
48from camcops_server.cc_modules.cc_device import Device
49from camcops_server.cc_modules.cc_validators import (
50 validate_anything,
51 validate_device_name,
52 validate_username,
53)
54from camcops_server.cc_modules.cc_version import (
55 FIRST_CPP_TABLET_VER,
56 FIRST_TABLET_VER_WITH_SEPARATE_IDNUM_TABLE,
57 FIRST_TABLET_VER_WITHOUT_IDDESC_IN_PT_TABLE,
58 FIRST_TABLET_VER_WITH_EXPLICIT_PKNAME_IN_UPLOAD_TABLE,
59 make_version,
60 MINIMUM_TABLET_VERSION,
61)
63if TYPE_CHECKING:
64 from camcops_server.cc_modules.cc_request import CamcopsRequest
66log = BraceStyleAdapter(logging.getLogger(__name__))
68INVALID_USERNAME_PASSWORD = (
69 "Invalid username/password (or user not authorized)"
70)
71NO_UPLOAD_GROUP_SET = "No upload group set for user "
74class TabletSession(object):
75 """
76 Represents session information for client devices. They use HTTPS POST
77 calls and do not bother with cookies.
78 """
80 def __init__(self, req: "CamcopsRequest") -> None:
81 # Check the basics
82 if req.method != HttpMethod.POST:
83 raise HTTPBadRequest("Must use POST method")
84 # ... this is for humans to view, so it has a pretty error
86 # Read key things
87 self.req = req
88 self.operation = req.get_str_param(TabletParam.OPERATION)
89 try:
90 self.device_name = req.get_str_param(
91 TabletParam.DEVICE, validator=validate_device_name
92 )
93 self.username = req.get_str_param(
94 TabletParam.USER, validator=validate_username
95 )
96 except ValueError as e:
97 fail_user_error(str(e))
98 self.password = req.get_str_param(
99 TabletParam.PASSWORD, validator=validate_anything
100 )
101 self.session_id = req.get_int_param(TabletParam.SESSION_ID)
102 self.session_token = req.get_str_param(
103 TabletParam.SESSION_TOKEN, validator=validate_anything
104 )
105 self.tablet_version_str = req.get_str_param(
106 TabletParam.CAMCOPS_VERSION, validator=validate_anything
107 )
108 try:
109 self.tablet_version_ver = make_version(self.tablet_version_str)
110 except ValueError:
111 fail_user_error(
112 f"CamCOPS tablet version nonsensical: "
113 f"{self.tablet_version_str!r}"
114 )
116 # Basic security check: no pretending to be the server
117 if self.device_name == DEVICE_NAME_FOR_SERVER:
118 fail_user_error(
119 f"Tablets cannot use the device name "
120 f"{DEVICE_NAME_FOR_SERVER!r}"
121 )
122 if self.username == USER_NAME_FOR_SYSTEM:
123 fail_user_error(
124 f"Tablets cannot use the username {USER_NAME_FOR_SYSTEM!r}"
125 )
127 self._device_obj = None # type: Optional[Device]
129 # Ensure table version is OK
130 if self.tablet_version_ver < MINIMUM_TABLET_VERSION:
131 fail_user_error(
132 f"Tablet CamCOPS version too old: is "
133 f"{self.tablet_version_str}, need {MINIMUM_TABLET_VERSION}"
134 )
135 # Other version things are done via properties
137 # Upload efficiency
138 self._dirty_table_names = set() # type: Set[str]
140 # Report
141 log.info(
142 "Incoming client API connection from IP={i}, port={p}, "
143 "device_name={dn!r}, "
144 # "device_id={di}, "
145 "camcops_version={v}, " "username={u}, operation={o}",
146 i=req.remote_addr,
147 p=req.remote_port,
148 dn=self.device_name,
149 # di=self.device_id,
150 v=self.tablet_version_str,
151 u=self.username,
152 o=self.operation,
153 )
155 def __repr__(self) -> str:
156 return simple_repr(
157 self,
158 [
159 "session_id",
160 "session_token",
161 "device_name",
162 "username",
163 "operation",
164 ],
165 with_addr=True,
166 )
168 # -------------------------------------------------------------------------
169 # Database objects, accessed on demand
170 # -------------------------------------------------------------------------
172 @property
173 def device(self) -> Optional[Device]:
174 """
175 Returns the :class:`camcops_server.cc_modules.cc_device.Device`
176 associated with this request/session, or ``None``.
177 """
178 if self._device_obj is None:
179 dbsession = self.req.dbsession
180 self._device_obj = Device.get_device_by_name(
181 dbsession, self.device_name
182 )
183 return self._device_obj
185 # -------------------------------------------------------------------------
186 # Permissions and similar
187 # -------------------------------------------------------------------------
189 @property
190 def device_id(self) -> Optional[int]:
191 """
192 Returns the integer device ID, if known.
193 """
194 device = self.device
195 if not device:
196 return None
197 return device.id
199 @property
200 def user_id(self) -> Optional[int]:
201 """
202 Returns the integer user ID, if known.
203 """
204 user = self.req.user
205 if not user:
206 return None
207 return user.id
209 def is_device_registered(self) -> bool:
210 """
211 Is the device registered with our server?
212 """
213 return self.device is not None
215 def reload_device(self):
216 """
217 Re-fetch the device information from the database.
218 (Or, at least, do so when it's next required.)
219 """
220 self._device_obj = None
222 def ensure_device_registered(self) -> None:
223 """
224 Ensure the device is registered. Raises :exc:`UserErrorException`
225 on failure.
226 """
227 if not self.is_device_registered():
228 fail_user_error("Unregistered device")
230 def ensure_valid_device_and_user_for_uploading(self) -> None:
231 """
232 Ensure the device/username/password combination is valid for uploading.
233 Raises :exc:`UserErrorException` on failure.
234 """
235 user = self.req.user
236 if not user:
237 fail_user_error(INVALID_USERNAME_PASSWORD)
238 if user.upload_group_id is None:
239 fail_user_error(NO_UPLOAD_GROUP_SET + user.username)
240 if not user.may_upload:
241 fail_user_error("User not authorized to upload to selected group")
242 # Username/password combination found and is valid. Now check device.
243 self.ensure_device_registered()
245 def ensure_valid_user_for_device_registration(self) -> None:
246 """
247 Ensure the username/password combination is valid for device
248 registration. Raises :exc:`UserErrorException` on failure.
249 """
250 user = self.req.user
251 if not user:
252 fail_user_error(INVALID_USERNAME_PASSWORD)
253 if user.upload_group_id is None:
254 fail_user_error(NO_UPLOAD_GROUP_SET + user.username)
255 if not user.may_register_devices:
256 fail_user_error(
257 "User not authorized to register devices for " "selected group"
258 )
260 def set_session_id_token(
261 self, session_id: int, session_token: str
262 ) -> None:
263 """
264 Sets the session ID and token.
265 Typical situation:
267 - :class:`camcops_server.cc_modules.cc_tabletsession.TabletSession`
268 created; may or may not have an ID/token as part of the POST request
269 - :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
270 translates that into a server-side session
271 - If one wasn't found and needs to be created, we write back
272 the values here.
273 """
274 self.session_id = session_id
275 self.session_token = session_token
277 # -------------------------------------------------------------------------
278 # Version information (via property as not always needed)
279 # -------------------------------------------------------------------------
281 @property
282 def cope_with_deleted_patient_descriptors(self) -> bool:
283 """
284 Must we cope with an old client that had ID descriptors
285 in the Patient table?
286 """
287 return (
288 self.tablet_version_ver
289 < FIRST_TABLET_VER_WITHOUT_IDDESC_IN_PT_TABLE
290 )
292 @property
293 def cope_with_old_idnums(self) -> bool:
294 """
295 Must we cope with an old client that had ID numbers embedded in the
296 Patient table?
297 """
298 return (
299 self.tablet_version_ver
300 < FIRST_TABLET_VER_WITH_SEPARATE_IDNUM_TABLE
301 )
303 @property
304 def explicit_pkname_for_upload_table(self) -> bool:
305 """
306 Is the client a nice new one that explicitly names the
307 primary key when uploading tables?
308 """
309 return (
310 self.tablet_version_ver
311 >= FIRST_TABLET_VER_WITH_EXPLICIT_PKNAME_IN_UPLOAD_TABLE
312 )
314 @property
315 def pkname_in_upload_table_neither_first_nor_explicit(self):
316 """
317 Is the client a particularly tricky old version that is a C++ client
318 (generally a good thing, but meaning that the primary key might not be
319 the first field in uploaded tables) but had a bug such that it did not
320 explicitly name its PK either?
322 See discussion of bug in ``NetworkManager::sendTableWhole`` (C++).
323 For these versions, the only safe thing is to take ``"id"`` as the
324 name of the (client-side) primary key.
325 """
326 return (
327 self.tablet_version_ver >= FIRST_CPP_TABLET_VER
328 and not self.explicit_pkname_for_upload_table
329 )