def get_all_plots_for_one_task_html(self, tasks: List[Task]) -> str: """ HTML for all plots for a given task type. """ html = "" ntasks = len(tasks) if ntasks == 0: return html if not tasks[0].provides_trackers: # ask the first of the task instances return html alltrackers = [task.get_trackers(self.req) for task in tasks] datetimes = [task.get_creation_datetime() for task in tasks] ntrackers = len(alltrackers[0]) # ... number of trackers supplied by the first task (and all tasks) for tracker in range(ntrackers): values = [ alltrackers[tasknum][tracker].value for tasknum in range(ntasks) ] html += self.get_single_plot_html( datetimes, values, specimen_tracker=alltrackers[0][tracker]) for task in tasks: audit( self.req, "Tracker data accessed", table=task.tablename, server_pk=task.pk, patient_server_pk=task.get_patient_server_pk(), ) return html
def create_superuser(cls, req: "CamcopsRequest", username: str, password: str) -> bool: """ Creates a superuser. Will fail if the user already exists. Args: req: :class:`camcops_server.cc_modules.cc_request.CamcopsRequest` username: the new superuser's username password: the new superuser's password Returns: success? """ assert username, "Can't create superuser with no name" assert username != USER_NAME_FOR_SYSTEM, ( "Can't create user with name {!r}".format(USER_NAME_FOR_SYSTEM)) dbsession = req.dbsession user = cls.get_user_by_name(dbsession, username) if user: # already exists! return False # noinspection PyArgumentList user = cls(username=username) # does work! user.superuser = True audit(req, "SUPERUSER CREATED: " + user.username, from_console=True) user.set_password(req, password) # will audit dbsession.add(user) return True
def task_collection_to_tsv_zip_response(req: "CamcopsRequest", collection: "TaskCollection", sort_by_heading: bool) -> Response: """ Converts a set of tasks to a TSV (tab-separated value) response, as a set of TSV files (one per table) in a ZIP file. Args: req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest` collection: a :class:`camcops_server.cc_modules.cc_taskcollection.TaskCollection` sort_by_heading: sort columns within each page by heading name? Returns: a :class:`pyramid.response.Response` object """ # noqa # ------------------------------------------------------------------------- # Create memory file and ZIP file within it # ------------------------------------------------------------------------- memfile = io.BytesIO() z = zipfile.ZipFile(memfile, "w") # ------------------------------------------------------------------------- # Iterate through tasks # ------------------------------------------------------------------------- audit_descriptions = [] # type: List[str] # Task may return >1 file for TSV output (e.g. for subtables). tsvcoll = TsvCollection() for cls in collection.task_classes(): for task in gen_audited_tasks_for_task_class(collection, cls, audit_descriptions): tsv_pages = task.get_tsv_pages(req) tsvcoll.add_pages(tsv_pages) if sort_by_heading: tsvcoll.sort_headings_within_all_pages() # Write to ZIP. # If there are no valid task instances, there'll be no TSV; that's OK. for filename_stem in tsvcoll.get_page_names(): tsv_filename = filename_stem + ".tsv" tsv_contents = tsvcoll.get_tsv_file(filename_stem) z.writestr(tsv_filename, tsv_contents.encode("utf-8")) # ------------------------------------------------------------------------- # Finish and serve # ------------------------------------------------------------------------- z.close() # Audit audit(req, "Basic dump: {}".format("; ".join(audit_descriptions))) # Return the result zip_contents = memfile.getvalue() memfile.close() zip_filename = "CamCOPS_dump_{}.zip".format( format_datetime(req.now, DateFormat.FILENAME)) return ZipResponse(body=zip_contents, filename=zip_filename)
def set_password(self, req: "CamcopsRequest", new_password: str) -> None: """ Set a user's password. """ self.hashedpw = rnc_crypto.hash_password(new_password, BCRYPT_DEFAULT_LOG_ROUNDS) self.last_password_change_utc = req.now_utc self.must_change_password = False audit(req, "Password changed for user " + self.username)
def enable_user(cls, req: "CamcopsRequest", username: str) -> None: """ Unlock user and clear login failures. Args: req: :class:`camcops_server.cc_modules.cc_request.CamcopsRequest` username: the user's username """ SecurityAccountLockout.unlock_user(req, username) cls.clear_login_failures(req, username) audit(req, "User {} re-enabled".format(username))
def audit(self, req: "CamcopsRequest", details: str, from_console: bool = False) -> None: """ Audits an action to this patient. """ audit(req, details, patient_server_pk=self._pk, table=Patient.__tablename__, server_pk=self._pk, from_console=from_console)
def set_password_directly(req: "CamcopsRequest", username: str, password: str) -> bool: """ If the user exists, set its password. Returns Boolean success. Used from the command line. """ dbsession = req.dbsession user = User.get_user_by_name(dbsession, username) if not user: return False user.set_password(req, password) user.enable(req) audit(req, "Password changed for user " + user.username, from_console=True) return True
def lock_user_out(cls, req: "CamcopsRequest", username: str, lockout_minutes: int) -> None: """ Lock user out for a specified number of minutes. Args: req: :class:`camcops_server.cc_modules.cc_request.CamcopsRequest` username: the user's username lockout_minutes: number of minutes """ dbsession = req.dbsession now = req.now_utc lock_until = now + datetime.timedelta(minutes=lockout_minutes) # noinspection PyArgumentList lock = cls(username=username, lock_until=lock_until) dbsession.add(lock) audit( req, "Account {} locked out for {} minutes".format( username, lockout_minutes))
def act_on_login_failure(cls, req: "CamcopsRequest", username: str) -> None: """ Record login failure and lock out user if necessary. Args: req: :class:`camcops_server.cc_modules.cc_request.CamcopsRequest` username: the user's username """ cfg = req.config audit(req, "Failed login as user: {}".format(username)) cls.record_login_failure(req, username) nfailures = cls.how_many_login_failures(req, username) nlockouts = nfailures // cfg.lockout_threshold nfailures_since_last_lockout = nfailures % cfg.lockout_threshold if nlockouts >= 1 and nfailures_since_last_lockout == 0: # new lockout required lockout_minutes = nlockouts * \ cfg.lockout_duration_increment_minutes SecurityAccountLockout.lock_user_out(req, username, lockout_minutes)
def task_collection_to_sqlite_response(req: "CamcopsRequest", collection: "TaskCollection", export_options: TaskExportOptions, as_sql_not_binary: bool) -> Response: """ Converts a set of tasks to an SQLite export, either as binary or the SQL text to regenerate it. Args: req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest` collection: a :class:`camcops_server.cc_modules.cc_taskcollection.TaskCollection` export_options: a :class:`TaskExportOptions` object as_sql_not_binary: provide SQL text, rather than SQLite binary? Returns: a :class:`pyramid.response.Response` object """ # noqa # ------------------------------------------------------------------------- # Create memory file, dumper, and engine # ------------------------------------------------------------------------- # This approach failed: # # memfile = io.StringIO() # # def dump(querysql, *multiparams, **params): # compsql = querysql.compile(dialect=engine.dialect) # memfile.write("{};\n".format(compsql)) # # engine = create_engine('{dialect}://'.format(dialect=dialect_name), # strategy='mock', executor=dump) # dst_session = sessionmaker(bind=engine)() # type: SqlASession # # ... you get the error # AttributeError: 'MockConnection' object has no attribute 'begin' # ... which is fair enough. # # Next best thing: SQLite database. # Two ways to deal with it: # (a) duplicate our C++ dump code (which itself duplicate the SQLite # command-line executable's dump facility), then create the database, # dump it to a string, serve the string; or # (b) offer the binary SQLite file. # Or... (c) both. # Aha! pymysqlite.iterdump does this for us. # # If we create an in-memory database using create_engine('sqlite://'), # can we get the binary contents out? Don't think so. # # So we should first create a temporary on-disk file, then use that. # ------------------------------------------------------------------------- # Make temporary file (one whose filename we can know). # We use tempfile.mkstemp() for security, or NamedTemporaryFile, # which is a bit easier. However, you can't necessarily open the file # again under all OSs, so that's no good. The final option is # TemporaryDirectory, which is secure and convenient. # # https://docs.python.org/3/library/tempfile.html # https://security.openstack.org/guidelines/dg_using-temporary-files-securely.html # noqa # https://stackoverflow.com/questions/3924117/how-to-use-tempfile-namedtemporaryfile-in-python # noqa # ------------------------------------------------------------------------- db_basename = "temp.sqlite3" with tempfile.TemporaryDirectory() as tmpdirname: db_filename = os.path.join(tmpdirname, db_basename) # --------------------------------------------------------------------- # Make SQLAlchemy session # --------------------------------------------------------------------- url = "sqlite:///" + db_filename engine = create_engine(url, echo=False) dst_session = sessionmaker(bind=engine)() # type: SqlASession # --------------------------------------------------------------------- # Iterate through tasks, creating tables as we need them. # --------------------------------------------------------------------- audit_descriptions = [] # type: List[str] task_generator = gen_audited_tasks_by_task_class( collection, audit_descriptions) # --------------------------------------------------------------------- # Next bit very tricky. We're trying to achieve several things: # - a copy of part of the database structure # - a copy of part of the data, with relationships intact # - nothing sensitive (e.g. full User records) going through # - adding new columns for Task objects offering summary values # - Must treat tasks all together, because otherwise we will insert # duplicate dependency objects like Group objects. # --------------------------------------------------------------------- copy_tasks_and_summaries(tasks=task_generator, dst_engine=engine, dst_session=dst_session, export_options=export_options, req=req) dst_session.commit() # --------------------------------------------------------------------- # Audit # --------------------------------------------------------------------- audit(req, "SQL dump: {}".format("; ".join(audit_descriptions))) # --------------------------------------------------------------------- # Fetch file contents, either as binary, or as SQL # --------------------------------------------------------------------- filename_stem = "CamCOPS_dump_{}".format( format_datetime(req.now, DateFormat.FILENAME)) suggested_filename = filename_stem + (".sql" if as_sql_not_binary else ".sqlite3") if as_sql_not_binary: # SQL text con = sqlite3.connect(db_filename) with io.StringIO() as f: # noinspection PyTypeChecker for line in con.iterdump(): f.write(line + "\n") con.close() f.flush() sql_text = f.getvalue() return TextAttachmentResponse(body=sql_text, filename=suggested_filename) else: # SQLite binary with open(db_filename, 'rb') as f: binary_contents = f.read() return SqliteBinaryResponse(body=binary_contents, filename=suggested_filename)
def _get_xml( self, audit_string: str, xml_name: str, indent_spaces: int = 4, eol: str = "\n", include_comments: bool = False, ) -> str: """ Returns an XML document representing this object. Args: audit_string: description used to audit access to this information xml_name: name of the root XML element indent_spaces: number of spaces to indent formatted XML eol: end-of-line string include_comments: include comments describing each field? Returns: an XML UTF-8 document representing the task. """ iddef = self.taskfilter.get_only_iddef() if not iddef: raise ValueError("Tracker/CTV doesn't have a single ID number " "criterion") branches = [ self.consistency_info.get_xml_root(), XmlElement( name="_search_criteria", value=[ XmlElement( name="task_tablename_list", value=",".join(self.taskfilter.task_tablename_list), ), XmlElement( name=ViewParam.WHICH_IDNUM, value=iddef.which_idnum, datatype=XmlDataTypes.INTEGER, ), XmlElement( name=ViewParam.IDNUM_VALUE, value=iddef.idnum_value, datatype=XmlDataTypes.INTEGER, ), XmlElement( name=ViewParam.START_DATETIME, value=format_datetime(self.taskfilter.start_datetime, DateFormat.ISO8601), datatype=XmlDataTypes.DATETIME, ), XmlElement( name=ViewParam.END_DATETIME, value=format_datetime(self.taskfilter.end_datetime, DateFormat.ISO8601), datatype=XmlDataTypes.DATETIME, ), ], ), ] options = TaskExportOptions( xml_include_plain_columns=True, xml_include_calculated=True, include_blobs=False, ) for t in self.collection.all_tasks: branches.append(t.get_xml_root(self.req, options)) audit( self.req, audit_string, table=t.tablename, server_pk=t.pk, patient_server_pk=t.get_patient_server_pk(), ) tree = XmlElement(name=xml_name, value=branches) return get_xml_document( tree, indent_spaces=indent_spaces, eol=eol, include_comments=include_comments, )