예제 #1
0
 def __init__(self):
     # __init__() cannot have arguments, as it has replaced
     # DefaultUserImportFactory.make_reader() and is instantiated from
     # UserImport.__init__() without arguments.
     # So we'll fetch the necessary information from the configuration.
     self.config = Configuration()
     filename = self.config["input"]["filename"]
     header_lines = self.config["csv"]["header_lines"]
     super(HttpApiCsvReader, self).__init__(filename, header_lines)
     self.config = Configuration()
예제 #2
0
    def __init__(self, name=None, school=None, **kwargs):
        self.action = None  # "A", "D" or "M"
        self.entry_count = 0  # line/node number of input data
        self.udm_properties = dict(
        )  # UDM properties from input, that are not stored in Attributes
        self.input_data = list()  # raw input data created by SomeReader.read()
        self.old_user = None  # user in LDAP, when modifying
        self.in_hook = False  # if a hook is currently running

        for attr in self._additional_props:
            try:
                val = kwargs.pop(attr)
                setattr(self, attr, val)
            except KeyError:
                pass

        if not self.factory:
            self.__class__.factory = Factory()
            self.__class__.ucr = self.factory.make_ucr()
            self.__class__.config = Configuration()
            self.__class__.reader = self.factory.make_reader()
            self.__class__.logger = get_logger()
            self.__class__.default_username_max_length = self._default_username_max_length
            self.__class__.attribute_udm_names = dict(
                (attr.udm_name, name)
                for name, attr in self._attributes.items() if attr.udm_name)
            self.__class__.no_overwrite_attributes = self.ucr.get(
                "ucsschool/import/generate/user/attributes/no-overwrite-by-schema",
                "mailPrimaryAddress uid").split()
        self._lo = None
        self._userexpiry = None
        self._purge_ts = None
        super(ImportUser, self).__init__(name, school, **kwargs)
예제 #3
0
    def __init__(self, dry_run=True):
        """
		:param dry_run: bool: set to False to actually commit changes to LDAP
		"""
        self.dry_run = dry_run
        self.errors = list()
        self.imported_users = list()
        self.added_users = defaultdict(
            list
        )  # dict of lists of dicts: {ImportStudent: [ImportStudent.to_dict(), ..], ..}
        self.modified_users = defaultdict(list)  # like added_users
        self.deleted_users = defaultdict(list)  # like added_users
        self.config = Configuration()
        self.logger = get_logger()
        self.connection, self.position = get_admin_connection()
        self.factory = Factory()
        self.reader = self.factory.make_reader()
예제 #4
0
    def __init__(self, dry_run=True):
        """
		:param dry_run: bool: set to False to actually commit changes to LDAP
		"""
        self.dry_run = dry_run
        self.config = Configuration()
        self.logger = get_logger()
        self.factory = Factory()
        self.result_exporter = self.factory.make_result_exporter()
        self.password_exporter = self.factory.make_password_exporter()
        self.errors = list()
예제 #5
0
	def __init__(self, filename, header_lines=0, **kwargs):
		"""
		:param filename: str: Path to file with user data.
		:param header_lines: int: Number of lines before the actual data starts.
		:param kwargs: dict: optional parameters for use in derived classes
		"""
		self.config = Configuration()
		self.logger = get_logger()
		self.filename = filename
		self.header_lines = header_lines
		self.import_users = self.read()
		self.factory = Factory()
		self.ucr = self.factory.make_ucr()
		self.entry_count = 0    # line/node in input data
		self.input_data = None  # input data, as raw as possible/sensible
예제 #6
0
 def __init__(self):
     self.config = Configuration()
     filename = self.config["input"]["filename"]
     header_lines = self.config["csv"]["header_lines"]
     super(TypeCsvReader, self).__init__(filename, header_lines)
예제 #7
0
 def __init__(self, *arg, **kwargs):
     super(LegacyNewUserPasswordCsvExporter, self).__init__(*arg, **kwargs)
     self.config = Configuration()
 def __init__(self):
     self.config = Configuration()
     self.logger = get_logger()
     self.load_methods_from_config()
