Coverage for cc_modules/cc_tabletsession.py : 37%

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
3"""
4camcops_server/cc_modules/cc_tabletsession.py
6===============================================================================
8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com).
10 This file is part of CamCOPS.
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.
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.
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/>.
25===============================================================================
27**Session information for client devices (tablets etc.).**
29"""
31import logging
32from typing import Optional, Set, TYPE_CHECKING
34from cardinal_pythonlib.logs import BraceStyleAdapter
35from cardinal_pythonlib.reprfunc import simple_repr
36from pyramid.exceptions import HTTPBadRequest
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)
62if TYPE_CHECKING:
63 from camcops_server.cc_modules.cc_request import CamcopsRequest
65log = BraceStyleAdapter(logging.getLogger(__name__))
67INVALID_USERNAME_PASSWORD = "Invalid username/password (or user not authorized)"
68NO_UPLOAD_GROUP_SET = "No upload group set for user "
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
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}")
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}")
115 self._device_obj = None # type: Optional[Device]
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
124 # Upload efficiency
125 self._dirty_table_names = set() # type: Set[str]
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)
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 )
149 # -------------------------------------------------------------------------
150 # Database objects, accessed on demand
151 # -------------------------------------------------------------------------
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
165 # -------------------------------------------------------------------------
166 # Permissions and similar
167 # -------------------------------------------------------------------------
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
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
189 def is_device_registered(self) -> bool:
190 """
191 Is the device registered with our server?
192 """
193 return self.device is not None
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
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")
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()
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")
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:
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
255 # -------------------------------------------------------------------------
256 # Version information (via property as not always needed)
257 # -------------------------------------------------------------------------
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)
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)
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)
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?
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)