def make_superuser(username: str = None) -> bool:
    """
    Make a superuser from the command line.
    """
    with command_line_request_context() as req:
        username = get_username_from_cli(
            req=req,
            prompt="Username for new superuser (or to gain superuser status)",
            starting_username=username,
        )
        existing_user = User.get_user_by_name(req.dbsession, username)
        if existing_user:
            log.info("Giving superuser status to {!r}", username)
            existing_user.superuser = True
            success = True
        else:
            log.info("Creating superuser {!r}", username)
            password = get_new_password_from_cli(username=username)
            success = User.create_superuser(req, username, password)
        if success:
            log.info("Success")
            return True
        else:
            log.critical("Failed to create superuser")
            return False
Beispiel #2
0
    def create_user(self, **kwargs) -> User:
        user = User()
        user.hashedpw = ""

        for key, value in kwargs.items():
            setattr(user, key, value)

        self.dbsession.add(user)

        return user
Beispiel #3
0
    def test_user(self) -> None:
        self.announce("test_user")
        req = self.req

        SecurityAccountLockout.delete_old_account_lockouts(req)
        self.assertIsInstance(
            SecurityAccountLockout.is_user_locked_out(req, "dummy_user"), bool)
        self.assertIsInstanceOrNone(
            SecurityAccountLockout.user_locked_out_until(req, "dummy_user"),
            Pendulum,
        )

        self.assertIsInstance(
            SecurityLoginFailure.how_many_login_failures(req, "dummy_user"),
            int,
        )
        SecurityLoginFailure.clear_login_failures_for_nonexistent_users(req)
        SecurityLoginFailure.clear_dummy_login_failures_if_necessary(req)
        SecurityLoginFailure.clear_dummy_login_failures_if_necessary(req)
        # ... do it twice (we had a bug relating to offset-aware vs
        # offset-naive date/time objects).

        self.assertIsInstance(User.is_username_permissible("some_user"), bool)
        User.take_some_time_mimicking_password_encryption()

        u = self.dbsession.query(User).first()  # type: User
        assert u, "Missing user in demo database!"

        g = self.dbsession.query(Group).first()  # type: Group
        assert g, "Missing group in demo database!"

        self.assertIsInstance(u.is_password_correct("dummy_password"), bool)
        self.assertIsInstance(u.must_agree_terms, bool)
        u.agree_terms(req)
        u.clear_login_failures(req)
        self.assertIsInstance(u.is_locked_out(req), bool)
        self.assertIsInstanceOrNone(u.locked_out_until(req), Pendulum)
        u.enable(req)
        self.assertIsInstance(u.may_login_as_tablet, bool)
        # TODO: cc_user.UserTests: could do more here
        self.assertIsInstance(u.authorized_as_groupadmin, bool)
        self.assertIsInstance(u.may_use_webviewer, bool)
        self.assertIsInstance(u.authorized_to_add_special_note(g.id), bool)
        self.assertIsInstance(u.authorized_to_erase_tasks(g.id), bool)
        self.assertIsInstance(u.authorized_to_dump, bool)
        self.assertIsInstance(u.authorized_for_reports, bool)
        self.assertIsInstance(u.may_view_all_patients_when_unfiltered, bool)
        self.assertIsInstance(u.may_view_no_patients_when_unfiltered, bool)
        self.assertIsInstance(u.may_upload_to_group(g.id), bool)
        self.assertIsInstance(u.may_upload, bool)
        self.assertIsInstance(u.may_register_devices, bool)
Beispiel #4
0
 def _login_from_ts(self, ts: "TabletSession") -> None:
     """
     Used by :meth:`get_session_for_tablet` to log in using information
     provided by a
     :class:`camcops_server.cc_modules.cc_tabletsession.TabletSession`.
     """
     if DEBUG_CAMCOPS_SESSION_CREATION:
         log.debug(
             "Considering login from tablet (with username: {!r}",
             ts.username,
         )
     self.is_api_session = True
     if ts.username:
         user = User.get_user_from_username_password(
             ts.req, ts.username, ts.password)
         if DEBUG_CAMCOPS_SESSION_CREATION:
             log.debug("... looked up User: {!r}", user)
         if user:
             # Successful login of sorts, ALTHOUGH the user may be
             # severely restricted (if they can neither register nor
             # upload). However, effecting a "login" here means that the
             # error messages can become more helpful!
             self.login(user)
     if DEBUG_CAMCOPS_SESSION_CREATION:
         log.debug("... final session user: {!r}", self.user)