예제 #9
0
class UserImport(object):
    def __init__(self, dry_run=True):
        """
		:param dry_run: bool: set to False to actually commit changes to LDAP
		"""
        self.dry_run = dry_run
        self.errors = list()
        self.imported_users = list()
        self.added_users = defaultdict(
            list
        )  # dict of lists of dicts: {ImportStudent: [ImportStudent.to_dict(), ..], ..}
        self.modified_users = defaultdict(list)  # like added_users
        self.deleted_users = defaultdict(list)  # like added_users
        self.config = Configuration()
        self.logger = get_logger()
        self.connection, self.position = get_admin_connection()
        self.factory = Factory()
        self.reader = self.factory.make_reader()

    def read_input(self):
        """
		Read users from input data.
		* UcsSchoolImportErrors are stored in in self.errors (with input entry
		number in error.entry_count).

		:return: list: ImportUsers found in input
		"""
        num = 1
        self.logger.info(
            "------ Starting to read users from input data... ------")
        while True:
            try:
                import_user = self.reader.next()
                self.logger.info("Done reading %d. user: %s", num, import_user)
                self.imported_users.append(import_user)
            except StopIteration:
                break
            except UcsSchoolImportError as exc:
                self.logger.exception("Error reading %d. user: %s", num, exc)
                self._add_error(exc)
            num += 1
        self.logger.info("------ Read %d users from input data. ------",
                         len(self.imported_users))
        return self.imported_users

    def create_and_modify_users(self, imported_users=None):
        """
		Create and modify users.
		* self.added_users and self.modified_users will hold objects of created/
		modified ImportUsers.
		* UcsSchoolImportErrors are stored in self.errors (with failed ImportUser
		object in error.import_user).

		:param imported_users: list: ImportUser objects
		:return tuple: (self.errors, self.added_users, self.modified_users)
		"""
        self.logger.info("------ Creating / modifying users... ------")
        usernum = 0
        total = len(imported_users)
        while imported_users:
            imported_user = imported_users.pop(0)
            usernum += 1
            percentage = 10 + 90 * usernum / total  # 10% - 100%
            self.progress_report(
                description='Creating and modifying users: {}%.'.format(
                    percentage),
                percentage=percentage,
                done=usernum,
                total=total,
                errors=len(self.errors))
            if imported_user.action == "D":
                continue
            try:
                self.logger.debug("Creating / modifying user %d/%d %s...",
                                  usernum, total, imported_user)
                user = self.determine_add_modify_action(imported_user)
                cls_name = user.__class__.__name__

                try:
                    action_str = {
                        "A": "Adding",
                        "D": "Deleting",
                        "M": "Modifying"
                    }[user.action]
                except KeyError:
                    raise UnkownAction(
                        "{}  (source_uid:{} record_uid: {}) has unknown action '{}'."
                        .format(user, user.source_uid, user.record_uid,
                                user.action),
                        entry_count=user.entry_count,
                        import_user=user)

                if user.action in ["A", "M"]:
                    self.logger.info(
                        "%s %s (source_uid:%s record_uid:%s) attributes=%r udm_properties=%r...",
                        action_str, user, user.source_uid, user.record_uid,
                        user.to_dict(), user.udm_properties)
                password = user.password  # save password of new user for later export (NewUserPasswordCsvExporter)
                try:
                    if user.action == "A":
                        err = CreationError
                        store = self.added_users[cls_name]
                        if self.dry_run:
                            self.logger.info("Dry run: would create %s now.",
                                             user)
                            success = True
                        else:
                            success = user.create(lo=self.connection)
                    elif user.action == "M":
                        err = ModificationError
                        store = self.modified_users[cls_name]
                        if self.dry_run:
                            self.logger.info("Dry run: would modify %s now.",
                                             user)
                            success = True
                        else:
                            success = user.modify(lo=self.connection)
                    else:
                        # delete
                        continue
                except ValidationError as exc:
                    raise UserValidationError, UserValidationError(
                        "ValidationError when {} {} "
                        "(source_uid:{} record_uid: {}): {}".format(
                            action_str.lower(), user, user.source_uid,
                            user.record_uid, exc),
                        validation_error=exc,
                        import_user=user), sys.exc_info()[2]

                if success:
                    self.logger.info(
                        "Success %s %d/%d %s (source_uid:%s record_uid: %s).",
                        action_str.lower(), usernum, total, user,
                        user.source_uid, user.record_uid)
                    user.password = password
                    store.append(user.to_dict())
                else:
                    raise err(
                        "Error {} {}/{} {} (source_uid:{} record_uid: {}), does probably {}exist."
                        .format(action_str.lower(), usernum,
                                len(imported_users), user, user.source_uid,
                                user.record_uid,
                                "not " if user.action == "M" else "already "),
                        entry_count=user.entry_count,
                        import_user=user)

            except (CreationError, ModificationError) as exc:
                self.logger.error("Entry #%d: %s", exc.entry_count,
                                  exc)  # traceback useless
                self._add_error(exc)
            except UcsSchoolImportError as exc:
                self.logger.exception("Entry #%d: %s", exc.entry_count, exc)
                self._add_error(exc)
        num_added_users = sum(map(len, self.added_users.values()))
        num_modified_users = sum(map(len, self.modified_users.values()))
        self.logger.info("------ Created %d users, modified %d users. ------",
                         num_added_users, num_modified_users)
        return self.errors, self.added_users, self.modified_users

    def determine_add_modify_action(self, imported_user):
        """
		Determine what to do with the ImportUser. Should set attribute "action"
		to either "A" or "M". If set to "M" the returned user must be a opened
		ImportUser from LDAP.
		Run modify preparations here, like school-move etc.

		:param imported_user: ImportUser from input
		:return: ImportUser: ImportUser with action set and possibly fetched
		from LDAP
		"""
        try:
            user = imported_user.get_by_import_id(self.connection,
                                                  imported_user.source_uid,
                                                  imported_user.record_uid)
            imported_user.old_user = user
            imported_user.prepare_all(new_user=False)
            if user.school != imported_user.school:
                user = self.school_move(imported_user, user)
            user.update(imported_user)
            if user.disabled != "none" or user.has_expiry(
                    self.connection) or user.has_purge_timestamp(
                        self.connection):
                self.logger.info(
                    "Found user %r that was previously deactivated or is scheduled for deletion (purge timestamp is "
                    "non-empty), reactivating user.", user)
                user.reactivate()
            user.action = "M"
        except noObject:
            imported_user.prepare_all(new_user=True)
            user = imported_user
            user.action = "A"
        return user

    def detect_users_to_delete(self):
        """
		Find difference between source database and UCS user database.

		:return list of tuples: [(source_uid, record_uid), ..]
		"""
        self.logger.info("------ Detecting which users to delete... ------")
        users_to_delete = list()

        if self.config["no_delete"]:
            self.logger.info(
                "------ Looking only for users with action='D' (no_delete=%r) ------",
                self.config["no_delete"])
            for user in self.imported_users:
                if user.action == "D":
                    try:
                        users_to_delete.append(
                            (user.source_uid, user.record_uid))
                    except noObject:
                        msg = "User to delete not found in LDAP: {}.".format(
                            user)
                        self.logger.error(msg)
                        self._add_error(
                            DeletionError(msg,
                                          entry_count=user.entry_count,
                                          import_user=user))
            return users_to_delete

        source_uid = self.config["sourceUID"]
        attr = ["ucsschoolSourceUID", "ucsschoolRecordUID"]
        oc_filter = self.factory.make_import_user(
            []).get_ldap_filter_for_user_role()
        filter_s = filter_format(
            "(&{}(ucsschoolSourceUID=%s)(ucsschoolRecordUID=*))".format(
                oc_filter), (source_uid, ))
        self.logger.debug('Searching with filter=%r', filter_s)

        id2imported_user = dict()  # for fast access later
        for iu in self.imported_users:
            id2imported_user[(iu.source_uid, iu.record_uid)] = iu
        imported_user_ids = set(id2imported_user.keys())

        # Find all users that exist in UCS but not in input.
        ucs_ldap_users = self.connection.search(filter_s, attr=attr)
        ucs_user_ids = set([(lu[1]["ucsschoolSourceUID"][0].decode('utf-8'),
                             lu[1]["ucsschoolRecordUID"][0].decode('utf-8'))
                            for lu in ucs_ldap_users])

        users_to_delete = list(ucs_user_ids - imported_user_ids)
        self.logger.debug("users_to_delete=%r", users_to_delete)
        return users_to_delete

    def delete_users(self, users=None):
        """
		Delete users.
		* detect_users_to_delete() should have run before this.
		* self.deleted_users will hold objects of deleted ImportUsers.
		* UcsSchoolImportErrors are stored in self.errors (with failed ImportUser
		object in error.import_user).
		* To add or change a deletion strategy overwrite do_delete().

		:param users: list of tuples: [(source_uid, record_uid), ..]
		:return: tuple: (self.errors, self.deleted_users)
		"""
        self.logger.info("------ Deleting %d users... ------", len(users))
        a_user = self.factory.make_import_user([])
        for num, ucs_id_not_in_import in enumerate(users, start=1):
            percentage = 10 * num / len(users)  # 0% - 10%
            self.progress_report(
                description='Deleting users: {}.'.format(percentage),
                percentage=percentage,
                done=num,
                total=len(users),
                errors=len(self.errors))
            try:
                user = a_user.get_by_import_id(self.connection,
                                               ucs_id_not_in_import[0],
                                               ucs_id_not_in_import[1])
                user.action = "D"  # mark for logging/csv-output purposes
            except noObject as exc:
                self.logger.error(
                    "Cannot delete non existing user with source_uid=%r, record_uid=%r: %s",
                    ucs_id_not_in_import[0], ucs_id_not_in_import[1], exc)
                continue
            try:
                success = self.do_delete(user)
                if success:
                    self.logger.info(
                        "Success deleting %d/%d %r (source_uid:%s record_uid: %s).",
                        num, len(users), user.name, user.source_uid,
                        user.record_uid)
                else:
                    raise DeletionError(
                        "Error deleting user '{}' (source_uid:{} record_uid: {}), has probably already been deleted."
                        .format(user.name, user.source_uid, user.record_uid),
                        entry_count=user.entry_count,
                        import_user=user)
                self.deleted_users[user.__class__.__name__].append(
                    user.to_dict())
            except UcsSchoolImportError as exc:
                self.logger.exception("Error in entry #%d: %s",
                                      exc.entry_count, exc)
                self._add_error(exc)
        self.logger.info("------ Deleted %d users. ------",
                         sum(map(len, self.deleted_users.values())))
        return self.errors, self.deleted_users

    def school_move(self, imported_user, user):
        """
		Change users primary school.

		:param imported_user: User from input with target school
		:param user: existing User with old school
		:return: ImportUser: user in new position, freshly fetched from LDAP
		"""
        self.logger.info("Moving %s from school %r to %r...", user,
                         user.school, imported_user.school)
        user = self.do_school_move(imported_user, user)
        return user

    def do_school_move(self, imported_user, user):
        """
		Change users primary school - school_move() without calling Python
		hooks (ucsschool lib calls executables anyway).
		"""
        if self.dry_run:
            self.logger.info("Dry run - not doing the school move.")
            res = True
        else:
            res = user.change_school(imported_user.school, self.connection)
        if not res:
            raise MoveError("Error moving {} from school '{}' to '{}'.".format(
                user, user.school, imported_user.school),
                            entry_count=imported_user.entry_count,
                            import_user=imported_user)
        # refetch user from LDAP
        user = imported_user.get_by_import_id(self.connection,
                                              imported_user.source_uid,
                                              imported_user.record_uid)
        return user

    def do_delete(self, user):
        """
		Delete or deactivate a user.
		IMPLEMENTME to add or change a deletion variant.

		:param user: ImportUser
		:return bool: whether the deletion worked
		"""
        deactivation_grace = max(
            0,
            int(
                self.config.get('deletion_grace_period',
                                {}).get('deactivation', 0)))
        deletion_grace = max(
            0,
            int(
                self.config.get('deletion_grace_period',
                                {}).get('deletion', 0)))
        modified = False
        success = None

        if deletion_grace <= deactivation_grace:
            # just delete, ignore deactivation setting
            if deletion_grace == 0:
                # delete user right now
                success = self.delete_user_now(user)
            else:
                # delete user later
                modified |= self.set_deletion_grace(user, deletion_grace)
        else:
            # deactivate first, delete later
            if deactivation_grace == 0:
                # deactivate user right now
                modified |= self.deactivate_user_now(user)
            else:
                # deactivate user later
                modified |= self.set_deactivation_grace(
                    user, deactivation_grace)

            # delete user later
            modified |= self.set_deletion_grace(user, deletion_grace)

        if success is not None:
            # immediate deletion
            pass
        elif self.dry_run:
            self.logger.info(
                'Dry run - not expiring, deactivating or setting the purge timestamp.'
            )
            success = True
        elif modified:
            success = user.modify(lo=self.connection)
        else:
            # not a dry_run, but user was not modified, because
            # disabled / expiration date / purge timestamp were already set
            success = True

        user.invalidate_all_caches()
        return success

    def deactivate_user_now(self, user):
        """
		Deactivate the user. Does not run user.modify().

		:param user: ImportUser object
		:return: bool: whether any changes were made to the object and
		user.modify() is required
		"""
        if user.disabled == 'all':
            self.logger.info('User %s is already disabled.', user)
            return False
        else:
            self.logger.info('Deactivating user %s...', user)
            user.deactivate()
            return True

    def delete_user_now(self, user):
        """
		Truly delete the user.

		:param user: ImportUser object
		:return: bool: return value from the ucsschool.lib.model remove() call
		"""
        self.logger.info('Deleting user %s...', user)
        if self.dry_run:
            self.logger.info('Dry run - not removing the user.')
            return True
        else:
            return user.remove(self.connection)

    def set_deactivation_grace(self, user, grace):
        """
		Sets the account expiration date (UDM attribute "userexpiry") on the
		user object. Does not run user.modify().

		:param user: ImportUser object
		:return: bool: whether any changes were made to the object and
		user.modify() is required
		"""
        if user.disabled == 'all':
            self.logger.info(
                'User %s is already disabled. No account expiration date is set.',
                user)
            return False
        elif user.has_expiry(self.connection):
            self.logger.info(
                'An account expiration date is already set for user %s. The entry remains unchanged.',
                user)
            return False
        else:
            expiry = datetime.datetime.now() + datetime.timedelta(days=grace)
            expiry_str = expiry.strftime('%Y-%m-%d')
            self.logger.info('Setting account expiration date of %s to %s...',
                             user, expiry_str)
            user.expire(expiry_str)
            return True

    def set_deletion_grace(self, user, grace):
        """
		Sets the account deletion timestamp (UDM attribute "ucsschoolPurgeTimestamp")
		on the user object. Does not run user.modify().

		:param user: ImportUser object
		:return: bool: whether any changes were made to the object and
		user.modify() is required
		"""
        if user.has_purge_timestamp(self.connection):
            self.logger.info(
                'User %s is already scheduled for deletion. The entry remains unchanged.',
                user)
            return False
        else:
            purge_ts = datetime.datetime.now() + datetime.timedelta(days=grace)
            purge_ts_str = purge_ts.strftime('%Y-%m-%d')
            self.logger.info('Setting deletion grace date of %s to %r...',
                             user, purge_ts_str)
            user.set_purge_timestamp(purge_ts_str)
            return True

    def log_stats(self):
        """
		Log statistics about read, created, modified and deleted users.
		"""
        self.logger.info("------ User import statistics ------")
        self.logger.info("Read users from input data: %d",
                         len(self.imported_users))
        cls_names = self.added_users.keys()
        cls_names.extend(self.modified_users.keys())
        cls_names.extend(self.deleted_users.keys())
        cls_names = set(cls_names)
        columns = 4
        for cls_name in sorted(cls_names):
            self.logger.info("Created %s: %d", cls_name,
                             len(self.added_users.get(cls_name, [])))
            for i in range(0, len(self.added_users[cls_name]), columns):
                self.logger.info("  %s", [
                    iu["name"]
                    for iu in self.added_users[cls_name][i:i + columns]
                ])
            self.logger.info("Modified %s: %d", cls_name,
                             len(self.modified_users.get(cls_name, [])))
            for i in range(0, len(self.modified_users[cls_name]), columns):
                self.logger.info("  %s", [
                    iu["name"]
                    for iu in self.modified_users[cls_name][i:i + columns]
                ])
            self.logger.info("Deleted %s: %d", cls_name,
                             len(self.deleted_users.get(cls_name, [])))
            for i in range(0, len(self.deleted_users[cls_name]), columns):
                self.logger.info("  %s", [
                    iu["name"]
                    for iu in self.deleted_users[cls_name][i:i + columns]
                ])
        self.logger.info("Errors: %d", len(self.errors))
        if self.errors:
            self.logger.info("Entry #: Error description")
        for error in self.errors:
            self.logger.info(
                "  %d: %s: %s", error.entry_count,
                error.import_user.name if error.import_user else "NoName",
                error)
        self.logger.info("------ End of user import statistics ------")

    def _add_error(self, err):
        self.errors.append(err)
        if -1 < self.config["tolerate_errors"] < len(self.errors):
            raise ToManyErrors(
                "More than {} errors.".format(self.config["tolerate_errors"]),
                self.errors)

    def progress_report(self,
                        description,
                        percentage=0,
                        done=0,
                        total=0,
                        **kwargs):
        if 'progress_notification_function' not in self.config:
            return
        self.config['progress_notification_function'](description, percentage,
                                                      done, total, **kwargs)