Ejemplo n.º 1
0
class NotificationPlugin(Deamon, IUserChangeListener):
    """
    Send email notification when a repository get too old (without a backup).
    """

    _encryption = Option('EmailEncryption', 'none')

    _email_host = Option('EmailHost', _get_func=lambda x: x.partition(':')[0])

    _email_port = Option('EmailHost',
                         _get_func=lambda x: int(x.partition(':')[2]))

    _email_from = Option('EmailSender')

    _email_notification_time = Option('EmailNotificationTime', '23:00')

    _smtp_username = Option('EmailUsername', None)

    _smtp_password = Option('EmailPassword', None)

    _header_name = Option("HeaderName", "rdiffweb")

    _send_change_notification = BoolOption("EmailSendChangedNotification",
                                           False)

    def __init__(self, bus, app):
        self.app = app
        self.app.store.add_change_listener(self)
        Deamon.__init__(self, bus)

    @property
    def job_execution_time(self):
        return self._email_notification_time

    def job_run(self):
        """
        Implementation of JobPLugin interface.
        Go trough all the repositories and users to send mail in batches.
        """
        self.send_notifications()

    def user_attr_changed(self, userobj, attrs={}):
        """
        Implementation of IUserChangeListener interface.
        """
        if not self._send_change_notification:
            return

        # Leave if the mail was not changed.
        if 'email' not in attrs:
            return

        if not userobj.email:
            logging.info("can't sent mail to user [%s] without an email",
                         userobj.username)
            return

        # If the email attributes was changed, send a mail notification.
        self.send_mail(userobj, _("Email address changed"),
                       "email_changed.html")

    def user_password_changed(self, username, password):
        """
        Implementation of IUserChangeListener interface.
        """

        # get User object (to get email)
        userobj = self.app.store.get_user(username)
        assert userobj

        if not userobj.email:
            logging.info("can't sent mail to user [%s] without an email",
                         userobj.username)
            return

        # If the email attributes was changed, send a mail notification.
        self.send_mail(userobj, _("Password changed"), "password_changed.html")

    def send_notifications(self):
        """
        Loop trough all the user repository and send notifications.
        """

        now = librdiff.RdiffTime()

        def _user_repos():
            """Return a generator trought user repos to be notified."""
            for user in self.app.store.users():
                # Check if user has email.
                if not user.email:
                    continue
                # Identify old repo for current user.
                old_repos = []
                for repo in user.repo_objs:
                    # Check if repo has age configured (in days)
                    maxage = repo.maxage
                    if not maxage or maxage <= 0:
                        continue
                    # Check repo age.
                    if repo.last_backup_date < (
                            now - datetime.timedelta(days=maxage)):
                        old_repos.append(repo)
                # Return an item only if user had old repo
                if old_repos:
                    yield user, old_repos

        # For each candidate, send mail.
        for user, repos in _user_repos():
            parms = {'user': user, 'repos': repos}
            self.send_mail(user, _('Notification'), 'email_notification.html',
                           **parms)

    def send_mail(self, to_user, subject, template_name, **kwargs):
        """
        Reusable method to be called to send email to the user user.
        `user` user object where to send the email.
        ``
        """
        # Verify if the users as an email.
        assert to_user
        assert to_user.email

        # Build email from template.
        parms = {'user': to_user}

        if self._header_name:
            parms["header_name"] = self._header_name
        parms.update(kwargs)

        # Compile both template.
        html = self.app.templates.compile_template(template_name, **parms)
        text = html2plaintext(html)

        # Record the MIME types of both parts - text/plain and text/html.
        part1 = MIMEText(text, 'plain', 'utf8')
        part2 = MIMEText(html, 'html', 'utf8')

        email_from = self._header_name
        if self._email_from:
            email_from += " <%s>" % self._email_from

        msg = MIMEMultipart('alternative')
        msg['Subject'] = subject
        msg['From'] = email_from
        msg['To'] = to_user.email
        msg.attach(part1)
        msg.attach(part2)

        # Open an SMTP connection.
        conn = None
        if self._encryption == 'ssl':
            conn = smtplib.SMTP_SSL(self._email_host, self._email_port)
        else:
            conn = smtplib.SMTP(self._email_host, self._email_port)
        try:
            if self._encryption == 'starttls':
                conn.starttls()

            # Authenticate if required.
            if self._smtp_username:
                conn.login(self._smtp_username, self._smtp_password)
            conn.sendmail(self._email_from, to_user.email, msg.as_string())
        finally:
            if conn is not None:
                conn.quit()