Beispiel #5
0
    def get_device(self, dbsession: "SqlASession") -> "Device":
        dummy_device_name = "dummy_device"

        device = Device.get_device_by_name(dbsession, dummy_device_name)
        if device is None:
            device = Device()
            device.name = dummy_device_name
            device.friendly_name = "Dummy tablet device"
            device.registered_by_user = User.get_system_user(dbsession)
            device.when_registered_utc = pendulum.DateTime.utcnow()
            device.camcops_version = CAMCOPS_SERVER_VERSION
            dbsession.add(device)
            dbsession.flush()  # So that we can use the PK elsewhere
        return device
Beispiel #6
0
 def get_server_device(cls, dbsession: SqlASession) -> "Device":
     """
     Return the special device meaning "the server", creating it if it
     doesn't already exist.
     """
     device = cls.get_device_by_name(dbsession, DEVICE_NAME_FOR_SERVER)
     if device is None:
         device = Device()
         device.name = DEVICE_NAME_FOR_SERVER
         device.friendly_name = "CamCOPS server"
         device.registered_by_user = User.get_system_user(dbsession)
         device.when_registered_utc = Pendulum.utcnow()
         device.camcops_version = CAMCOPS_SERVER_VERSION
         dbsession.add(device)
     return device
def enable_user_cli(username: str = None) -> bool:
    """
    Re-enable a locked user account from the command line.
    """
    with command_line_request_context() as req:
        if username is None:
            username = get_username_from_cli(
                req=req, prompt="Username to unlock", must_exist=True
            )
        else:
            if not User.user_exists(req, username):
                log.critical("No such user: {!r}", username)
                return False
        SecurityLoginFailure.enable_user(req, username)
        log.info("Enabled.")
        return True
Beispiel #8
0
 def login_from_ts(cc: "CamcopsSession", ts_: "TabletSession") -> None:
     if DEBUG_CAMCOPS_SESSION_CREATION:
         log.debug("Considering login from tablet (with username: {!r}",
                   ts_.username)
     if ts_.username:
         user = User.get_user_from_username_password(
             ts.req, ts.username, ts.password)
         if DEBUG_CAMCOPS_SESSION_CREATION:
             log.debug("... looked up User: {!r}", user)
         if user:
             # Successful login of sorts, ALTHOUGH the user may be
             # severely restricted (if they can neither register nor
             # upload). However, effecting a "login" here means that the
             # error messages can become more helpful!
             cc.login(user)
     if DEBUG_CAMCOPS_SESSION_CREATION:
         log.debug("... final session user: {!r}", cc.user)
def get_username_from_cli(
    req: CamcopsRequest,
    prompt: str,
    starting_username: str = "",
    must_exist: bool = False,
    must_not_exist: bool = False,
) -> str:
    """
    Asks the user (via stdout/stdin) for a username.

    Args:
        req: CamcopsRequest object
        prompt: textual prompt
        starting_username: try this username and ask only if it fails tests
        must_exist: the username must exist
        must_not_exist: the username must not exist

    Returns:
        the username

    """
    assert not (must_exist and must_not_exist)
    first = True
    while True:
        if first:
            username = starting_username
            first = False
        else:
            username = ""
        username = username or ask_user(prompt)
        exists = User.user_exists(req, username)
        if must_not_exist and exists:
            log.error("... user already exists!")
            continue
        if must_exist and not exists:
            log.error("... no such user!")
            continue
        if username == USER_NAME_FOR_SYSTEM:
            log.error("... username {!r} is reserved", USER_NAME_FOR_SYSTEM)
            continue
        return username
