예제 #1
0
    def __init__(self, redis, cork, config):
        self.redis = redis
        self.cork = cork
        self.config = config

        self.default_coll = config['default_coll']

        self.temp_prefix = config['temp_prefix']

        mailing_list = os.environ.get('MAILING_LIST', '').lower()
        self.mailing_list = mailing_list in ('true', '1', 'yes')
        self.default_list_endpoint = os.environ.get('MAILING_LIST_ENDPOINT', '')
        self.list_key = os.environ.get('MAILING_LIST_KEY', '')
        self.list_removal_endpoint = os.path.expandvars(
                                        os.environ.get('MAILING_LIST_REMOVAL', ''))
        self.payload = os.environ.get('MAILING_LIST_PAYLOAD', '')
        self.remove_on_delete = (os.environ.get('REMOVE_ON_DELETE', '')
                                 in ('true', '1', 'yes'))

        self.announce_list = os.environ.get('ANNOUNCE_MAILING_LIST_ENDPOINT', False)
        invites = expandvars(config.get('invites_enabled', 'true')).lower()
        self.invites_enabled = invites in ('true', '1', 'yes')

        try:
            self.redis.hsetnx('h:defaults', 'max_size', int(config['default_max_size']))
            self.redis.hsetnx('h:defaults', 'max_anon_size', int(config['default_max_anon_size']))
        except Exception as e:
            print('WARNING: Unable to init defaults: ' + str(e))

        self.all_users = UserTable(self.redis, self._get_access)

        self.invites = RedisTable(self.redis, 'h:invites')
예제 #2
0
    def __init__(self, redis, cork, config):
        super().__init__(redis, cork, config)
        self.admin_override = False
        try:
            self.redis.hsetnx('h:defaults', 'max_size',
                              int(config['default_max_size']))
            self.redis.hsetnx('h:defaults', 'max_anon_size',
                              int(config['default_max_anon_size']))
        except Exception as e:
            print('WARNING: Unable to init defaults: ' + str(e))

        self.all_users = UserTable(self.redis, self._get_access)
예제 #3
0
    def __init__(self, redis, cork, config):
        self.redis = redis
        self.cork = cork
        self.config = config

        self.default_coll = config['default_coll']

        self.temp_prefix = config['temp_prefix']

        mailing_list = os.environ.get('MAILING_LIST', '').lower()
        self.mailing_list = mailing_list in ('true', '1', 'yes')
        self.default_list_endpoint = os.environ.get('MAILING_LIST_ENDPOINT', '')
        self.list_key = os.environ.get('MAILING_LIST_KEY', '')
        self.list_removal_endpoint = os.path.expandvars(
                                        os.environ.get('MAILING_LIST_REMOVAL', ''))
        self.payload = os.environ.get('MAILING_LIST_PAYLOAD', '')
        self.remove_on_delete = (os.environ.get('REMOVE_ON_DELETE', '')
                                 in ('true', '1', 'yes'))

        self.announce_list = os.environ.get('ANNOUNCE_MAILING_LIST_ENDPOINT', False)
        invites = expandvars(config.get('invites_enabled', 'true')).lower()
        self.invites_enabled = invites in ('true', '1', 'yes')

        try:
            self.redis.hsetnx('h:defaults', 'max_size', int(config['default_max_size']))
            self.redis.hsetnx('h:defaults', 'max_anon_size', int(config['default_max_anon_size']))
        except Exception as e:
            print('WARNING: Unable to init defaults: ' + str(e))

        self.all_users = UserTable(self.redis, self._get_access)

        self.invites = RedisTable(self.redis, 'h:invites')