Ejemplo n.º 2
0
class Store():
    """
    This class handle all data storage operations.
    """

    _db_file = Option("SQLiteDBFile", "/etc/rdiffweb/rdw.db")
    _allow_add_user = BoolOption("AddMissingUser", False)
    _admin_user = Option("AdminUser", "admin")

    def __init__(self, app):
        self.app = app
        self._database = SQLiteBackend(self._db_file)
        self._password_stores = [LdapPasswordStore(app)]
        self._change_listeners = []

        # Register entry point.
        for entry_point in pkg_resources.iter_entry_points(
                'rdiffweb.IUserChangeListener'):  # @UndefinedVariable
            try:
                cls = entry_point.load()
                # Creating the listener should register it self.But let make sure of it.
                listener = cls(self.app)
                if listener not in self._change_listeners:
                    self._change_listeners.append(listener)
            except:
                logging.error("IUserChangeListener [%s] fail to load",
                              entry_point)

    def create_admin_user(self):
        # Check if admin user exists. If not, created it.
        if not self.get_user(self._admin_user):
            userobj = self.add_user(self._admin_user, 'admin123')
            userobj.is_admin = True

    def add_change_listener(self, listener):
        self._change_listeners.append(listener)

    def remove_change_listener(self, listener):
        self._change_listeners.remove(listener)

    def add_user(self, user, password=None, attrs=None):
        """
        Used to add a new user with an optional password.
        """
        assert password is None or isinstance(password, str)
        # Check if user already exists.
        if self.get_user(user):
            raise RdiffError(_("User %s already exists." % (user, )))

        # Find a database where to add the user
        logger.debug("adding new user [%s]", user)
        if password:
            inserted = self._database.insert('users',
                                             username=user,
                                             password=_hash_password(password))
        else:
            inserted = self._database.insert('users',
                                             username=user,
                                             password='')
        assert inserted
        record = self._database.findone('users', username=user)
        userobj = UserObject(self, record)
        self._notify('user_added', userobj, attrs)
        # Return user object
        return userobj

    def count_users(self):
        return self._database.count('users')

    def count_repos(self):
        return self._database.count('repos')

    def get_user(self, user):
        """Return a user object."""
        record = self._database.findone('users', username=user)
        if record:
            return UserObject(self, record)
        return None

    def users(self, search=None, criteria=None):
        """
        Search users database. Return a generator of user object.
        
        search: Define a search term to look into email or username.
        criteria: Define a search filter: admins, ldap
        """
        if search:
            users = self._database.search('users', search, 'username',
                                          'useremail')
        elif criteria:
            if criteria == 'admins':
                users = self._database.find('users', isadmin=1)
            elif criteria == 'ldap':
                users = self._database.find('users', password='')
            else:
                return
        else:
            users = self._database.find('users')
        for record in users:
            yield UserObject(self, record)

    def repos(self, search=None, criteria=None):
        """
        Quick listing of all the repository object for all user.
        
        search: Define a search term to look into path, email or username.
        criteria: Define a search filter: ok, failed, interrupted, in_progress
        """
        if search:
            repos = self._database.search('repos', search, 'RepoPath',
                                          'username', 'useremail')
        else:
            repos = self._database.find('repos')

        for record in repos:
            user_record = self._database.findone('users',
                                                 userid=record['userid'])
            user_obj = UserObject(self, user_record)
            repo_obj = RepoObject(user_obj, record)
            if not criteria or criteria == repo_obj.status[0]:
                yield repo_obj

    def login(self, user, password):
        """
        Called to authenticate the given user.

        Check if the credentials are valid. Then may actually add the user
        in database if allowed.

        If valid, return the username. Return False if the user exists but the
        password doesn't matches. Return None if the user was not found in any
        password store.
        The return user object. The username may not be equals to the given username.
        """
        assert isinstance(user, str)
        assert password is None or isinstance(user, str)
        # Validate credential using database first.
        logger.debug("validating user [%s] credentials", user)
        record = self._database.findone('users', username=user)
        if record and record['password'] == _hash_password(password):
            userobj = UserObject(self, record)
            self._notify('user_logined', userobj, None)
            return userobj

        # Fallback to LDAP
        for store in self._password_stores:
            try:
                valid = store.are_valid_credentials(user, password)
                if valid:
                    break
            except:
                pass
        if not valid:
            return None
        # Get real username and external attributes.
        real_user, attrs = valid
        # Check if user exists in database
        userobj = self.get_user(real_user)
        if not userobj:
            if self._allow_add_user:
                # Create user
                userobj = self.add_user(real_user, attrs=attrs)
            else:
                logger.info("user [%s] not found in database", real_user)
                return None
        self._notify('user_logined', userobj, attrs)
        return userobj

    def _notify(self, mod, *args):
        for listener in self._change_listeners:
            # Support divergent account change listener implementations too.
            try:
                logger.debug('notify %s#%s()', listener.__class__.__name__,
                             mod)
                getattr(listener, mod)(*args)
            except:
                logger.warning('IUserChangeListener [%s] fail to run [%s]',
                               listener.__class__.__name__,
                               mod,
                               exc_info=1)
