Esempio n. 1
0
    def get_anonymisation_database(self):
        """Open the anonymisation staging database. That is not performance-
        critical and the connection does not need to be cached. Will raise
        an exception upon a connection error."""
        # Follows same security principles as above.
        config = ConfigParser.ConfigParser()
        config.readfp(codecs.open(self.CAMCOPS_CONFIG_FILE, "r", "utf8"))
        section = CONFIG_FILE_MAIN_SECTION

        server = get_config_parameter(config, section, "ANONSTAG_DB_SERVER", str, DEFAULT_DB_SERVER)
        port = get_config_parameter(config, section, "ANONSTAG_DB_PORT", int, DEFAULT_DB_PORT)
        database = get_config_parameter(config, section, "ANONSTAG_DB_NAME", str, None)
        if database is None:
            raise RuntimeError("ANONSTAG_DB_NAME not specified in config")
        user = get_config_parameter(config, section, "ANONSTAG_DB_USER", str, None)
        if user is None:
            raise RuntimeError("ANONSTAG_DB_USER not specified in config")
        # It is a potential disaster if the anonymisation database is the same
        # database as the main database - risk of destroying original data.
        # We mitigate this risk in two ways.
        # (1) We check here. Since different server/port combinations could
        #     resolve to the same host, we take the extremely conservative
        #     approach of requiring a different database name.
        if database == self.DB_NAME:
            raise RuntimeError("ANONSTAG_DB_NAME must be different from " "DB_NAME")
        # (2) We prefix all tablenames in the CRIS staging database;
        #     see cc_task.
        try:
            password = get_config_parameter(config, section, "ANONSTAG_DB_PASSWORD", str, None)
        except:  # deliberately conceal details for security
            password = None
            raise RuntimeError("Problem reading ANONSTAG_DB_PASSWORD from " "config")
        if password is None:
            raise RuntimeError("ANONSTAG_DB_PASSWORD not specified in config")
        try:
            db = rnc_db.DatabaseSupporter()
            db.connect_to_database_mysql(
                server=server,
                port=port,
                database=database,
                user=user,
                password=password,
                autocommit=False  # NB therefore need to commit
                # ... done in camcops.py at the end of a session
            )
        except:  # deliberately conceal details for security
            raise rnc_db.NoDatabaseError(
                "Problem opening or reading from database; details concealed " "for security reasons"
            )
        finally:
            # Executed whether an exception is raised or not.
            password = None
        # ---------------------------------------------------------------------
        # Password is now re-obscured in all situations. Onwards...
        # ---------------------------------------------------------------------
        return db