예제 #4
0
class UserManager(object):
    USER_RX = re.compile(r'^[A-Za-z0-9][\w-]{2,30}$')

    RESTRICTED_NAMES = ['login', 'logout', 'user', 'admin', 'manager', 'coll', 'collection',
                        'guest', 'settings', 'profile', 'api', 'anon', 'webrecorder',
                        'anonymous', 'register', 'join', 'download', 'live', 'embed']

    PASS_RX = re.compile(r'^(?=.*[\d\W])(?=.*[a-z])(?=.*[A-Z]).{8,}$')

    LC_USERNAMES_KEY = 'h:lc_users'

    def __init__(self, redis, cork, config):
        self.redis = redis
        self.cork = cork
        self.config = config

        self.default_coll = config['default_coll']

        self.temp_prefix = config['temp_prefix']

        mailing_list = os.environ.get('MAILING_LIST', '').lower()
        self.mailing_list = mailing_list in ('true', '1', 'yes')
        self.default_list_endpoint = os.environ.get('MAILING_LIST_ENDPOINT', '')
        self.list_key = os.environ.get('MAILING_LIST_KEY', '')
        self.list_removal_endpoint = os.path.expandvars(
                                        os.environ.get('MAILING_LIST_REMOVAL', ''))
        self.payload = os.environ.get('MAILING_LIST_PAYLOAD', '')
        self.remove_on_delete = (os.environ.get('REMOVE_ON_DELETE', '')
                                 in ('true', '1', 'yes'))

        self.announce_list = os.environ.get('ANNOUNCE_MAILING_LIST_ENDPOINT', False)
        invites = expandvars(config.get('invites_enabled', 'true')).lower()
        self.invites_enabled = invites in ('true', '1', 'yes')

        try:
            self.redis.hsetnx('h:defaults', 'max_size', int(config['default_max_size']))
            self.redis.hsetnx('h:defaults', 'max_anon_size', int(config['default_max_anon_size']))
        except Exception as e:
            print('WARNING: Unable to init defaults: ' + str(e))

        self.all_users = UserTable(self.redis, self._get_access)

        self.invites = RedisTable(self.redis, 'h:invites')

    def register_user(self, input_data, host):
        msg = OrderedDict()
        redir_extra = ''

        username = input_data.get('username', '')
        full_name = input_data.get('full_name', '')
        email = input_data.get('email', '')

        if 'username' not in input_data:
            msg['username'] = '******'

        elif username.startswith(self.temp_prefix):
            msg['username'] = '******'

        if 'email' not in input_data:
            msg['email'] = 'Missing Email'

        if self.invites_enabled:
            try:
                val_email = self.is_valid_invite(input_data['invite'])
                if val_email != email:
                    raise ValidationException('Sorry, this invite can only be used with email: {0}'.format(val_email))
            except ValidationException as ve:
                msg['invite'] = str(ve)

            else:
                redir_extra = '?invite=' + input_data.get('invite', '')

        try:
            self.validate_user(username, email)
            self.validate_password(input_data['password'], input_data['confirmpassword'])

        except ValidationException as ve:
            msg['validation'] = str(ve)

        try:
            move_info = self.get_move_temp_info(input_data)
        except ValidationException as ve:
            msg['move_info'] = str(ve)

        if msg:
            return msg, redir_extra


        try:
            desc = {'name': full_name}

            if move_info:
                desc['move_info'] = move_info

            desc = json.dumps(desc)

            self.cork.register(username, input_data['password'], email, role='archivist',
                          max_level=50,
                          subject='webrecorder.io Account Creation',
                          email_template='webrecorder/templates/emailconfirm.html',
                          description=desc,
                          host=host)

            # add to announce list if user opted in
            if input_data.get('announce_mailer') and self.announce_list:
                self.add_to_mailing_list(username, email, full_name,
                                         list_endpoint=self.announce_list)

            if self.invites_enabled:
                self.delete_invite(email)

            # extend session for upto 90 mins to store data to be migrated
            # to allow time for user to validate registration
            if move_info:
                self.get_session().save()

        except ValidationException as ve:
            msg['validation'] = str(ve)

        except Exception as ex:
            import traceback
            traceback.print_exc()
            msg['other_error'] = 'Registration failed: ' + str(ex)

        if not msg:
            msg['success'] = ('A confirmation e-mail has been sent to <b>{0}</b>. ' +
                              'Please check your e-mail to complete the registration!').format(username)

        return msg, redir_extra

    def get_move_temp_info(self, input_data):
        move_temp = input_data.get('moveTemp')

        if not move_temp:
            return None

        to_coll_title = input_data.get('toColl', '')
        to_coll = sanitize_title(to_coll_title)

        if not to_coll:
            raise ValidationException('invalid_coll_name')


        if not self.access.session_user.is_anon():
            raise ValidationException('invalid_user_import')

        return {'from_user': self.access.session_user.name,
                'to_coll': to_coll,
                'to_title': to_coll_title,
               }

    def validate_registration(self, reg_code, cookie, username):
        cookie_validate = 'valreg=' + reg_code

        if cookie_validate not in cookie:
            return {'error': 'invalid_code'}

        try:
            user, first_coll = self.create_user_from_reg(reg_code, username)

            return {'registered': user.name,
                    'first_coll_name': first_coll.name}

        except ValidationException as ve:
            return {'error': ve.msg}

        except Exception as e:
            import traceback
            traceback.print_exc()
            return {'error': 'invalid_code'}

    def find_case_insensitive_username(self, username):
        lower_username = username.lower()

        new_username = self.redis.hget(self.LC_USERNAMES_KEY,  lower_username)
        if new_username == '-' or new_username == username or new_username is None:
            return None

        if new_username == '':
            return lower_username

        return new_username

    def login_user(self, input_data):
        """Authenticate users"""
        username = input_data.get('username', '')
        password = input_data.get('password', '')

        try:
            move_info = self.get_move_temp_info(input_data)
        except ValidationException as ve:
            return {'error': str(ve)}

        # first, authenticate the user
        # if failing, see if case-insensitive username and try that
        if not self.cork.is_authenticate(username, password):
            username = self.find_case_insensitive_username(username)
            if not username or not self.cork.is_authenticate(username, password):
                return {'error': 'invalid_login'}

        # if not enough space, don't continue with login
        if move_info:
            if not self.has_space_for_new_collection(username,
                                                     move_info['from_user'],
                                                    'temp'):
                #return {'error': 'Sorry, not enough space to import this Temporary Collection into your account.'}
                return {'error': 'out_of_space'}

        user = self.all_users[username]
        new_collection = None

        try:
            if move_info:
                new_collection = self.move_temp_coll(user, move_info)
        except DupeNameException as de:
            return {'error': 'duplicate_name'}
            #return {'error': 'Collection "{0}" already exists'.format(move_info['to_title'])}

        remember_me = get_bool(input_data.get('remember_me'))

        # login session and access system
        self.access.log_in(username, remember_me)

        user.update_last_login()

        return {'success': '1',
                'new_coll_name': new_collection.name if new_collection else None,
                'user': user}

    def logout(self):
        sesh = self.get_session()
        sesh.delete()
        return

    def has_user_email(self, email):
        #TODO: implement a email table, if needed?

        for n, user_data in self.all_users.items():
            if user_data['email_addr'] == email:
                return True

        return False

    def get_user_email(self, user):
        if not user:
            return ''

        try:
            user_data = self.all_users[user]
        except:
            user_data = None

        if user_data:
            return user_data.get('email_addr', '')
        else:
            return ''

    def is_username_available(self, username):
        username_lc = username.lower()

        # username matches of the restricted names
        if username_lc in self.RESTRICTED_NAMES:
            return False

        # username doesn't match the allowed regex
        if not self.USER_RX.match(username):
            return False

        # lowercase username already exists
        if self.redis.hexists(self.LC_USERNAMES_KEY, username_lc):
            return False

        # username already exists! (shouldn't match if lowercase exists, but just in case)
        if username in self.all_users:
            return False

        return True

    def validate_user(self, user, email):
        if not self.is_username_available(user):
            raise ValidationException('username_not_available')

        if self.has_user_email(email):
            raise ValidationException('email_not_available')

        return True

    def validate_password(self, password, confirm):
        if password != confirm:
            raise ValidationException('password_mismatch')

        if not self.PASS_RX.match(password):
            raise ValidationException('password_invalid')

        return True

    def _get_access(self):
        return request['webrec.access']

    @property
    def access(self):
        return self._get_access()

    def get_roles(self):
        return [x for x in self.cork._store.roles]

    def get_user_coll(self, username, coll_name):
        try:
            user = self.all_users[username]
        except:
            return None, None

        collection = user.get_collection_by_name(coll_name)
        return user, collection

    def get_user_coll_rec(self, username, coll_name, rec):
        user, collection = self.get_user_coll(username, coll_name)
        if collection:
            recording = collection.get_recording(rec)
        else:
            recording = None

        return user, collection, recording

    def update_password(self, curr_password, password, confirm):
        username = self.access.session_user.name

        if not self.cork.verify_password(username, curr_password):
            raise ValidationException('invalid_password')

        self.validate_password(password, confirm)

        self.cork.update_password(username, password)

    def reset_password(self, password, confirm, resetcode):
        self.validate_password(password, confirm)

        try:
            self.cork.reset_password(resetcode, password)
        except AuthException:
            raise ValidationException('invalid_reset_code')

    def is_valid_invite(self, invitekey):
        try:
            if not invitekey:
                return False

            key = base64.b64decode(invitekey.encode('utf-8')).decode('utf-8')
            key.split(':', 1)
            email, hash_ = key.split(':', 1)

            entry = self.invites[email]

            if entry and entry.get('hash_') == hash_:
                return email
        except Exception as e:
            print(e)
            pass

        msg = 'Sorry, that is not a valid invite code. Please try again or request another invite'
        raise ValidationException(msg)

    def delete_invite(self, email):
        try:
            archive_invites = RedisTable(self.redis, 'h:arc_invites')
            archive_invites[email] = self.invites[email]
        except:
            pass

        del self.invites[email]

    def save_invite(self, email, name, desc=''):
        if not email or not name:
            return False

        self.invites[email] = {'name': name, 'email': email, 'reg_data': desc}
        return True

    def send_invite(self, email, email_template, host):
        entry = self.invites[email]
        if not entry:
            print('No Such Email In Invite List')
            return False

        hash_ = base64.b64encode(os.urandom(21)).decode('utf-8')
        entry['hash_'] = hash_

        full_hash = email + ':' + hash_
        invitekey = base64.b64encode(full_hash.encode('utf-8')).decode('utf-8')

        email_text = template(
            email_template,
            host=host,
            email_addr=email,
            name=entry.get('name', email),
            invite=invitekey,
        )
        self.cork.mailer.send_email(email, 'You are invited to join webrecorder.io beta!', email_text)
        entry['sent'] = str(datetime.utcnow())
        return True

    def add_to_mailing_list(self, username, email, name, list_endpoint=None):
        """3rd party mailing list subscription"""
        if not (list_endpoint or self.default_list_endpoint) or not self.list_key:
            print('MAILING_LIST is turned on, but required fields are '
                  'missing.')
            return

        # if no endpoint provided, use default
        if list_endpoint is None:
            list_endpoint = self.default_list_endpoint

        try:
            res = requests.post(list_endpoint,
                                auth=('nop', self.list_key),
                                data=self.payload.format(
                                    email=email,
                                    name=name,
                                    username=username),
                                timeout=1.5)

            if res.status_code != 200:
                print('Unexpected mailing list API response.. '
                      'status code: {0.status_code}\n'
                      'content: {0.content}'.format(res))

        except Exception as e:
            if e is requests.exceptions.Timeout:
                print('Mailing list API timed out..')
            else:
                print('Adding to mailing list failed:', e)

    def remove_from_mailing_list(self, email):
        """3rd party mailing list removal"""
        if not self.list_removal_endpoint or not self.list_key:
            # fail silently, log info
            print('REMOVE_ON_DELETE is turned on, but required '
                  'fields are missing.')
            return

        try:
            email = email.encode('utf-8').lower()
            email_hash = hashlib.md5(email).hexdigest()
            res = requests.delete(self.list_removal_endpoint.format(email_hash),
                                  auth=('nop', self.list_key),
                                  timeout=1.5)

            if res.status_code != 204:
                print('Unexpected mailing list API response.. '
                      'status code: {0.status_code}\n'
                      'content: {0.content}'.format(res))

        except Exception as e:
            if e is requests.exceptions.Timeout:
                print('Mailing list API timed out..')
            else:
                print('Removing from mailing list failed:', e)

    def get_session(self):
        return request.environ['webrec.session']

    def create_new_user(self, username, init_info=None):
        init_info = init_info or {}

        user = self.all_users.make_user(username)
        user.create_new()

        # track lowercase username
        lower_username = username.lower()
        self.redis.hset(self.LC_USERNAMES_KEY, lower_username,
                        username if lower_username != username else '')

        first_coll = None

        move_info = init_info.get('move_info')
        if move_info:
            first_coll = self.move_temp_coll(user, move_info)

        elif self.default_coll:
            first_coll = user.create_collection(self.default_coll['id'],
                                   title=self.default_coll['title'],
                                   desc=self.default_coll['desc'].format(username),
                                   public=False)

        # email subscription set up?
        if self.mailing_list:
            name = init_info.get('name', '')
            self.add_to_mailing_list(username, user['email_addr'], name)

        return user, first_coll

    def create_user_as_admin(self, email, username, passwd, passwd2, role, name):
        """Create a new user with command line arguments or series of prompts,
           preforming basic validation
        """
        self.access.assert_is_superuser()

        errs = []

        # EMAIL
        # validate email
        if not re.match(r'[\w.-/+]+@[\w.-]+.\w+', email):
            errs.append('valid email required!')

        if email in [data['email_addr'] for u, data in self.all_users.items()]:
            errs.append('A user already exists with {0} email!'.format(email))

        # USERNAME
        # validate username
        if not username:
            errs.append('please specify a username!')

        if not self.is_username_available(username):
            errs.append('Invalid username.')

        # ROLE
        if role not in self.get_roles():
            errs.append('Not a valid role.')

        # PASSWD
        if passwd != passwd2 or not self.PASS_RX.match(passwd):
            errs.append('Passwords must match and be at least 8 characters long '
                        'with lowercase, uppercase, and either digits or symbols.')

        if errs:
            return errs, None

        # add user to cork
        #self.cork._store.users[username] = {
        self.all_users[username] = {
            'role': role,
            'hash': self.cork._hash(username, passwd).decode('ascii'),
            'email_addr': email,
            'full_name': name,
            'creation_date': str(datetime.utcnow()),
            'last_login': str(datetime.utcnow()),
        }
        #self.cork._store.save_users()

        return None, self.create_new_user(username, {'email': email,
                                                     'name': name})
    def create_user_from_reg(self, reg, username):
        user, init_info = self.cork.validate_registration(reg, username)

        if init_info:
            init_info = json.loads(init_info)

        user, first_coll = self.create_new_user(user, init_info)

        # login here
        self.access.log_in(user.name, remember_me=False)

        return user, first_coll

    def update_user_as_admin(self, user, data):
        """ Update any property on specified user
        For admin-only
        """
        self.access.assert_is_curr_user(user)

        errs = []

        if not data:
            errs.append('Nothing To Update')

        if 'role' in data and data['role'] not in self.get_roles():
            errs.append('Not a valid role.')

        if 'max_size' in data and not isinstance(data['max_size'], int):
            errs.append('max_size must be an int')

        if errs:
            return errs

        if 'name' in data:
            #user['desc'] = '{{"name":"{name}"}}'.format(name=data.get('name', ''))
            user['name'] = data.get('name', '')

        if 'desc' in data:
            user['desc'] = data['desc']

        if 'max_size' in data:
            user['max_size'] = data['max_size']

        if 'role' in data:
            user['role'] = data['role']

        return None

    def delete_user(self, username):
        try:
            user = self.all_users[username]
            self.access.assert_is_curr_user(user)
        except Exception:
            return False

        if self.mailing_list and self.remove_on_delete:
            self.remove_from_mailing_list(user['email_addr'])

        # remove user and from all users table
        del self.all_users[username]

        try:
            self.get_session().delete()
        except Exception:
            pass

        return True

    def has_space_for_new_collection(self, to_username, from_username, coll_name):
        try:
            to_user = self.all_users[to_username]
        except:
            return False

        from_user = self.all_users[from_username]
        collection = from_user.get_collection_by_name(coll_name)
        if not collection:
            return False

        return (collection.size <= to_user.get_size_remaining())

    def move_temp_coll(self, user, move_info):
        from_user = self.all_users[move_info['from_user']]
        temp_coll = from_user.get_collection_by_name('temp')
        if not from_user.move(temp_coll, move_info['to_coll'], user):
            return None

        temp_coll.set_prop('title', move_info['to_title'])

        # don't delete data in temp user dir as its waiting to be committed!
        self.get_session().set_anon_commit_wait()

        for recording in temp_coll.get_recordings():
            # will be marked for commit
            recording.set_closed()

        return temp_coll