Ejemplo n.º 3
0
class Store():
    """
    This class handle all data storage operations.
    """

    _db_file = Option("SQLiteDBFile", "/etc/rdiffweb/rdw.db")
    _allow_add_user = BoolOption("AddMissingUser", False)
    _admin_user = Option("AdminUser", "admin")

    def __init__(self, app):
        self.app = app
        from rdiffweb.core.store_sqlite import SQLiteBackend
        self._database = SQLiteBackend(self._db_file)
        self._password_stores = [LdapPasswordStore(app)]
        self._change_listeners = []

        # Register entry point.
        for entry_point in pkg_resources.iter_entry_points(
                'rdiffweb.IUserChangeListener'):  # @UndefinedVariable
            try:
                cls = entry_point.load()
                # Creating the listener should register it self.But let make sure of it.
                listener = cls(self.app)
                if listener not in self._change_listeners:
                    self._change_listeners.append(listener)
            except:
                logging.error("IUserChangeListener [%s] fail to load",
                              entry_point)

    def create_admin_user(self):
        # Check if admin user exists. If not, created it.
        if not self.get_user(self._admin_user):
            userobj = self.add_user(self._admin_user, 'admin123')
            userobj.role = ADMIN_ROLE

    def add_change_listener(self, listener):
        self._change_listeners.append(listener)

    def remove_change_listener(self, listener):
        self._change_listeners.remove(listener)

    def add_user(self, user, password=None, attrs=None):
        """
        Used to add a new user with an optional password.
        """
        assert password is None or isinstance(password, str)
        # Check if user already exists.
        if self.get_user(user):
            raise RdiffError(_("User %s already exists." % (user, )))

        # Find a database where to add the user
        logger.debug("adding new user [%s]", user)
        if password:
            inserted = self._database.insert('users',
                                             username=user,
                                             password=hash_password(password))
        else:
            inserted = self._database.insert('users',
                                             username=user,
                                             password='')
        assert inserted
        record = self._database.findone('users', username=user)
        userobj = UserObject(self, record)
        self._notify('user_added', userobj, attrs)
        # Return user object
        return userobj

    def count_users(self):
        return self._database.count('users')

    def count_repos(self):
        return self._database.count('repos')

    def get_repo(self, name, as_user=None):
        """
        Return the repository identified as `name`.
        `name` should be <username>/<repopath>
        """
        username, repopath = _split_path(name)
        repopath = decodefilename(repopath)

        # Check permissions
        as_user = as_user or self.app.currentuser
        assert as_user, "as_user or current user must be defined"
        if username != as_user.username and not as_user.is_admin:
            raise AccessDeniedError(name)

        # Get the userid associated to the username.
        user_obj = self.get_user(username)
        if not user_obj:
            raise DoesNotExistError(name)

        # Get the repo object.
        return user_obj.get_repo(repopath)

    def get_repo_path(self, path, as_user=None):
        """
        Return a the repository identified by the given `path`.
        `path` should be <username>/<repopath>/<subdir>
        """
        assert isinstance(path, bytes) or isinstance(path, str)
        sep = b'/' if isinstance(path, bytes) else '/'
        path = path.strip(sep) + sep
        # Since we don't know which part of the "path" is the repopath,
        # we need to do multiple search.
        try:
            startpos = 0
            while True:
                pos = path.index(sep, startpos)
                try:
                    repo_obj = self.get_repo(path[:pos], as_user)
                    path_obj = repo_obj.get_path(path[pos + 1:])
                    return repo_obj, path_obj
                except DoesNotExistError:
                    # continue looping
                    startpos = pos + 1
        except ValueError:
            raise DoesNotExistError(path)

    def get_user(self, user):
        """Return a user object."""
        record = self._database.findone('users', username=user)
        if record:
            return UserObject(self, record)
        return None

    def users(self, search=None, criteria=None):
        """
        Search users database. Return a generator of user object.
        
        search: Define a search term to look into email or username.
        criteria: Define a search filter: admins, ldap
        """
        if search:
            users = self._database.search('users', search, 'username',
                                          'useremail')
        elif criteria:
            if criteria == 'admins':
                users = self._database.find('users', role=ADMIN_ROLE)
            elif criteria == 'ldap':
                users = self._database.find('users', password='')
            else:
                return
        else:
            users = self._database.find('users')
        for record in users:
            yield UserObject(self, record)

    def repos(self, search=None, criteria=None):
        """
        Quick listing of all the repository object for all user.
        
        search: Define a search term to look into path, email or username.
        criteria: Define a search filter: ok, failed, interrupted, in_progress
        """
        if search:
            repos = self._database.search('repos', search, 'RepoPath',
                                          'username', 'useremail')
        else:
            repos = self._database.find('repos')

        for record in repos:
            user_record = self._database.findone('users',
                                                 userid=record['userid'])
            user_obj = UserObject(self, user_record)
            repo_obj = RepoObject(user_obj, record)
            if not criteria or criteria == repo_obj.status[0]:
                yield repo_obj

    def login(self, user, password):
        """
        Called to authenticate the given user.

        Check if the credentials are valid. Then may actually add the user
        in database if allowed.

        If valid, return the username. Return False if the user exists but the
        password doesn't matches. Return None if the user was not found in any
        password store.
        The return user object. The username may not be equals to the given username.
        """
        assert isinstance(user, str)
        assert password is None or isinstance(user, str)
        # Validate credential using database first.
        logger.debug("validating user [%s] credentials", user)
        userobj = self.get_user(user)
        if userobj and userobj.hash_password:
            if not check_password(password, userobj.hash_password):
                return None
            self._notify('user_logined', userobj, None)
            return userobj

        # Fallback to LDAP
        if userobj or self._allow_add_user:
            for store in self._password_stores:
                try:
                    valid = store.are_valid_credentials(user, password)
                    if valid:
                        real_user, attrs = valid
                        if not userobj:
                            userobj = self.add_user(real_user, attrs=attrs)
                        self._notify('user_logined', userobj, attrs)
                        return userobj
                except:
                    logger.warn('fail to validate credentials', exc_info=1)
        return None

    def _notify(self, mod, *args):
        for listener in self._change_listeners:
            # Support divergent account change listener implementations too.
            try:
                logger.debug('notify %s#%s()', listener.__class__.__name__,
                             mod)
                getattr(listener, mod)(*args)
            except:
                logger.warning('IUserChangeListener [%s] fail to run [%s]',
                               listener.__class__.__name__,
                               mod,
                               exc_info=1)