Esempio n. 2
0
    def __init__(self, *args, **kwargs):
        """Initialize. Possible methods:

            RecipientDefinition()
            RecipientDefinition(config, section)

        Args:
            config: ConfigParser INI file object
            section: name of recipient and of INI file section
        """
        rnc_db.blank_object(self, RecipientDefinition.FIELDS)
        # HL7 fields not copied to database
        self.ping_first = None
        self.network_timeout_ms = None
        self.idnum_type_list = [None] * NUMBER_OF_IDNUMS
        self.idnum_aa_list = [None] * NUMBER_OF_IDNUMS
        self.keep_message = None
        self.keep_reply = None
        # File fields not copied to database (because actual filename stored):
        self.patient_spec_if_anonymous = None
        self.patient_spec = None
        self.filename_spec = None
        self.make_directory = None
        # Some default values we never want to be None
        self.include_anonymous = False
        # Internal use
        self.valid = False

        # Variable constructor...
        nargs = len(args)
        if nargs == 0:
            # dummy one
            self.type = RECIPIENT_TYPE.FILE
            self.primary_idnum = 1
            self.require_idnum_mandatory = False
            self.finalized_only = False
            self.task_format = VALUE.OUTPUTTYPE_XML
            # File
            self.include_anonymous = True
            self.patient_spec_if_anonymous = "anonymous"
            self.patient_spec = "{surname}_{forename}_{idshortdesc1}{idnum1}"
            self.filename_spec = (
                "/tmp/camcops_debug_testing/"
                "TestCamCOPS_{patient}_{created}_{tasktype}-{serverpk}"
                ".{filetype}"
            )
            self.overwrite_files = False
            self.make_directory = True
            return
        elif nargs != 2:
            raise AssertionError("RecipientDefinition: bad __init__ call")

        # Standard constructor
        config = args[0]
        section = args[1]

        self.recipient = section
        try:
            self.type = get_config_parameter(
                config, section, "TYPE", str, "hl7")
            self.primary_idnum = get_config_parameter(
                config, section, "PRIMARY_IDNUM", int, None)
            self.require_idnum_mandatory = get_config_parameter_boolean(
                config, section, "REQUIRE_PRIMARY_IDNUM_MANDATORY_IN_POLICY",
                True)
            sd = get_config_parameter(
                config, section, "START_DATE", str, None)
            self.start_date = cc_dt.get_date_from_string(sd)
            ed = get_config_parameter(
                config, section, "END_DATE", str, None)
            self.end_date = cc_dt.get_date_from_string(ed)
            self.finalized_only = get_config_parameter_boolean(
                config, section, "FINALIZED_ONLY", True)
            self.task_format = get_config_parameter(
                config, section, "TASK_FORMAT", str, VALUE.OUTPUTTYPE_PDF)
            self.xml_field_comments = get_config_parameter_boolean(
                config, section, "XML_FIELD_COMMENTS", True)

            # HL7
            if self.using_hl7():
                self.host = get_config_parameter(
                    config, section, "HOST", str, None)
                self.port = get_config_parameter(
                    config, section, "PORT", int, DEFAULT_HL7_PORT)
                self.ping_first = get_config_parameter_boolean(
                    config, section, "PING_FIRST", True)
                self.network_timeout_ms = get_config_parameter(
                    config, section, "NETWORK_TIMEOUT_MS", int, 10000)
                for n in range(1, NUMBER_OF_IDNUMS + 1):
                    i = n - 1
                    nstr = str(n)
                    self.idnum_type_list[i] = get_config_parameter(
                        config, section, "IDNUM_TYPE_" + nstr, str, "")
                    self.idnum_aa_list[i] = get_config_parameter(
                        config, section, "IDNUM_AA_" + nstr, str, "")
                self.keep_message = get_config_parameter_boolean(
                    config, section, "KEEP_MESSAGE", False)
                self.keep_reply = get_config_parameter_boolean(
                    config, section, "KEEP_REPLY", False)
                self.divert_to_file = get_config_parameter(
                    config, section, "DIVERT_TO_FILE", str, None)
                self.treat_diverted_as_sent = get_config_parameter_boolean(
                    config, section, "TREAT_DIVERTED_AS_SENT", False)
                if self.divert_to_file:
                    self.host = None
                    self.port = None
                    self.ping_first = None
                    self.network_timeout_ms = None
                    self.keep_reply = None
                self.include_anonymous = False

            # File
            if self.using_file():
                self.include_anonymous = get_config_parameter_boolean(
                    config, section, "INCLUDE_ANONYMOUS", False)
                self.patient_spec_if_anonymous = get_config_parameter(
                    config, section, "PATIENT_SPEC_IF_ANONYMOUS", str,
                    "anonymous")
                self.patient_spec = get_config_parameter(
                    config, section, "PATIENT_SPEC", str, None)
                self.filename_spec = get_config_parameter(
                    config, section, "FILENAME_SPEC", str, None)
                self.overwrite_files = get_config_parameter_boolean(
                    config, section, "OVERWRITE_FILES", False)
                self.make_directory = get_config_parameter_boolean(
                    config, section, "MAKE_DIRECTORY", False)
                self.rio_metadata = get_config_parameter_boolean(
                    config, section, "RIO_METADATA", False)
                self.rio_idnum = get_config_parameter(
                    config, section, "RIO_IDNUM", int, None)
                self.rio_uploading_user = get_config_parameter(
                    config, section, "RIO_UPLOADING_USER", str, None)
                self.rio_document_type = get_config_parameter(
                    config, section, "RIO_DOCUMENT_TYPE", str, None)
                self.script_after_file_export = get_config_parameter(
                    config, section, "SCRIPT_AFTER_FILE_EXPORT", str, None)

            self.check_valid()

        except ConfigParser.NoSectionError:
            logger.warning("Config file section missing: [{}]".format(
                section
            ))
            self.valid = False