Beispiel #10
0
    def add_data(self) -> None:
        # noinspection PyTypeChecker
        next_id = self.next_id(Group.id)

        self.group = Group()
        self.group.name = f"dummygroup{next_id}"
        self.group.description = "Dummy group"
        self.group.upload_policy = "sex AND anyidnum"
        self.group.finalize_policy = "sex AND idnum1001"
        self.dbsession.add(self.group)
        self.dbsession.commit()  # sets PK fields

        self.user = User.get_system_user(self.dbsession)
        self.user.upload_group_id = self.group.id

        self.device = self.get_device(self.dbsession)
        self.dbsession.commit()

        self.nhs_iddef = IdNumDefinition(
            which_idnum=1001,
            description="NHS number (TEST)",
            short_description="NHS#",
            hl7_assigning_authority="NHS",
            hl7_id_type="NHSN",
        )
        self.dbsession.add(self.nhs_iddef)
        try:
            self.dbsession.commit()
        except IntegrityError:
            self.dbsession.rollback()

        for patient_id in range(
            self.FIRST_PATIENT_ID, self.FIRST_PATIENT_ID + self.NUM_PATIENTS
        ):
            Faker.seed(patient_id)
            self.add_patient(patient_id)
            log.info(f"Adding tasks for patient {patient_id}")

            Faker.seed()
            self.add_tasks(patient_id)
Beispiel #11
0
def merge_camcops_db(
    src: str,
    echo: bool,
    report_every: int,
    dummy_run: bool,
    info_only: bool,
    default_group_id: Optional[int],
    default_group_name: Optional[str],
    groupnum_map: Dict[int, int],
    whichidnum_map: Dict[int, int],
    skip_export_logs: bool = True,
    skip_audit_logs: bool = True,
) -> None:
    """
    Merge an existing database (with a pre-v2 or later structure) into a
    comtemporary CamCOPS database.

    Args:
        src:
            source database SQLAlchemy URL

        echo:
            echo the SQL that is produced?

        report_every:
            provide a progress report every *n* records

        dummy_run:
            don't alter the destination database

        info_only:
            show info, then stop

        default_group_id:
            integer group ID (in the destination database) to use for source
            records that have no group (because they come from a very old
            source database) but need one

        default_group_name:
            group name (in the destination database) to use for source
            records that have no group (because they come from a very old
            source database) but need one

        groupnum_map:
            dictionary mapping group ID values from the source database to
            the destination database

        whichidnum_map:
            dictionary mapping ``which_idnum`` values from the source database
            to the destination database

        skip_export_logs:
            skip export log tables

        skip_audit_logs:
            skip audit log table

    """
    req = get_command_line_request()  # requires manual COMMIT; see below
    src_engine = create_engine(src, echo=echo, pool_pre_ping=True)
    log.info("SOURCE: " + get_safe_url_from_engine(src_engine))
    log.info("DESTINATION: " + get_safe_url_from_engine(req.engine))
    log.info(
        "Destination ID number type map (source:destination) is: {!r}",
        whichidnum_map,
    )
    log.info("Group number type map (source:destination) is {!r}",
             groupnum_map)

    # Delay the slow import until we've checked our syntax
    log.info("Loading all models...")
    # noinspection PyUnresolvedReferences
    import camcops_server.cc_modules.cc_all_models  # delayed import  # import side effects (ensure all models registered)  # noqa

    log.info("Models loaded.")

    # Now, any special dependencies?
    # From the point of view of translating any tablet-related fields, the
    # actual (server) PK values are irrelevant; all relationships will be
    # identical if you change any PK (not standard database practice, but
    # convenient here).
    # The dependencies that do matter are server-side things, like user_id
    # variables.

    # For debugging only, some junk:
    # test_dependencies = [
    #     TableDependency(parent_tablename="patient",
    #                     child_tablename="_dirty_tables")
    # ]

    # -------------------------------------------------------------------------
    # Tables to skip
    # -------------------------------------------------------------------------

    skip_tables = [
        # Transient stuff we don't want to copy across, or wouldn't want to
        # overwrite the destination with, or where the PK structure has
        # changed and we don't care about old data:
        TableIdentity(tablename=x) for x in (
            CamcopsSession.__tablename__,
            DirtyTable.__tablename__,
            ServerSettings.__tablename__,
            SecurityAccountLockout.__tablename__,
            SecurityLoginFailure.__tablename__,
            UserGroupMembership.__tablename__,
            group_group_table.name,
        )
    ]

    # Tedious and bulky stuff the user may want to skip:
    if skip_export_logs:
        skip_tables.extend([
            TableIdentity(tablename=x) for x in (
                Email.__tablename__,
                ExportRecipient.__tablename__,
                ExportedTask.__tablename__,
                ExportedTaskEmail.__tablename__,
                ExportedTaskFileGroup.__tablename__,
                ExportedTaskHL7Message.__tablename__,
            )
        ])
    if skip_audit_logs:
        skip_tables.append(TableIdentity(tablename=AuditEntry.__tablename__))

    # -------------------------------------------------------------------------
    # Initial operations on SOURCE database
    # -------------------------------------------------------------------------

    src_tables = get_table_names(src_engine)
    skip_tables += get_skip_tables(src_tables=src_tables)
    src_iddefs = get_src_iddefs(src_engine, src_tables)
    log.info("Source ID number definitions: {!r}", src_iddefs)

    # -------------------------------------------------------------------------
    # Initial operations on DESTINATION database
    # -------------------------------------------------------------------------
    dst_session = req.dbsession
    # So that system users get the first ID (cosmetic!):
    _ = User.get_system_user(dbsession=dst_session)
    _ = Device.get_server_device(dbsession=dst_session)

    # -------------------------------------------------------------------------
    # Set up source-to-destination mappings
    # -------------------------------------------------------------------------

    # Map source to destination ID number types
    for src_which_idnum, dest_which_idnum in whichidnum_map.items():
        assert isinstance(src_which_idnum, int)
        assert isinstance(dest_which_idnum, int)
        src_iddef = src_iddefs[src_which_idnum]
        dst_iddef = ensure_dest_iddef_exists(dest_which_idnum, dst_session)
        ensure_no_iddef_clash(src_iddef, dst_iddef)

    # Map source to destination group numbers
    for src_groupnum, dest_groupnum in groupnum_map.items():
        assert isinstance(src_groupnum, int)
        assert isinstance(dest_groupnum, int)
        _ = get_dst_group(dest_groupnum, dst_session)

    # -------------------------------------------------------------------------
    # Merge
    # -------------------------------------------------------------------------

    # Merge! It's easy...
    trcon_info = dict(
        default_group_id=default_group_id,
        default_group_name=default_group_name,
        src_iddefs=src_iddefs,
        whichidnum_map=whichidnum_map,
        groupnum_map=groupnum_map,
    )
    merge_db(
        base_class=Base,
        src_engine=src_engine,
        dst_session=dst_session,
        allow_missing_src_tables=True,
        allow_missing_src_columns=True,
        translate_fn=translate_fn,
        skip_tables=skip_tables,
        only_tables=None,
        tables_to_keep_pks_for=None,
        # extra_table_dependencies=test_dependencies,
        extra_table_dependencies=None,
        dummy_run=dummy_run,
        info_only=info_only,
        report_every=report_every,
        flush_per_table=True,
        flush_per_record=False,
        commit_with_flush=False,
        commit_at_end=True,
        prevent_eager_load=True,
        trcon_info=trcon_info,
    )

    # -------------------------------------------------------------------------
    # Postprocess
    # -------------------------------------------------------------------------

    postprocess(src_engine=src_engine, dst_session=dst_session)

    # -------------------------------------------------------------------------
    # Done
    # -------------------------------------------------------------------------

    dst_session.commit()