예제 #5
0
class UserManager(object):
    USER_RX = re.compile(r'^[A-Za-z0-9][\w-]{2,30}$')

    RESTRICTED_NAMES = ['login', 'logout', 'user', 'admin', 'manager', 'coll', 'collection',
                        'guest', 'settings', 'profile', 'api', 'anon', 'webrecorder',
                        'anonymous', 'register', 'join', 'download', 'live', 'embed']

    PASS_RX = re.compile(r'^(?=.*[\d\W])(?=.*[a-z])(?=.*[A-Z]).{8,}$')

    LC_USERNAMES_KEY = 'h:lc_users'

    def __init__(self, redis, cork, config):
        self.redis = redis
        self.cork = cork
        self.config = config

        self.default_coll = config['default_coll']

        self.temp_prefix = config['temp_prefix']

        mailing_list = os.environ.get('MAILING_LIST', '').lower()
        self.mailing_list = mailing_list in ('true', '1', 'yes')
        self.default_list_endpoint = os.environ.get('MAILING_LIST_ENDPOINT', '')
        self.list_key = os.environ.get('MAILING_LIST_KEY', '')
        self.list_removal_endpoint = os.path.expandvars(
                                        os.environ.get('MAILING_LIST_REMOVAL', ''))
        self.payload = os.environ.get('MAILING_LIST_PAYLOAD', '')
        self.remove_on_delete = (os.environ.get('REMOVE_ON_DELETE', '')
                                 in ('true', '1', 'yes'))

        self.announce_list = os.environ.get('ANNOUNCE_MAILING_LIST_ENDPOINT', False)
        invites = expandvars(config.get('invites_enabled', 'true')).lower()
        self.invites_enabled = invites in ('true', '1', 'yes')

        try:
            self.redis.hsetnx('h:defaults', 'max_size', int(config['default_max_size']))
            self.redis.hsetnx('h:defaults', 'max_anon_size', int(config['default_max_anon_size']))
        except Exception as e:
            print('WARNING: Unable to init defaults: ' + str(e))

        self.all_users = UserTable(self.redis, self._get_access)

        self.invites = RedisTable(self.redis, 'h:invites')

    def register_user(self, input_data, host):
        msg = OrderedDict()
        redir_extra = ''

        username = input_data.get('username', '')
        full_name = input_data.get('full_name', '')
        email = input_data.get('email', '')

        if 'username' not in input_data:
            msg['username'] = '******'

        elif username.startswith(self.temp_prefix):
            msg['username'] = '******'

        if 'email' not in input_data:
            msg['email'] = 'Missing Email'

        if self.invites_enabled:
            try:
                val_email = self.is_valid_invite(input_data['invite'])
                if val_email != email:
                    raise ValidationException('Sorry, this invite can only be used with email: {0}'.format(val_email))
            except ValidationException as ve:
                msg['invite'] = str(ve)

            else:
                redir_extra = '?invite=' + input_data.get('invite', '')

        try:
            self.validate_user(username, email)
            self.validate_password(input_data['password'], input_data['confirmpassword'])

        except ValidationException as ve:
            msg['validation'] = str(ve)

        try:
            move_info = self.get_move_temp_info(input_data)
        except ValidationException as ve:
            msg['move_info'] = str(ve)

        if msg:
            return msg, redir_extra


        try:
            desc = {'name': full_name}

            if move_info:
                desc['move_info'] = move_info

            desc = json.dumps(desc)

            self.cork.register(username, input_data['password'], email, role='archivist',
                          max_level=50,
                          subject='webrecorder.io Account Creation',
                          email_template='webrecorder/templates/emailconfirm.html',
                          description=desc,
                          host=host)

            # add to announce list if user opted in
            if input_data.get('announce_mailer') and self.announce_list:
                self.add_to_mailing_list(username, email, full_name,
                                         list_endpoint=self.announce_list)

            if self.invites_enabled:
                self.delete_invite(email)

            # extend session for upto 90 mins to store data to be migrated
            # to allow time for user to validate registration
            if move_info:
                self.get_session().save()

        except ValidationException as ve:
            msg['validation'] = str(ve)

        except Exception as ex:
            import traceback
            traceback.print_exc()
            msg['other_error'] = 'Registration failed: ' + str(ex)

        if not msg:
            msg['success'] = ('A confirmation e-mail has been sent to <b>{0}</b>. ' +
                              'Please check your e-mail to complete the registration!').format(username)

        return msg, redir_extra

    def get_move_temp_info(self, input_data):
        move_temp = input_data.get('moveTemp')

        if not move_temp:
            return None

        to_coll_title = input_data.get('toColl', '')
        to_coll = sanitize_title(to_coll_title)

        if not to_coll:
            raise ValidationException('invalid_coll_name')


        if not self.access.session_user.is_anon():
            raise ValidationException('invalid_user_import')

        return {'from_user': self.access.session_user.name,
                'to_coll': to_coll,
                'to_title': to_coll_title,
               }

    def validate_registration(self, reg_code, cookie, username):
        cookie_validate = 'valreg=' + reg_code

        if cookie_validate not in cookie:
            return {'error': 'invalid_code'}

        try:
            user, first_coll = self.create_user_from_reg(reg_code, username)

            return {'registered': user.name,
                    'first_coll_name': first_coll.name}

        except ValidationException as ve:
            return {'error': ve.msg}

        except Exception as e:
            import traceback
            traceback.print_exc()
            return {'error': 'invalid_code'}

    def find_case_insensitive_username(self, username):
        lower_username = username.lower()

        new_username = self.redis.hget(self.LC_USERNAMES_KEY,  lower_username)
        if new_username == '-' or new_username == username or new_username is None:
            return None

        if new_username == '':
            return lower_username

        return new_username

    def login_user(self, input_data):
        """Authenticate users"""
        username = input_data.get('username', '')
        password = input_data.get('password', '')

        try:
            move_info = self.get_move_temp_info(input_data)
        except ValidationException as ve:
            return {'error': str(ve)}

        # first, authenticate the user
        # if failing, see if case-insensitive username and try that
        if not self.cork.is_authenticate(username, password):
            username = self.find_case_insensitive_username(username)
            if not username or not self.cork.is_authenticate(username, password):
                return {'error': 'invalid_login'}

        # if not enough space, don't continue with login
        if move_info:
            if not self.has_space_for_new_collection(username,
                                                     move_info['from_user'],
                                                    'temp'):
                #return {'error': 'Sorry, not enough space to import this Temporary Collection into your account.'}
                return {'error': 'out_of_space'}

        user = self.all_users[username]
        new_collection = None

        try:
            if move_info:
                new_collection = self.move_temp_coll(user, move_info)
        except DupeNameException as de:
            return {'error': 'duplicate_name'}
            #return {'error': 'Collection "{0}" already exists'.format(move_info['to_title'])}

        remember_me = get_bool(input_data.get('remember_me'))

        # login session and access system
        self.access.log_in(username, remember_me)

        user.update_last_login()

        return {'success': '1',
                'new_coll_name': new_collection.name if new_collection else None,
                'user': user}

    def logout(self):
        sesh = self.get_session()
        sesh.delete()
        return

    def has_user_email(self, email):
        #TODO: implement a email table, if needed?

        for n, user_data in self.all_users.items():
            if user_data['email_addr'] == email:
                return True

        return False

    def get_user_email(self, user):
        if not user:
            return ''

        try:
            user_data = self.all_users[user]
        except:
            user_data = None

        if user_data:
            return user_data.get('email_addr', '')
        else:
            return ''

    def is_username_available(self, username):
        username_lc = username.lower()

        # username matches of the restricted names
        if username_lc in self.RESTRICTED_NAMES:
            return False

        # username doesn't match the allowed regex
        if not self.USER_RX.match(username):
            return False

        # lowercase username already exists
        if self.redis.hexists(self.LC_USERNAMES_KEY, username_lc):
            return False

        # username already exists! (shouldn't match if lowercase exists, but just in case)
        if username in self.all_users:
            return False

        return True

    def validate_user(self, user, email):
        if not self.is_username_available(user):
            raise ValidationException('username_not_available')

        if self.has_user_email(email):
            raise ValidationException('email_not_available')

        return True

    def validate_password(self, password, confirm):
        if password != confirm:
            raise ValidationException('password_mismatch')

        if not self.PASS_RX.match(password):
            raise ValidationException('password_invalid')

        return True

    def _get_access(self):
        return request['webrec.access']

    @property
    def access(self):
        return self._get_access()

    def get_roles(self):
        return [x for x in self.cork._store.roles]

    def get_user_coll(self, username, coll_name):
        try:
            user = self.all_users[username]
        except:
            return None, None

        collection = user.get_collection_by_name(coll_name)
        return user, collection

    def get_user_coll_rec(self, username, coll_name, rec):
        user, collection = self.get_user_coll(username, coll_name)
        if collection:
            recording = collection.get_recording(rec)
        else:
            recording = None

        return user, collection, recording

    def update_password(self, curr_password, password, confirm):
        username = self.access.session_user.name

        if not self.cork.verify_password(username, curr_password):
            raise ValidationException('invalid_password')

        self.validate_password(password, confirm)

        self.cork.update_password(username, password)

    def reset_password(self, password, confirm, resetcode):
        self.validate_password(password, confirm)

        try:
            self.cork.reset_password(resetcode, password)
        except AuthException:
            raise ValidationException('invalid_reset_code')

    def is_valid_invite(self, invitekey):
        try:
            if not invitekey:
                return False

            key = base64.b64decode(invitekey.encode('utf-8')).decode('utf-8')
            key.split(':', 1)
            email, hash_ = key.split(':', 1)

            entry = self.invites[email]

            if entry and entry.get('hash_') == hash_:
                return email
        except Exception as e:
            print(e)
            pass

        msg = 'Sorry, that is not a valid invite code. Please try again or request another invite'
        raise ValidationException(msg)

    def delete_invite(self, email):
        try:
            archive_invites = RedisTable(self.redis, 'h:arc_invites')
            archive_invites[email] = self.invites[email]
        except:
            pass

        del self.invites[email]

    def save_invite(self, email, name, desc=''):
        if not email or not name:
            return False

        self.invites[email] = {'name': name, 'email': email, 'reg_data': desc}
        return True

    def send_invite(self, email, email_template, host):
        entry = self.invites[email]
        if not entry:
            print('No Such Email In Invite List')
            return False

        hash_ = base64.b64encode(os.urandom(21)).decode('utf-8')
        entry['hash_'] = hash_

        full_hash = email + ':' + hash_
        invitekey = base64.b64encode(full_hash.encode('utf-8')).decode('utf-8')

        email_text = template(
            email_template,
            host=host,
            email_addr=email,
            name=entry.get('name', email),
            invite=invitekey,
        )
        self.cork.mailer.send_email(email, 'You are invited to join webrecorder.io beta!', email_text)
        entry['sent'] = str(datetime.utcnow())
        return True

    def add_to_mailing_list(self, username, email, name, list_endpoint=None):
        """3rd party mailing list subscription"""
        if not (list_endpoint or self.default_list_endpoint) or not self.list_key:
            print('MAILING_LIST is turned on, but required fields are '
                  'missing.')
            return

        # if no endpoint provided, use default
        if list_endpoint is None:
            list_endpoint = self.default_list_endpoint

        try:
            res = requests.post(list_endpoint,
                                auth=('nop', self.list_key),
                                data=self.payload.format(
                                    email=email,
                                    name=name,
                                    username=username),
                                timeout=1.5)

            if res.status_code != 200:
                print('Unexpected mailing list API response.. '
                      'status code: {0.status_code}\n'
                      'content: {0.content}'.format(res))

        except Exception as e:
            if e is requests.exceptions.Timeout:
                print('Mailing list API timed out..')
            else:
                print('Adding to mailing list failed:', e)

    def remove_from_mailing_list(self, email):
        """3rd party mailing list removal"""
        if not self.list_removal_endpoint or not self.list_key:
            # fail silently, log info
            print('REMOVE_ON_DELETE is turned on, but required '
                  'fields are missing.')
            return

        try:
            email = email.encode('utf-8').lower()
            email_hash = hashlib.md5(email).hexdigest()
            res = requests.delete(self.list_removal_endpoint.format(email_hash),
                                  auth=('nop', self.list_key),
                                  timeout=1.5)

            if res.status_code != 204:
                print('Unexpected mailing list API response.. '
                      'status code: {0.status_code}\n'
                      'content: {0.content}'.format(res))

        except Exception as e:
            if e is requests.exceptions.Timeout:
                print('Mailing list API timed out..')
            else:
                print('Removing from mailing list failed:', e)

    def get_session(self):
        return request.environ['webrec.session']

    def create_new_user(self, username, init_info=None):
        init_info = init_info or {}

        user = self.all_users.make_user(username)
        user.create_new()

        # track lowercase username
        lower_username = username.lower()
        self.redis.hset(self.LC_USERNAMES_KEY, lower_username,
                        username if lower_username != username else '')

        first_coll = None

        move_info = init_info.get('move_info')
        if move_info:
            first_coll = self.move_temp_coll(user, move_info)

        elif self.default_coll:
            first_coll = user.create_collection(self.default_coll['id'],
                                   title=self.default_coll['title'],
                                   desc=self.default_coll['desc'].format(username),
                                   public=False)

        # email subscription set up?
        if self.mailing_list:
            name = init_info.get('name', '')
            self.add_to_mailing_list(username, user['email_addr'], name)

        return user, first_coll

    def create_user_as_admin(self, email, username, passwd, passwd2, role, name):
        """Create a new user with command line arguments or series of prompts,
           preforming basic validation
        """
        self.access.assert_is_superuser()

        errs = []

        # EMAIL
        # validate email
        if not re.match(r'[\w.-/+]+@[\w.-]+.\w+', email):
            errs.append('valid email required!')

        if email in [data['email_addr'] for u, data in self.all_users.items()]:
            errs.append('A user already exists with {0} email!'.format(email))

        # USERNAME
        # validate username
        if not username:
            errs.append('please specify a username!')

        if not self.is_username_available(username):
            errs.append('Invalid username.')

        # ROLE
        if role not in self.get_roles():
            errs.append('Not a valid role.')

        # PASSWD
        if passwd != passwd2 or not self.PASS_RX.match(passwd):
            errs.append('Passwords must match and be at least 8 characters long '
                        'with lowercase, uppercase, and either digits or symbols.')

        if errs:
            return errs, None

        # add user to cork
        #self.cork._store.users[username] = {
        self.all_users[username] = {
            'role': role,
            'hash': self.cork._hash(username, passwd).decode('ascii'),
            'email_addr': email,
            'full_name': name,
            'creation_date': str(datetime.utcnow()),
            'last_login': str(datetime.utcnow()),
        }
        #self.cork._store.save_users()

        return None, self.create_new_user(username, {'email': email,
                                                     'name': name})
    def create_user_from_reg(self, reg, username):
        user, init_info = self.cork.validate_registration(reg, username)

        if init_info:
            init_info = json.loads(init_info)

        user, first_coll = self.create_new_user(user, init_info)

        # login here
        self.access.log_in(user.name, remember_me=False)

        return user, first_coll

    def update_user_as_admin(self, user, data):
        """ Update any property on specified user
        For admin-only
        """
        self.access.assert_is_curr_user(user)

        errs = []

        if not data:
            errs.append('Nothing To Update')

        if 'role' in data and data['role'] not in self.get_roles():
            errs.append('Not a valid role.')

        if 'max_size' in data and not isinstance(data['max_size'], int):
            errs.append('max_size must be an int')

        if errs:
            return errs

        if 'name' in data:
            #user['desc'] = '{{"name":"{name}"}}'.format(name=data.get('name', ''))
            user['name'] = data.get('name', '')

        if 'desc' in data:
            user['desc'] = data['desc']

        if 'max_size' in data:
            user['max_size'] = data['max_size']

        if 'role' in data:
            user['role'] = data['role']

        if 'customer_id' in data:
            user['customer_id'] = data['customer_id']

        if 'customer_max_size' in data:
            user['customer_max_size'] = data['customer_max_size']

        return None

    def delete_user(self, username):
        try:
            user = self.all_users[username]
            self.access.assert_is_curr_user(user)
        except Exception:
            return False

        if self.mailing_list and self.remove_on_delete:
            self.remove_from_mailing_list(user['email_addr'])

        # remove user and from all users table
        del self.all_users[username]

        try:
            self.get_session().delete()
        except Exception:
            pass

        return True

    def has_space_for_new_collection(self, to_username, from_username, coll_name):
        try:
            to_user = self.all_users[to_username]
        except:
            return False

        from_user = self.all_users[from_username]
        collection = from_user.get_collection_by_name(coll_name)
        if not collection:
            return False

        return (collection.size <= to_user.get_size_remaining())

    def move_temp_coll(self, user, move_info):
        from_user = self.all_users[move_info['from_user']]
        temp_coll = from_user.get_collection_by_name('temp')
        if not from_user.move(temp_coll, move_info['to_coll'], user):
            return None

        temp_coll.set_prop('title', move_info['to_title'])

        # don't delete data in temp user dir as its waiting to be committed!
        self.get_session().set_anon_commit_wait()

        for recording in temp_coll.get_recordings():
            # will be marked for commit
            recording.set_closed()

        return temp_coll