Esempio n. 3
0
    def set_webview(self, environ, config):
        # ---------------------------------------------------------------------
        # Delayed imports
        # ---------------------------------------------------------------------
        import cgi
        import operator
        import os
        import urllib

        import cc_filename
        import cc_html  # caution, circular import
        import cc_policy
        import cc_namedtuples
        import cc_recipdef
        import cc_version

        logger = cc_logger.logger

        # ---------------------------------------------------------------------
        # Read from the environment
        # ---------------------------------------------------------------------
        # http://www.zytrax.com/tech/web/env_var.htm
        # Apache standard CGI variables:
        self.SCRIPT_NAME = environ.get("SCRIPT_NAME", "")
        self.SERVER_NAME = environ.get("SERVER_NAME")

        # Reconstruct URL:
        # http://www.python.org/dev/peps/pep-0333/#url-reconstruction
        url = environ.get("wsgi.url_scheme", "") + "://"
        if environ.get("HTTP_HOST"):
            url += environ.get("HTTP_HOST")
        else:
            url += environ.get("SERVER_NAME", "")
        if environ.get("wsgi.url_scheme") == "https":
            if environ.get("SERVER_PORT") != "443":
                url += ":" + environ.get("SERVER_PORT", "")
        else:
            if environ.get("SERVER_PORT") != "80":
                url += ":" + environ.get("SERVER_PORT", "")
        url += urllib.quote(environ.get("SCRIPT_NAME", ""))
        url += urllib.quote(environ.get("PATH_INFO", ""))
        # But not the query string:
        # if environ.get("QUERY_STRING"):
        #    url += "?" + environ.get("QUERY_STRING")

        self.SCRIPT_PUBLIC_URL_ESCAPED = cgi.escape(url)

        # ---------------------------------------------------------------------
        # Read from the config file:
        # ---------------------------------------------------------------------
        section = CONFIG_FILE_MAIN_SECTION
        self.MYSQL = get_config_parameter(config, section, "MYSQL", str, DEFAULT_MYSQL)
        self.MYSQLDUMP = get_config_parameter(config, section, "MYSQLDUMP", str, DEFAULT_MYSQLDUMP)

        self.LOCAL_INSTITUTION_URL = get_config_parameter(
            config, section, "LOCAL_INSTITUTION_URL", str, DEFAULT_LOCAL_INSTITUTION_URL
        )
        # note order dependency: RESOURCES_DIRECTORY, LOCAL_LOGO_FILE_ABSOLUTE
        self.RESOURCES_DIRECTORY = get_config_parameter(
            config, section, "RESOURCES_DIRECTORY", str, DEFAULT_RESOURCES_DIRECTORY
        )
        self.LOCAL_LOGO_FILE_ABSOLUTE = get_config_parameter(
            config,
            section,
            "LOCAL_LOGO_FILE_ABSOLUTE",
            str,
            os.path.join(self.RESOURCES_DIRECTORY, LOCAL_LOGO_FILE_WEBREF),
        )
        self.INTROSPECTION_DIRECTORY = get_config_parameter(
            config, section, "INTROSPECTION_DIRECTORY", str, DEFAULT_INTROSPECTION_DIRECTORY
        )
        self.INTROSPECTION = get_config_parameter_boolean(config, section, "INTROSPECTION", True)
        self.HL7_LOCKFILE = get_config_parameter(config, section, "HL7_LOCKFILE", str, None)
        self.SUMMARY_TABLES_LOCKFILE = get_config_parameter(config, section, "SUMMARY_TABLES_LOCKFILE", str, None)

        self.PASSWORD_CHANGE_FREQUENCY_DAYS = get_config_parameter(
            config, section, "PASSWORD_CHANGE_FREQUENCY_DAYS", int, DEFAULT_PASSWORD_CHANGE_FREQUENCY_DAYS
        )
        self.LOCKOUT_THRESHOLD = get_config_parameter(
            config, section, "LOCKOUT_THRESHOLD", int, DEFAULT_LOCKOUT_THRESHOLD
        )
        self.LOCKOUT_DURATION_INCREMENT_MINUTES = get_config_parameter(
            config, section, "LOCKOUT_DURATION_INCREMENT_MINUTES", int, DEFAULT_LOCKOUT_DURATION_INCREMENT_MINUTES
        )
        self.DISABLE_PASSWORD_AUTOCOMPLETE = get_config_parameter_boolean(
            config, section, "DISABLE_PASSWORD_AUTOCOMPLETE", True
        )

        self.PATIENT_SPEC_IF_ANONYMOUS = get_config_parameter(
            config, section, "PATIENT_SPEC_IF_ANONYMOUS", str, "anonymous"
        )
        self.PATIENT_SPEC = get_config_parameter(config, section, "PATIENT_SPEC", str, None)
        self.TASK_FILENAME_SPEC = get_config_parameter(config, section, "TASK_FILENAME_SPEC", str, None)
        self.TRACKER_FILENAME_SPEC = get_config_parameter(config, section, "TRACKER_FILENAME_SPEC", str, None)
        self.CTV_FILENAME_SPEC = get_config_parameter(config, section, "CTV_FILENAME_SPEC", str, None)

        self.WEBVIEW_LOGLEVEL = get_config_parameter_loglevel(config, section, "WEBVIEW_LOGLEVEL", logging.INFO)
        logger.setLevel(self.WEBVIEW_LOGLEVEL)

        self.SEND_ANALYTICS = get_config_parameter_boolean(config, section, "SEND_ANALYTICS", True)

        self.EXPORT_CRIS_DATA_DICTIONARY_TSV_FILE = get_config_parameter(
            config, section, "EXPORT_CRIS_DATA_DICTIONARY_TSV_FILE", str, None
        )

        # http://stackoverflow.com/questions/335695/lists-in-configparser
        try:
            hl7_items = config.items(CONFIG_FILE_RECIPIENTLIST_SECTION)
            for key, recipientdef_name in hl7_items:
                logger.debug(u"HL7 config: key={}, recipientdef_name=" "{}".format(key, recipientdef_name))
                h = cc_recipdef.RecipientDefinition(config, recipientdef_name)
                if h.valid:
                    self.HL7_RECIPIENT_DEFS.append(h)
        except ConfigParser.NoSectionError:
            logger.info("No config file section [{}]".format(CONFIG_FILE_RECIPIENTLIST_SECTION))

        # ---------------------------------------------------------------------
        # Built from the preceding:
        # ---------------------------------------------------------------------

        self.INTROSPECTION_FILES = []
        if self.INTROSPECTION:
            rootdir = self.INTROSPECTION_DIRECTORY
            for d in INTROSPECTABLE_DIRECTORIES:
                searchdir = os.sep.join([rootdir, d]) if d else rootdir
                for fname in os.listdir(searchdir):
                    junk, ext = os.path.splitext(fname)
                    if ext not in INTROSPECTABLE_EXTENSIONS:
                        continue
                    fullpath = os.sep.join([searchdir, fname])
                    prettypath = os.sep.join([d, fname]) if d else fname
                    self.INTROSPECTION_FILES.append(
                        cc_namedtuples.IntrospectionFileDetails(
                            fullpath=fullpath, prettypath=prettypath, searchterm=fname, ext=ext
                        )
                    )
            self.INTROSPECTION_FILES = sorted(self.INTROSPECTION_FILES, key=operator.attrgetter("prettypath"))

        # Cache tokenized ID policies
        cc_policy.tokenize_upload_id_policy(self.ID_POLICY_UPLOAD_STRING)
        cc_policy.tokenize_finalize_id_policy(self.ID_POLICY_FINALIZE_STRING)
        # Valid?
        if not cc_policy.upload_id_policy_valid():
            raise RuntimeError("UPLOAD_POLICY invalid in config")
        if not cc_policy.finalize_id_policy_valid():
            raise RuntimeError("FINALIZE_POLICY invalid in config")

        if self.RESOURCES_DIRECTORY is not None:
            self.CAMCOPS_STRINGS_FILE_ABSOLUTE = os.path.join(self.RESOURCES_DIRECTORY, CAMCOPS_STRINGS_FILE)
            self.CAMCOPS_LOGO_FILE_ABSOLUTE = os.path.join(self.RESOURCES_DIRECTORY, CAMCOPS_LOGO_FILE_WEBREF)

        # Note: HTML4 uses <img ...>; XHTML uses <img ... />;
        # HTML5 is happy with <img ... />

        # IE float-right problems: http://stackoverflow.com/questions/1820007
        # Tables are a nightmare in IE (table max-width not working unless you
        # also specify it for image size, etc.)
        self.WEB_LOGO = u"""
            <div class="web_logo_header">
                <a href="{}"><img class="logo_left" src="{}" alt="" /></a>
                <a href="{}"><img class="logo_right" src="{}" alt="" /></a>
            </div>
        """.format(
            self.SCRIPT_NAME, CAMCOPS_LOGO_FILE_WEBREF, self.LOCAL_INSTITUTION_URL, LOCAL_LOGO_FILE_WEBREF
        )

        self.WEBSTART = cc_html.WEB_HEAD + self.WEB_LOGO

        if cc_version.PDF_ENGINE in ["weasyprint", "pdfkit"]:
            # weasyprint: div with floating img does not work properly
            self.PDF_LOGO_LINE = u"""
                <div class="pdf_logo_header">
                    <table>
                        <tr>
                            <td class="image_td">
                                <img class="logo_left" src="file://{}" />
                            </td>
                            <td class="centregap_td"></td>
                            <td class="image_td">
                                <img class="logo_right" src="file://{}" />
                            </td>
                        </tr>
                    </table>
                </div>
            """.format(
                self.CAMCOPS_LOGO_FILE_ABSOLUTE, self.LOCAL_LOGO_FILE_ABSOLUTE
            )
        elif cc_version.PDF_ENGINE in ["pdfkit"]:
            self.PDF_LOGO_LINE = u"""
                <div class="pdf_logo_header">
                    <table>
                        <tr>
                            <td class="image_td">
                                <img class="logo_left" src="file://{}" />
                            </td>
                            <td class="centregap_td"></td>
                            <td class="image_td">
                                <img class="logo_right" src="file://{}" />
                            </td>
                        </tr>
                    </table>
                </div>
            """.format(
                self.CAMCOPS_LOGO_FILE_ABSOLUTE, self.LOCAL_LOGO_FILE_ABSOLUTE
            )
            # self.PDF_LOGO_LINE = u"""
            #     <div class="pdf_logo_header">
            #         <img class="logo_left" src="file://{}" />
            #         <img class="logo_right" src="file://{}" />
            #     </div>
            # """.format(
            #     self.CAMCOPS_LOGO_FILE_ABSOLUTE,
            #     self.LOCAL_LOGO_FILE_ABSOLUTE,
            # )
        elif cc_version.PDF_ENGINE in ["xhtml2pdf"]:
            # xhtml2pdf
            # hard to get logos positioned any other way than within a table
            self.PDF_LOGO_LINE = u"""
                <div class="header">
                    <table class="noborder">
                        <tr class="noborder">
                            <td class="noborderphoto" width="45%">
                                <img src="file://{}" height="{}"
                                        align="left" />
                            </td>
                            <td class="noborderphoto" width="10%"></td>
                            <td class="noborderphoto" width="45%">
                                <img src="file://{}" height="{}"
                                        align="right" />
                            </td>
                        </tr>
                    </table>
                </div>
            """.format(
                self.CAMCOPS_LOGO_FILE_ABSOLUTE,
                cc_html.PDF_LOGO_HEIGHT,
                self.LOCAL_LOGO_FILE_ABSOLUTE,
                cc_html.PDF_LOGO_HEIGHT,
            )
        else:
            raise AssertionError("Invalid PDF engine")

        if not self.PATIENT_SPEC_IF_ANONYMOUS:
            raise RuntimeError("Blank PATIENT_SPEC_IF_ANONYMOUS in [server] " "section of config file")

        if not self.PATIENT_SPEC:
            raise RuntimeError("Missing/blank PATIENT_SPEC in [server] section" " of config file")
        if not cc_filename.patient_spec_for_filename_is_valid(self.PATIENT_SPEC):
            raise RuntimeError("Invalid PATIENT_SPEC in [server] section of " "config file")

        if not self.TASK_FILENAME_SPEC:
            raise RuntimeError("Missing/blank TASK_FILENAME_SPEC in " "[server] section of config file")
        if not cc_filename.filename_spec_is_valid(self.TASK_FILENAME_SPEC):
            raise RuntimeError("Invalid TASK_FILENAME_SPEC in " "[server] section of config file")

        if not self.TRACKER_FILENAME_SPEC:
            raise RuntimeError("Missing/blank TRACKER_FILENAME_SPEC in " "[server] section of config file")
        if not cc_filename.filename_spec_is_valid(self.TRACKER_FILENAME_SPEC):
            raise RuntimeError("Invalid TRACKER_FILENAME_SPEC in " "[server] section of config file")

        if not self.CTV_FILENAME_SPEC:
            raise RuntimeError("Missing/blank CTV_FILENAME_SPEC in " "[server] section of config file")
        if not cc_filename.filename_spec_is_valid(self.CTV_FILENAME_SPEC):
            raise RuntimeError("Invalid CTV_FILENAME_SPEC in " "[server] section of config file")