Ejemplo n.º 4
0
class UserManager():
    """
    This class handle all user operation. This class is greatly inspired from
    TRAC account manager class.
    """

    _db_file = Option("SQLiteDBFile", "/etc/rdiffweb/rdw.db")
    _allow_add_user = BoolOption("AddMissingUser", False)
    _admin_user = Option("AdminUser", "admin")

    def __init__(self, app):
        self.app = app
        self._database = SQLiteUserDB(self._db_file)
        self._password_stores = [LdapPasswordStore(app)]
        self._change_listeners = []

        # Register entry point.
        for entry_point in pkg_resources.iter_entry_points(
                'rdiffweb.IUserChangeListener'):  # @UndefinedVariable
            try:
                cls = entry_point.load()
                # Creating the listener should register it self.But let make sure of it.
                listener = cls(self.app)
                if listener not in self._change_listeners:
                    self._change_listeners.append(listener)
            except:
                logging.error("IUserChangeListener [%s] fail to load",
                              entry_point)

        # Check if admin user exists. If not, created it.
        if not self.exists(self._admin_user):
            userobj = self.add_user(self._admin_user, 'admin123')
            userobj.is_admin = True

    def add_change_listener(self, listener):
        self._change_listeners.append(listener)

    def remove_change_listener(self, listener):
        self._change_listeners.remove(listener)

    def add_user(self, user, password=None, attrs=None):
        """
        Used to add a new user with an optional password.
        """
        assert password is None or isinstance(password, str)
        # Check if user already exists.
        if self._database.exists(user):
            raise RdiffError(_("User %s already exists." % (user, )))
        # Find a database where to add the user
        logger.debug("adding new user [%s]", user)
        self._database.add_user(user, password)
        userobj = UserObject(self, self._database, user)
        self._notify('user_added', userobj, attrs)
        # Return user object
        return userobj

    def delete_user(self, user):
        """
        Delete the given user from password store.

        Return True if the user was deleted. Return False if the user didn't
        exists.
        """
        if hasattr(user, 'username'):
            user = user.username

        if user == self._admin_user:
            raise ValueError(_("can't delete admin user"))

        # Delete user from database (required).
        logger.info("deleting user [%s] from database", user)
        self._database.delete_user(user)
        self._notify('user_deleted', user)
        return True

    def exists(self, user):
        """
        Verify if the given user exists in our database.

        Return True if the user exists. False otherwise.
        """
        return self._database.exists(user)

    def get_user(self, user):
        """Return a user object."""
        if not self.exists(user):
            return None
        return UserObject(self, self._database, user)

    def users(self, search=None, criteria=None):
        """
        Search users database. Return a generator of user object.
        
        search: Define a search term to look into email or username.
        criteria: Define a search filter: admins, ldap
        """
        # TODO Add criteria as required.
        for username in self._database.users(search, criteria):
            yield UserObject(self, self._database, username)

    def repos(self, search=None, criteria=None):
        """
        Quick listing of all the repository object for all user.
        
        search: Define a search term to look into path, email or username.
        criteria: Define a search filter: ok, failed, interrupted, in_progress
        """
        for username, repo in self._database.repos(search, criteria):
            user_obj = UserObject(self, self._database, username)
            repo_obj = RepoObject(user_obj, repo)
            if not criteria or criteria == repo_obj.status[0]:
                yield repo_obj

    def login(self, user, password):
        """
        Called to authenticate the given user.

        Check if the credentials are valid. Then may actually add the user
        in database if allowed.

        If valid, return the username. Return False if the user exists but the
        password doesn't matches. Return None if the user was not found in any
        password store.
        The return user object. The username may not be equals to the given username.
        """
        assert isinstance(user, str)
        assert password is None or isinstance(user, str)
        # Validate the credentials
        logger.debug("validating user [%s] credentials", user)
        for store in [self._database] + self._password_stores:
            try:
                valid = store.are_valid_credentials(user, password)
                if valid:
                    break
            except:
                pass
        if not valid:
            return None
        # Get real username and external attributes.
        # Some password store provide extra attributes.
        if isinstance(valid, str):
            real_user, attrs = valid, None
        else:
            real_user, attrs = valid
        # Check if user exists in database
        userobj = self.get_user(real_user)
        if not userobj:
            if self._allow_add_user:
                # Create user
                userobj = self.add_user(real_user, attrs=attrs)
            else:
                logger.info("user [%s] not found in database", real_user)
                return None
        self._notify('user_logined', userobj, attrs)
        return userobj

    def set_password(self, username, password, old_password=None):
        # Try to update the user password.
        for store in self._password_stores:
            try:
                valid = store.are_valid_credentials(username, old_password)
                if valid:
                    store.set_password(username, password, old_password)
                    return
            except:
                pass
        # Fallback to database
        self._database.set_password(username, password, old_password)
        self._notify('user_password_changed', username, password)

    def _notify(self, mod, *args):
        for listener in self._change_listeners:
            # Support divergent account change listener implementations too.
            try:
                logger.debug('notify %s#%s()', listener.__class__.__name__,
                             mod)
                getattr(listener, mod)(*args)
            except:
                logger.warning('IUserChangeListener [%s] fail to run [%s]',
                               listener.__class__.__name__,
                               mod,
                               exc_info=1)