예제 #6
0
 def __init__(self, redis):
     self.redis = redis
     self.access = BaseAccess()
     self.users = UserTable(self.redis, self.get_access)
     self.roles = RedisTable(self.redis, 'h:roles')
     self.pending_registrations = RedisTable(self.redis, 'h:register')
예제 #7
0
class LdapUserManager(UserManager):
    def __init__(self, redis, cork, config):
        super().__init__(redis, cork, config)
        self.admin_override = False
        try:
            self.redis.hsetnx('h:defaults', 'max_size',
                              int(config['default_max_size']))
            self.redis.hsetnx('h:defaults', 'max_anon_size',
                              int(config['default_max_anon_size']))
        except Exception as e:
            print('WARNING: Unable to init defaults: ' + str(e))

        self.all_users = UserTable(self.redis, self._get_access)

    def _get_access(self):
        if self.admin_override:
            self.admin_override = False
            return BaseAccess()
        else:
            return request['webrec.access']

    def get_authenticated_user(self, username, password):
        """Returns the user matching the supplied username and password otherwise
        returns None

        :param str username: The username of the user
        :param str password: The users password
        :return: The authenticated user
        :rtype: User|None
        """
        ldap_username = username + '@' + os.environ.get('LDAP_DOMAIN')
        print('ldapusermanager authenticating {}'.format(ldap_username))
        c = ldap.initialize(os.environ.get('LDAP_URI', ''))

        c.protocol_version = 3
        c.set_option(ldap.OPT_REFERRALS, 0)
        try:
            result = c.simple_bind_s(ldap_username, password)
            adminusers = c.search_s(
                os.environ.get('LDAP_BASE'), ldap.SCOPE_SUBTREE,
                '(&(sAMAccountName={})(memberOf={}))'.format(
                    username, os.environ.get('LDAP_ADMIN_GROUP')))
            loginusers = c.search_s(
                os.environ.get('LDAP_BASE'), ldap.SCOPE_SUBTREE,
                '(&(sAMAccountName={})(memberOf={}))'.format(
                    username, os.environ.get('LDAP_LOGIN_GROUP')))
            is_admin = len([dn for (dn, attrs) in adminusers if dn]) == 1
            can_login = len([dn for (dn, attrs) in loginusers if dn]) == 1

            escaped_username = username.replace(".", "_")

            if not can_login:
                return None

            try:
                self.cork.is_authenticate(escaped_username, password)
                self.admin_override = True
                self.all_users[escaped_username] = {
                    'role':
                    'admin' if is_admin else 'archivist',
                    'hash':
                    self.cork._hash(escaped_username,
                                    password).decode('ascii'),
                    'email_addr':
                    "NYI",
                    'full_name':
                    username,
                    'creation_date':
                    str(datetime.utcnow()),
                    'last_login':
                    str(datetime.utcnow()),
                }
                return self.all_users[escaped_username]
            except Exception as e:
                print("user not found, exception was:")
                print(e)

            print('creating internal user')
            self.admin_override = True
            self.all_users[escaped_username] = {
                'role': 'admin' if is_admin else 'archivist',
                'hash': self.cork._hash(escaped_username,
                                        password).decode('ascii'),
                'email_addr': "NYI",
                'full_name': username,
                'creation_date': str(datetime.utcnow()),
                'last_login': str(datetime.utcnow()),
            }
            self.admin_override = True
            self.create_new_user(escaped_username)

            print('created internal user: {}'.format(
                self.all_users[escaped_username]))
            self.admin_override = False
            self.cork.is_authenticate(escaped_username, password)
            return self.all_users[escaped_username]
        except Exception as e:
            print(
                'ldap auth failed. falling back to internal auth. Exception: {}'
                .format(e))
            # fallback to internal auth
            if not self.cork.is_authenticate(username, password):
                username = self.find_case_insensitive_username(username)
                if not username or not self.cork.is_authenticate(
                        username, password):
                    return None
            return self.all_users[username]
        finally:
            c.unbind_s()

    def update_password(self, curr_password, password, confirm):
        return NotImplementedError

    def reset_password(self, password, confirm, resetcode):
        return NotImplementedError

    def create_user_as_admin(self, email, username, passwd, passwd2, role,
                             name):
        """Create a new user with command line arguments or series of prompts,
           preforming basic validation
        """
        self.access.assert_is_superuser()

        errs = []

        # EMAIL
        # validate email
        if not re.match(self.EMAIL_RX, email):
            errs.append('valid email required!')

        if email in [data['email_addr'] for u, data in self.all_users.items()]:
            errs.append('A user already exists with {0} email!'.format(email))

        # USERNAME
        # validate username
        if not username:
            errs.append('please specify a username!')

        if not self.is_username_available(username):
            errs.append('Invalid username.')

        # ROLE
        if role not in self.get_roles():
            errs.append('Not a valid role.')

        # PASSWD
        if passwd != passwd2 or not self.PASS_RX.match(passwd):
            errs.append(
                'Passwords must match and be at least 8 characters long '
                'with lowercase, uppercase, and either digits or symbols.')

        if errs:
            return errs, None

        # add user to cork
        #self.cork._store.users[username] = {
        self.all_users[username] = {
            'role': role,
            'hash': self.cork._hash(username, passwd).decode('ascii'),
            'email_addr': email,
            'full_name': name,
            'creation_date': str(datetime.utcnow()),
            'last_login': str(datetime.utcnow()),
        }
        #self.cork._store.save_users()

        return None, self.create_new_user(username, {
            'email': email,
            'name': name
        })

    def update_user_as_admin(self, user, data):
        """ Update any property on specified user
        For admin-only
        """
        self.access.assert_is_curr_user(user)

        errs = []

        if not data:
            errs.append('Nothing To Update')

        if 'role' in data and data['role'] not in self.get_roles():
            errs.append('Not a valid role.')

        if 'max_size' in data and not isinstance(data['max_size'], int):
            errs.append('max_size must be an int')

        if errs:
            return errs

        if 'name' in data:
            #user['desc'] = '{{"name":"{name}"}}'.format(name=data.get('name', ''))
            user['name'] = data.get('name', '')

        if 'desc' in data:
            user['desc'] = data['desc']

        if 'max_size' in data:
            user['max_size'] = data['max_size']

        if 'role' in data:
            user['role'] = data['role']

        if 'customer_id' in data:
            user['customer_id'] = data['customer_id']

        if 'customer_max_size' in data:
            user['customer_max_size'] = data['customer_max_size']

        return None