Beispiel #12
0
    def setUp(self) -> None:
        super().setUp()
        from cardinal_pythonlib.datetimefunc import (
            convert_datetime_to_utc,
            format_datetime,
        )
        from camcops_server.cc_modules.cc_blob import Blob
        from camcops_server.cc_modules.cc_constants import DateFormat
        from camcops_server.cc_modules.cc_device import Device
        from camcops_server.cc_modules.cc_group import Group
        from camcops_server.cc_modules.cc_patient import Patient
        from camcops_server.cc_modules.cc_patientidnum import PatientIdNum
        from camcops_server.cc_modules.cc_task import Task
        from camcops_server.cc_modules.cc_user import User
        from camcops_server.tasks.photo import Photo

        Base.metadata.create_all(self.engine)

        self.era_time = pendulum.parse("2010-07-07T13:40+0100")
        self.era_time_utc = convert_datetime_to_utc(self.era_time)
        self.era = format_datetime(self.era_time, DateFormat.ISO8601)

        # Set up groups, users, etc.
        # ... ID number definitions
        iddef1 = IdNumDefinition(which_idnum=1,
                                 description="NHS number",
                                 short_description="NHS#",
                                 hl7_assigning_authority="NHS",
                                 hl7_id_type="NHSN")
        self.dbsession.add(iddef1)
        iddef2 = IdNumDefinition(which_idnum=2,
                                 description="RiO number",
                                 short_description="RiO",
                                 hl7_assigning_authority="CPFT",
                                 hl7_id_type="CPFT_RiO")
        self.dbsession.add(iddef2)
        # ... group
        self.group = Group()
        self.group.name = "testgroup"
        self.group.description = "Test group"
        self.group.upload_policy = "sex AND anyidnum"
        self.group.finalize_policy = "sex AND idnum1"
        self.dbsession.add(self.group)
        self.dbsession.flush()  # sets PK fields

        # ... users

        self.user = User.get_system_user(self.dbsession)
        self.user.upload_group_id = self.group.id
        self.req._debugging_user = self.user  # improve our debugging user

        # ... devices
        self.server_device = Device.get_server_device(self.dbsession)
        self.other_device = Device()
        self.other_device.name = "other_device"
        self.other_device.friendly_name = "Test device that may upload"
        self.other_device.registered_by_user = self.user
        self.other_device.when_registered_utc = self.era_time_utc
        self.other_device.camcops_version = CAMCOPS_SERVER_VERSION
        self.dbsession.add(self.other_device)

        self.dbsession.flush()  # sets PK fields

        # Populate database with two of everything
        p1 = Patient()
        p1.id = 1
        self._apply_standard_db_fields(p1)
        p1.forename = "Forename1"
        p1.surname = "Surname1"
        p1.dob = pendulum.parse("1950-01-01")
        self.dbsession.add(p1)
        p1_idnum1 = PatientIdNum()
        p1_idnum1.id = 1
        self._apply_standard_db_fields(p1_idnum1)
        p1_idnum1.patient_id = p1.id
        p1_idnum1.which_idnum = iddef1.which_idnum
        p1_idnum1.idnum_value = 333
        self.dbsession.add(p1_idnum1)
        p1_idnum2 = PatientIdNum()
        p1_idnum2.id = 2
        self._apply_standard_db_fields(p1_idnum2)
        p1_idnum2.patient_id = p1.id
        p1_idnum2.which_idnum = iddef2.which_idnum
        p1_idnum2.idnum_value = 444
        self.dbsession.add(p1_idnum2)

        p2 = Patient()
        p2.id = 2
        self._apply_standard_db_fields(p2)
        p2.forename = "Forename2"
        p2.surname = "Surname2"
        p2.dob = pendulum.parse("1975-12-12")
        self.dbsession.add(p2)
        p2_idnum1 = PatientIdNum()
        p2_idnum1.id = 3
        self._apply_standard_db_fields(p2_idnum1)
        p2_idnum1.patient_id = p2.id
        p2_idnum1.which_idnum = iddef1.which_idnum
        p2_idnum1.idnum_value = 555
        self.dbsession.add(p2_idnum1)

        self.dbsession.flush()

        for cls in Task.all_subclasses_by_tablename():
            t1 = cls()
            t1.id = 1
            self._apply_standard_task_fields(t1)
            if t1.has_patient:
                t1.patient_id = p1.id

            if isinstance(t1, Photo):
                b = Blob()
                b.id = 1
                self._apply_standard_db_fields(b)
                b.tablename = t1.tablename
                b.tablepk = t1.id
                b.fieldname = 'photo_blobid'
                b.filename = "some_picture.png"
                b.mimetype = MimeType.PNG
                b.image_rotation_deg_cw = 0
                b.theblob = DEMO_PNG_BYTES
                self.dbsession.add(b)

                t1.photo_blobid = b.id

            self.dbsession.add(t1)

            t2 = cls()
            t2.id = 2
            self._apply_standard_task_fields(t2)
            if t2.has_patient:
                t2.patient_id = p2.id
            self.dbsession.add(t2)

        self.dbsession.commit()