Ejemplo n.º 5
0
class MinarcaUserSetup(IUserChangeListener, IUserQuota):
    """
    This plugin provide feedback information to the users about the disk usage.
    Since we define quota, this plugin display the user's quota.
    """
    
    _quota_api_url = Option('MinarcaQuotaApiUrl', 'http://*****:*****@localhost:8081/')
    _mode = IntOption('MinarcaUserDirMode', 0o0770)
    _owner = Option('MinarcaUserDirOwner', 'minarca')
    _group = Option('MinarcaUserDirGroup', 'minarca')
    _basedir = Option('MinarcaUserBaseDir', default='/backups/')
    _minarca_shell = Option('MinarcaShell', default='/opt/minarca/bin/minarca-shell')
    _auth_options = Option('MinarcaAuthOptions', default='no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty')
    _minarca_remotehost = Option('MinarcaRemoteHost')
    _minarca_identity = Option('MinarcaRemoteHostIdentity', default='/etc/ssh')
    _restrited_basedir = BoolOption('MinarcaRestrictedToBasedDir', default=True)
    _redirect_help = Option('MinarcaHelpURL', default='https://www.ikus-soft.com/en/support/#form')

    def __init__(self, app):
        self.app = app
        self.app.root.api.minarca = self.get_minarca
        self.app.root.help = self.get_help
        self.session = requests.Session()
        self.session.mount('https://', TimeoutHTTPAdapter(pool_connections=2, pool_maxsize=5))
        self.session.mount('http://', TimeoutHTTPAdapter(pool_connections=2, pool_maxsize=5))

    @cherrypy.expose
    @cherrypy.config(**{'tools.authform.on': False, 'tools.i18n.on': False, 'tools.authbasic.on': False, 'tools.sessions.on': False, 'error_page.default': False})
    def get_help(self):
        raise cherrypy.HTTPRedirect(self._redirect_help)

    @cherrypy.expose
    @cherrypy.tools.json_out()
    def get_minarca(self):
        
        # RemoteHost
        remotehost = self._minarca_remotehost
        if not remotehost:
            remotehost = urlparse(cherrypy.request.base).hostname
        
        # Identity known_hosts
        identity = ""
        files = [f for f in os.listdir(self._minarca_identity) if f.startswith('ssh_host') if f.endswith('.pub')]
        for fn in files:
            with open(os.path.join(self._minarca_identity, fn)) as fh:
                if ':' in remotehost:
                    hostname, port = remotehost.split(':', 1)
                    identity += "[" + hostname + "]:" + port + " " + fh.read()
                else:
                    identity += remotehost + " " + fh.read()
        
        # Get remote host value from config or from URL
        return {
            "version": pkg_resources.get_distribution("minarca-server").version,
            "remotehost": remotehost,
            "identity": identity,
        }
        
    def get_disk_usage(self, userobj):
        """
        Return the user disk space.
        """
        assert isinstance(userobj, UserObject)
        
        # Get Quota from web service
        url = os.path.join(self._quota_api_url, 'quota', userobj.username)
        r = self.session.get(url, timeout=1)
        r.raise_for_status()
        diskspace = r.json()
        assert diskspace and isinstance(diskspace, dict) and 'avail' in diskspace and 'used' in diskspace and 'size' in diskspace
        return diskspace

    def get_disk_quota(self, userobj):
        """
        Get's user's disk quota.
        """
        return self.get_disk_usage(userobj)['size']

    def set_disk_quota(self, userobj, quota):
        """
        Sets the user's quota.
        """
        assert isinstance(userobj, UserObject)
        assert quota
        
        # Always update unless quota not define
        logger.info('set  user [%s] quota [%s]', userobj.username, quota)
        url = os.path.join(self._quota_api_url, 'quota', userobj.username)
        r = self.session.post(url, data={'size': quota}, timeout=1)
        r.raise_for_status()
        diskspace = r.json()
        assert diskspace and isinstance(diskspace, dict) and 'avail' in diskspace and 'used' in diskspace and 'size' in diskspace
        return diskspace
    
    def user_logined(self, userobj, attrs):
        """
        Need to verify LDAP quota and update ZFS quota if required.
        """
        assert isinstance(userobj, UserObject)
        # TODO This is specific to Minarca Saas. We need to change this.
        # Get quota value from LDAP field.
        quota = False
        descriptions = attrs and attrs.get('description')
        if descriptions:
            quota_gb = [
                int(x[1:]) for x in descriptions
                if x.startswith(b"v") and x[1:].isdigit()]
            if quota_gb:
                quota_gb = max(quota_gb)
                quota = quota_gb * 1024 * 1024 * 1024
        
        # If we found a quota value, use quota api to set it.
        logger.info('found user [%s] quota [%s] from attrs', userobj.username, quota)
        if quota:
            userobj.disk_quota = quota

    def user_added(self, userobj, attrs):
        """
        When added (manually or not). Try to get data from LDAP.
        """
        assert isinstance(userobj, UserObject)
        try:
            self._update_user_email(userobj, attrs)
        except:
            logger.warning('fail to update user [%s] email from LDAP', userobj.username, exc_info=1)
        try:
            self._update_user_root(userobj, attrs)
        except:
            logger.warning('fail to update user [%s] root', userobj.username, exc_info=1)

    def user_attr_changed(self, userobj, attrs={}):
        """
        Listen to users attributes change to update the minarca authorized_keys.
        """
        if 'user_root' in attrs:
            self._update_user_root(userobj, attrs)

        # Update minarca's authorized_keys when users update their ssh keys.
        if 'authorizedkeys' in attrs or 'user_root' in attrs:
            # TODO schedule a background task to update the authorized_keys.
            self._update_authorized_keys()
            
    def user_deleted(self, username):
        """
        When user get dleted, update the authorized_key.
        """
        self._update_authorized_keys()

    def _update_user_email(self, userobj, attrs):
        """
        Called to update the user email and home directory from LDAP info.
        """
        # Get user email from LDAP
        mail = attrs and attrs.get('mail', None)
        if not mail:
            return
        # mail might be a list.
        if hasattr(mail, '__getitem__') and len(mail):
            mail = mail[0]
            
        if isinstance(mail, bytes):
            mail = mail.decode('utf-8')
            
        logger.info('update user [%s] email from LDAP [%s]', userobj.username, mail)
        userobj.email = mail

    def _update_user_root(self, userobj, attrs):
        """
        Called to update the user's home directory. Either to define it with
        default value or restrict it to base dir.
        """
        # Define default user_home if not provided
        user_root = userobj.user_root or os.path.join(self._basedir, userobj.username)
        # Normalise path to avoid relative path.
        user_root = os.path.abspath(user_root)
        # Verify if the user_root is inside base dir.
        if self._restrited_basedir and not user_root.startswith(self._basedir):
            logger.warn('restrict user [%s] root [%s] to base dir [%s]', userobj.username, user_root, self._basedir)
            user_root = os.path.join(self._basedir, userobj.username)
        # Persist the value if different then original
        if userobj.user_root != user_root:
            userobj.user_root = user_root
            
        # Create folder if inside our base dir and missing.
        if user_root.startswith(self._basedir) and not os.path.exists(user_root):
            logger.info('creating user [%s] root dir [%s]', userobj.username, user_root)
            try:
                os.mkdir(user_root)
                # Change mode
                os.chmod(user_root, self._mode)
                # Change owner
                os.chown(user_root, pwd.getpwnam(self._owner).pw_uid, grp.getgrnam(self._group).gr_gid)
            except:
                logger.exception('fail to create user [%s] root dir [%s]', userobj.username, user_root)

    def _update_authorized_keys(self):
        """
        Used to update the authorized_keys of minarca user.
        """
        
        # Create ssh subfolder
        ssh_dir = os.path.join(self._basedir, '.ssh')
        if not os.path.exists(ssh_dir):
            logger.info("creating .ssh folder [%s]", ssh_dir)
            os.mkdir(ssh_dir, 0o700)
            try:
                os.chown(ssh_dir, pwd.getpwnam(self._owner).pw_uid, grp.getgrnam(self._group).gr_gid)
            except:
                logger.warn("fail to set permissions on [%s] folder", ssh_dir)
        
        # Create the authorized_keys file
        filename = os.path.join(ssh_dir, 'authorized_keys')
        if not os.path.exists(filename):
            logger.info("creating authorized_keys [%s]", filename)
            with open(filename, 'w+'):
                os.utime(filename, None)
                # change file permissions
                os.chmod(filename, stat.S_IRUSR | stat.S_IWUSR)
                val = os.stat(ssh_dir)
                # Also try to change owner
                os.chown(filename, val.st_uid, val.st_gid)
        
        # Get list of keys
        seen = set()
        new_data = StringIO()
        for userobj in self.app.store.users():
            for key in userobj.authorizedkeys:
                
                if key.fingerprint in seen:
                    logger.warn("duplicates key %s, sshd will ignore it")
                else:
                    seen.add(key.fingerprint)
                    
                # Add option to the key
                options = """command="%s '%s' '%s'",%s""" % (self._minarca_shell, userobj.username, userobj.user_root, self._auth_options)
                key = AuthorizedKey(options=options, keytype=key.keytype, key=key.key, comment=key.comment)
                
                # Write the new key
                authorizedkeys.add(new_data, key)
        
        # Write the new file
        logger.info("updating authorized_keys file [%s]", filename)
        with open(filename, 'w', encoding='utf-8') as f:
            f.write(new_data.getvalue())