Esempio n. 4
0
    def set_common(self, environ, config, as_client_db):
        # logger = cc_logger.dblogger if as_client_db else cc_logger.logger
        # ---------------------------------------------------------------------
        # Read from the config file:
        # ---------------------------------------------------------------------
        section = CONFIG_FILE_MAIN_SECTION

        SESSION_TIMEOUT_MINUTES = get_config_parameter(
            config, section, "SESSION_TIMEOUT_MINUTES", int, DEFAULT_TIMEOUT_MINUTES
        )
        self.SESSION_TIMEOUT = datetime.timedelta(minutes=SESSION_TIMEOUT_MINUTES)

        self.EXTRA_STRING_FILES = get_config_parameter_multiline(config, section, "EXTRA_STRING_FILES", [])

        self.DB_NAME = config.get(section, "DB_NAME")
        # ... no default: will fail if not provided
        self.DB_USER = config.get(section, "DB_USER")
        # ... no default: will fail if not provided
        # DB_PASSWORD: handled later, for security reasons (see below)
        self.DB_SERVER = get_config_parameter(config, section, "DB_SERVER", str, DEFAULT_DB_SERVER)
        self.DB_PORT = get_config_parameter(config, section, "DB_PORT", int, DEFAULT_DB_PORT)

        self.DATABASE_TITLE = get_config_parameter(config, section, "DATABASE_TITLE", unicode, DEFAULT_DATABASE_TITLE)
        for n in range(1, NUMBER_OF_IDNUMS + 1):
            i = n - 1
            nstr = str(n)
            self.IDDESC[i] = get_config_parameter(config, section, "IDDESC_" + nstr, unicode, u"")
            self.IDSHORTDESC[i] = get_config_parameter(config, section, "IDSHORTDESC_" + nstr, unicode, u"")
        self.ID_POLICY_UPLOAD_STRING = get_config_parameter(config, section, "UPLOAD_POLICY", str, "")
        self.ID_POLICY_FINALIZE_STRING = get_config_parameter(config, section, "FINALIZE_POLICY", str, "")

        self.DBENGINE_LOGLEVEL = get_config_parameter_loglevel(config, section, "DBENGINE_LOGLEVEL", logging.INFO)
        rnc_db.set_loglevel(self.DBENGINE_LOGLEVEL)

        self.WKHTMLTOPDF_FILENAME = get_config_parameter(config, section, "WKHTMLTOPDF_FILENAME", str, None)
        rnc_pdf.set_processor(cc_version.PDF_ENGINE, wkhtmltopdf_filename=self.WKHTMLTOPDF_FILENAME)

        # ---------------------------------------------------------------------
        # SECURITY: in this section (reading the database password from the
        # config file and connecting to the database), consider the possibility
        # of a password leaking via a debugging exception handler. This
        # includes the possibility that the database code will raise an
        # exception that reveals the password, so we must replace all
        # exceptions with our own, bland one. In addition, we must obscure the
        # variable that actually contains the password, in all circumstances.
        # ---------------------------------------------------------------------
        try:
            db_password = config.get(section, "DB_PASSWORD")
        except:  # deliberately conceal details for security
            db_password = None
            raise RuntimeError("Problem reading DB_PASSWORD from config")

        if db_password is None:
            raise RuntimeError("No database password specified")
            # OK from a security perspective: if there's no password, there's
            # no password to leak via a debugging exception handler

        # Now connect to the database:
        try:
            self.db = rnc_db.DatabaseSupporter()
            # To generate a password-leak situation, e.g. mis-spell "password"
            # in the call below. If the exception is not caught,
            # wsgi_errorreporter.py will announce the password.
            # So we catch it!
            self.db.connect_to_database_mysql(
                server=self.DB_SERVER,
                port=self.DB_PORT,
                database=self.DB_NAME,
                user=self.DB_USER,
                password=db_password,
                autocommit=False  # NB therefore need to commit
                # ... done in camcops.py at the end of a session
            )
        except:  # deliberately conceal details for security
            raise rnc_db.NoDatabaseError(
                "Problem opening or reading from database; details concealed " "for security reasons"
            )
        finally:
            # Executed whether an exception is raised or not.
            db_password = None