Beispiel #13
0
    def test_does_not_create_user_when_name_exists(self) -> None:
        from camcops_server.cc_modules.cc_taskindex import (
            PatientIdNumIndexEntry,
        )

        patient = self.create_patient(
            _group_id=self.group.id, as_server_patient=True
        )
        idnum = self.create_patient_idnum(
            patient_id=patient.id,
            which_idnum=self.nhs_iddef.which_idnum,
            idnum_value=TEST_NHS_NUMBER,
            as_server_patient=True,
        )
        PatientIdNumIndexEntry.index_idnum(idnum, self.dbsession)

        proquint = patient.uuid_as_proquint

        user = User(
            username=make_single_user_mode_username(
                self.other_device.name, patient._pk
            )
        )
        user.set_password(self.req, "old password")
        self.dbsession.add(user)
        self.dbsession.commit()

        self.req.fake_request_post_from_dict(
            {
                TabletParam.CAMCOPS_VERSION: MINIMUM_TABLET_VERSION,
                TabletParam.DEVICE: self.other_device.name,
                TabletParam.OPERATION: Operations.REGISTER_PATIENT,
                TabletParam.PATIENT_PROQUINT: proquint,
            }
        )
        response = client_api(self.req)
        reply_dict = get_reply_dict_from_response(response)

        self.assertEqual(
            reply_dict[TabletParam.SUCCESS], SUCCESS_CODE, msg=reply_dict
        )

        username = reply_dict[TabletParam.USER]
        self.assertEqual(
            username,
            make_single_user_mode_username(
                self.other_device.name, patient._pk
            ),
        )
        password = reply_dict[TabletParam.PASSWORD]
        self.assertEqual(len(password), 32)

        valid_chars = string.ascii_letters + string.digits + string.punctuation
        self.assertTrue(all(c in valid_chars for c in password))

        user = (
            self.req.dbsession.query(User)
            .filter(User.username == username)
            .one_or_none()
        )
        self.assertIsNotNone(user)
        self.assertEqual(user.upload_group, patient.group)
        self.assertTrue(user.auto_generated)
        self.assertTrue(user.may_register_devices)
        self.assertTrue(user.may_upload)