Ejemplo n.º 6
0
class LdapPasswordStore():
    """Wrapper for LDAP authentication.

    This implementation assume the LDAP is using the system encoding."""

    uri = Option("LdapUri", doc="Get Ldap URI")
    base_dn = Option("LdapBaseDn", "", doc="Get Base DN")
    scope = Option("LdapScope", "subtree")
    tls = BoolOption("LdapTls", "false", doc="Check if TLs is enabled")
    attribute = Option("LdapAttribute", "uid", doc="Get attribute")
    filter = Option("LdapFilter", "(objectClass=*)")
    require_group = Option("LdapRequiredGroup")
    group_attribute = Option("LDAPGroupAttribute", "member")
    group_attribute_is_dn = BoolOption("LDAPGroupAttributeIsDN", "true")
    bind_dn = Option("LdapBindDn", "")
    bind_password = Option("LdapBindPassword", "")
    version = IntOption("LdapVersion", "3")
    network_timeout = IntOption("LdapNetworkTimeout", "100")
    timeout = IntOption("LdapTimeout", "300")
    encoding = Option("LdapEncoding", "utf-8", doc="Get default LdapEncoding")
    allow_password_change = BoolOption(
        "LdapAllowPasswordChange",
        "false",
        doc="Check if password change are allowed.")
    check_shadow_expire = BoolOption(
        "LdapCheckShadowExpire",
        "false",
        doc="Enable verification of Shadow Expire.")

    def __init__(self, app):
        self.app = app

    def are_valid_credentials(self, username, password):
        """Check if the given credential as valid according to LDAP."""
        assert isinstance(username, str)
        assert isinstance(password, str)
        # Skip validation if LdapUri is not provided
        if not self.uri:
            return False

        def check_crendential(l, r):
            # Check results
            if len(r) != 1:
                logger.debug("user [%s] not found in LDAP", username)
                return None

            # Bind using the user credentials. Throws an exception in case of
            # error.
            l.simple_bind_s(r[0][0], password)
            try:
                logger.info("user [%s] found in LDAP", username)

                # Verify the shadow expire
                if self.check_shadow_expire:
                    shadow_expire = self._attr_shadow_expire(r)
                    # Convert nb. days into seconds.
                    if shadow_expire and shadow_expire * 24 * 60 * 60 < time.time(
                    ):
                        logger.warn("user account %s expired: %s", username,
                                    shadow_expire)
                        raise RdiffError(
                            _('User account %s expired.' % username))

                # Get username
                dn = r[0][0]
                new_username = self._decode(r[0][1][self.attribute][0])

                # Verify if the user is member of the required group
                if self.require_group:
                    value = dn if self.group_attribute_is_dn else new_username
                    logger.info("check if user [%s] is member of [%s]", value,
                                self.require_group)
                    if not l.compare_s(self.require_group,
                                       self.group_attribute, value):
                        raise RdiffError(
                            _('Permissions denied for user account %s.' %
                              username))
            finally:
                l.unbind_s()
            # Return the username
            return new_username, r[0][1]

        # Execute the LDAP operation
        try:
            return self._execute(username, check_crendential)
        except:
            logger.exception("can't validate user [%s] credentials", username)
            return False

    def _attr(self, r, attr):
        if isinstance(attr, list):
            return dict([(x, r[0][1][x]) for x in attr if x in r[0][1]])
        elif attr in r[0][1]:
            if isinstance(r[0][1][attr], list):
                return [self._decode(x) for x in r[0][1][attr]]
            else:
                return self._decode(r[0][1][attr])
        return None

    def _attr_shadow_expire(self, r):
        """Get Shadow Expire value from `r`."""
        # get Shadow expire value
        shadow_expire = self._attr(r, 'shadowExpire')
        if not shadow_expire:
            return None
        if isinstance(shadow_expire, list):
            shadow_expire = shadow_expire[0]
        return int(shadow_expire)

    def _decode(self, value):
        """If required, decode the given bytes str into unicode."""
        if isinstance(value, bytes):
            value = value.decode(encoding=self.encoding)
        return value

    def _execute(self, username, function):
        assert isinstance(username, str)
        """Reusable method to run LDAP operation."""

        assert self.uri, "LdapUri must be define in configuration"
        assert self.base_dn, "LdapBaseDn must be define in configuration"
        if self.scope == "base":
            scope = ldap.SCOPE_BASE
        elif self.scope == "onelevel":
            scope = ldap.SCOPE_ONELEVEL
        else:
            scope = ldap.SCOPE_SUBTREE

        # try STARTLS if configured
        if self.tls:
            ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)

        # Check LDAP credential only.
        l = ldap.initialize(self.uri)

        # Set v2 or v3
        if self.version == 2:
            l.protocol_version = ldap.VERSION2
        else:
            l.protocol_version = ldap.VERSION3

        try:
            # Bind to the LDAP server
            logger.debug("binding to ldap server {}".format(self.uri))
            l.simple_bind_s(self.bind_dn, self.bind_password)

            # Search the LDAP server
            search_filter = "(&{}({}={}))".format(self.filter, self.attribute,
                                                  username)
            logger.debug("search ldap server: {}/{}?{}?{}?{}".format(
                self.uri, self.base_dn, self.attribute, scope, search_filter))
            r = l.search_s(self.base_dn, scope, search_filter)

            # Execute operation
            return function(l, r)
        except ldap.LDAPError as e:
            l.unbind_s()
            # Handle the LDAP exception and build a nice user message.
            logger.warning('ldap error', exc_info=1)
            msg = _("An LDAP error occurred: %s")
            ldap_msg = str(e)
            if hasattr(e, 'message') and isinstance(e.message, dict):
                if 'desc' in e.message:
                    ldap_msg = e.message['desc']
                if 'info' in e.message:
                    ldap_msg = e.message['info']
            raise RdiffError(msg % ldap_msg)

    def exists(self, username):
        """Check if the user exists in LDAP"""
        def check_user_exists(l, r):  # @UnusedVariable
            # Check the results
            if len(r) != 1:
                logger.debug("user [%s] not found", username)
                return False

            logger.debug("user [%s] found", username)
            return True

        # Execute the LDAP operation
        return self._execute(username, check_user_exists)

    def set_password(self, username, password, old_password=None):
        """Update the password of the given user."""
        assert isinstance(username, str)
        assert old_password is None or isinstance(old_password, str)
        assert isinstance(password, str)

        # Do nothing if password is empty
        if not password:
            raise RdiffError(_("Password can't be empty."))
        # Check if users are allowed to change their password in LDAP.
        if not self.allow_password_change:
            logger.warn(
                "authentication backend for user [%s] does not support changing the password",
                username)
            raise RdiffError(
                _("LDAP users are not allowed to change their password."))

        # Check if old_password id valid
        if old_password and not self.are_valid_credentials(
                username, old_password):
            raise RdiffError(_("Wrong password."))

        # Update the username password of the given user. If possible.
        return self._set_password_in_ldap(username, old_password, password)

    def _set_password_in_ldap(self, username, old_password, password):
        def change_passwd(l, r):
            if len(r) != 1:
                raise RdiffError(_("User %s not found." % (username, )))
            # Bind using the user credentials. Throws an exception in case of
            # error.
            if old_password is not None:
                l.simple_bind_s(r[0][0], old_password)
            l.passwd_s(r[0][0], old_password, password)
            l.unbind_s()
            logger.info("password for user [%s] is updated in LDAP", username)
            # User updated, return False
            return False

        # Execute the LDAP operation
        logger.debug("updating password for [%s] in LDAP", username)
        return self._execute(username, change_passwd)