Beispiel #14
0
    def setUp(self) -> None:
        super().setUp()

        self.set_era("2010-07-07T13:40+0100")

        # Set up groups, users, etc.
        # ... ID number definitions
        idnum_type_nhs = 1
        idnum_type_rio = 2
        idnum_type_study = 3
        self.nhs_iddef = IdNumDefinition(
            which_idnum=idnum_type_nhs,
            description="NHS number",
            short_description="NHS#",
            hl7_assigning_authority="NHS",
            hl7_id_type="NHSN",
        )
        self.dbsession.add(self.nhs_iddef)
        self.rio_iddef = IdNumDefinition(
            which_idnum=idnum_type_rio,
            description="RiO number",
            short_description="RiO",
            hl7_assigning_authority="CPFT",
            hl7_id_type="CPRiO",
        )
        self.dbsession.add(self.rio_iddef)
        self.study_iddef = IdNumDefinition(
            which_idnum=idnum_type_study,
            description="Study number",
            short_description="Study",
        )
        self.dbsession.add(self.study_iddef)
        # ... group
        self.group = Group()
        self.group.name = "testgroup"
        self.group.description = "Test group"
        self.group.upload_policy = "sex AND anyidnum"
        self.group.finalize_policy = "sex AND idnum1"
        self.group.ip_use = IpUse()
        self.dbsession.add(self.group)
        self.dbsession.flush()  # sets PK fields

        # ... users

        self.user = User.get_system_user(self.dbsession)
        self.user.upload_group_id = self.group.id
        self.req._debugging_user = self.user  # improve our debugging user

        # ... devices
        self.server_device = Device.get_server_device(self.dbsession)
        self.other_device = Device()
        self.other_device.name = "other_device"
        self.other_device.friendly_name = "Test device that may upload"
        self.other_device.registered_by_user = self.user
        self.other_device.when_registered_utc = self.era_time_utc
        self.other_device.camcops_version = CAMCOPS_SERVER_VERSION
        self.dbsession.add(self.other_device)

        # ... export recipient definition (the minimum)
        self.recipdef.primary_idnum = idnum_type_nhs

        self.dbsession.flush()  # sets PK fields

        self.create_tasks()