示例#1
0
    def rpc_database_delete_rows_by_id(self, row_ids):
        """
		Delete multiple rows from a table with the specified values in the id
		column. If a row id specified in *row_ids* does not exist, then it will
		be skipped and no error will be thrown.

		:param list row_ids: The row ids to delete.
		:return: The row ids that were deleted.
		:rtype: list
		"""
        table = DATABASE_TABLE_OBJECTS.get(self.path.split('/')[-3])
        assert table
        deleted_rows = []
        session = db_manager.Session()
        try:
            for row_id in row_ids:
                row = db_manager.get_row_by_id(session, table, row_id)
                if not row:
                    continue
                session.delete(row)
                deleted_rows.append(row_id)
            session.commit()
        finally:
            session.close()
        return deleted_rows
def upgrade():
    op.add_column(
        'campaigns',
        sqlalchemy.Column('credential_regex_username', sqlalchemy.String))
    op.add_column(
        'campaigns',
        sqlalchemy.Column('credential_regex_password', sqlalchemy.String))
    op.add_column(
        'campaigns',
        sqlalchemy.Column('credential_regex_mfa_token', sqlalchemy.String))

    op.add_column('credentials',
                  sqlalchemy.Column('mfa_token', sqlalchemy.String))
    op.add_column('credentials',
                  sqlalchemy.Column('regex_validated', sqlalchemy.Boolean))

    op.add_column('users', sqlalchemy.Column('access_level',
                                             sqlalchemy.Integer))
    op.execute('UPDATE users SET access_level = 1000')
    op.alter_column('users', 'access_level', nullable=False)

    # adjust the schema version metadata
    db_manager.Session.remove()
    db_manager.Session.configure(bind=op.get_bind())
    session = db_manager.Session()
    db_manager.set_metadata('schema_version', 9, session=session)
    session.commit()
示例#3
0
    def rpc_database_set_row_value(self, row_id, keys, values):
        """
		Set values for a row in the specified table with an id of *row_id*.

		:param tuple keys: The column names of *values*.
		:param tuple values: The values to be updated in the row.
		"""
        if not isinstance(keys, (list, tuple)):
            keys = (keys, )
        if not isinstance(values, (list, tuple)):
            values = (values, )
        assert len(keys) == len(values)
        table_name = self.path.split('/')[-2]
        for key, value in zip(keys, values):
            assert key in DATABASE_TABLES[table_name]
        table = DATABASE_TABLE_OBJECTS.get(table_name)
        assert table
        session = db_manager.Session()
        row = db_manager.get_row_by_id(session, table, row_id)
        if not row:
            session.close()
            assert row
        for key, value in zip(keys, values):
            setattr(row, key, value)
        session.commit()
        session.close()
        return
示例#4
0
	def test_query_get_total(self):
		session = db_manager.Session()
		result = graphql_schema.execute(
			"{ db { users { total } } }",
			context_value={'session': session}
		)
		self.assertEquals(result.data['db']['users']['total'], 2)
示例#5
0
    def rpc_database_get_rows(self, *args):
        """
		Retrieve the rows from the specified table where the search
		criteria matches.

		:return: A dictionary with columns and rows keys.
		:rtype: dict
		"""
        args = list(args)
        offset = 0
        fields = self.path.split('/')[1:-2]
        if len(args) == (len(fields) + 1):
            offset = (args.pop() * VIEW_ROW_COUNT)
        assert len(fields) == len(args)
        table_name = self.path.split('/')[-2]
        table = DATABASE_TABLE_OBJECTS.get(table_name)
        assert table

        # it's critical that the columns are in the order that the client is expecting
        columns = DATABASE_TABLES[table_name]
        rows = []
        session = db_manager.Session()
        query = session.query(table)
        query = query.filter_by(**dict(zip((f + '_id' for f in fields), args)))
        for row in query[offset:offset + VIEW_ROW_COUNT]:
            rows.append([getattr(row, c) for c in columns])
        session.close()
        if not len(rows):
            return None
        return {'columns': columns, 'rows': rows}
示例#6
0
    def message_id(self):
        """
		The message id that is associated with the current request's
		visitor. This is retrieved by looking at an 'id' parameter in the
		query and then by checking the
		:py:attr:`~.KingPhisherRequestHandler.visit_id` value in the
		database. If no message id is associated, this value is None. The
		resulting value will be either a confirmed valid value, or the value
		of the configurations server.secret_id for testing purposes.
		"""
        if hasattr(self, '_message_id'):
            return self._message_id
        self._message_id = None
        msg_id = self.get_query('id')
        if msg_id == self.config.get('server.secret_id'):
            self._message_id = msg_id
            return self._message_id
        session = db_manager.Session()
        if msg_id and db_manager.get_row_by_id(session, db_models.Message,
                                               msg_id):
            self._message_id = msg_id
        elif self.visit_id:
            visit = db_manager.get_row_by_id(session, db_models.Visit,
                                             self.visit_id)
            self._message_id = visit.message_id
        session.close()
        return self._message_id
示例#7
0
    def issue_alert(self, alert_text, campaign_id):
        """
		Send an SMS alert. If no *campaign_id* is specified all users
		with registered SMS information will receive the alert otherwise
		only users subscribed to the campaign specified.

		:param str alert_text: The message to send to subscribers.
		:param int campaign_id: The campaign subscribers to send the alert to.
		"""
        session = db_manager.Session()
        campaign = db_manager.get_row_by_id(session, db_models.Campaign,
                                            campaign_id)

        if '{campaign_name}' in alert_text:
            alert_text = alert_text.format(campaign_name=campaign.name)
        for subscription in campaign.alert_subscriptions:
            user = subscription.user
            carrier = user.phone_carrier
            number = user.phone_number
            if carrier is None or number is None:
                self.server.logger.warning(
                    "skipping alert because user {0} has missing information".
                    format(user.id))
                continue
            self.server.logger.debug(
                "sending alert SMS message to {0} ({1})".format(
                    number, carrier))
            sms.send_sms(alert_text, number, carrier,
                         '*****@*****.**')
        session.close()
示例#8
0
    def handle_email_opened(self, query):
        # image size: 43 Bytes
        img_data = '47494638396101000100800100000000ffffff21f90401000001002c00000000'
        img_data += '010001000002024c01003b'
        img_data = binascii.a2b_hex(img_data)
        self.send_response(200)
        self.send_header('Content-Type', 'image/gif')
        self.send_header('Content-Length', str(len(img_data)))
        self.end_headers()
        self.wfile.write(img_data)

        msg_id = self.get_query('id')
        if not msg_id:
            return
        session = db_manager.Session()
        query = session.query(db_models.Message)
        query = query.filter_by(id=msg_id, opened=None)
        message = query.first()
        if message and not message.campaign.has_expired:
            message.opened = db_models.current_timestamp()
            message.opener_ip = self.get_client_ip()
            message.opener_user_agent = self.headers.get('user-agent', None)
            session.commit()
        session.close()
        signals.safe_send('email-opened', self.logger, self)
示例#9
0
    def __init__(self, timeout='30m'):
        """
		:param timeout: The length of time in seconds for which sessions are valid.
		:type timeout: int, str
		"""
        self.logger = logging.getLogger('KingPhisher.Server.SessionManager')
        timeout = smoke_zephyr.utilities.parse_timespan(timeout)
        self.session_timeout = timeout
        self._sessions = {}
        self._lock = threading.Lock()

        # get valid sessions from the database
        expired = 0
        session = db_manager.Session()
        for stored_session in session.query(db_models.AuthenticatedSession):
            if stored_session.last_seen < time.time() - self.session_timeout:
                expired += 1
                continue
            auth_session = AuthenticatedSession.from_db_authenticated_session(
                stored_session)
            self._sessions[stored_session.id] = auth_session
        session.query(db_models.AuthenticatedSession).delete()
        session.commit()
        self.logger.info(
            "restored {0:,} valid sessions and skipped {1:,} expired sessions from the database"
            .format(len(self._sessions), expired))
示例#10
0
def main():
	parser = argparse.ArgumentParser(description='King Phisher Interactive Database Console', conflict_handler='resolve')
	utilities.argp_add_args(parser)
	config_group = parser.add_mutually_exclusive_group(required=True)
	config_group.add_argument('-c', '--config', dest='server_config', type=argparse.FileType('r'), help='the server configuration file')
	config_group.add_argument('-u', '--url', dest='database_url', help='the database connection url')
	arguments = parser.parse_args()

	if arguments.database_url:
		database_connection_url = arguments.database_url
	elif arguments.server_config:
		server_config = yaml.load(arguments.server_config)
		database_connection_url = server_config['server']['database']
	else:
		raise RuntimeError('no database connection was specified')

	engine = manager.init_database(database_connection_url)
	session = manager.Session()
	rpc_session = aaa.AuthenticatedSession(user=getpass.getuser())
	console = code.InteractiveConsole(dict(
		engine=engine,
		graphql=graphql,
		graphql_query=graphql_query,
		manager=manager,
		models=models,
		pprint=pprint.pprint,
		rpc_session=rpc_session,
		session=session
	))
	console.interact('starting interactive database console')

	if os.path.isdir(os.path.dirname(history_file)):
		readline.write_history_file(history_file)
示例#11
0
 def wrapper(handler_instance, *args, **kwargs):
     if log_call and rpc_logger.isEnabledFor(logging.DEBUG):
         args_repr = ', '.join(map(repr, args))
         if kwargs:
             for key, value in sorted(kwargs.items()):
                 args_repr += ", {0}={1!r}".format(key, value)
         msg = "calling RPC method {0}({1})".format(
             function.__name__, args_repr)
         if getattr(handler_instance, 'rpc_session', False):
             msg = handler_instance.rpc_session.user + ' is ' + msg
         rpc_logger.debug(msg)
     signals.rpc_method_call.send(path[1:-1],
                                  request_handler=handler_instance,
                                  args=args,
                                  kwargs=kwargs)
     if database_access:
         session = db_manager.Session()
         try:
             result = function(handler_instance, session, *args,
                               **kwargs)
         finally:
             session.close()
     else:
         result = function(handler_instance, *args, **kwargs)
     signals.rpc_method_called.send(path[1:-1],
                                    request_handler=handler_instance,
                                    args=args,
                                    kwargs=kwargs,
                                    retval=result)
     return result
示例#12
0
    def _respond_file_check_id(self):
        if not self.config.get('server.require_id'):
            return
        if self.message_id == self.config.get('server.secret_id'):
            return
        # a valid campaign_id requires a valid message_id
        if not self.campaign_id:
            self.server.logger.warning(
                'denying request due to lack of a valid id')
            raise errors.KingPhisherAbortRequestError()

        session = db_manager.Session()
        campaign = db_manager.get_row_by_id(session, db_models.Campaign,
                                            self.campaign_id)
        query = session.query(db_models.LandingPage)
        query = query.filter_by(campaign_id=self.campaign_id,
                                hostname=self.vhost)
        if query.count() == 0:
            self.server.logger.warning(
                'denying request with not found due to invalid hostname')
            session.close()
            raise errors.KingPhisherAbortRequestError()
        if campaign.reject_after_credentials and self.visit_id == None:
            query = session.query(db_models.Credential)
            query = query.filter_by(message_id=self.message_id)
            if query.count():
                self.server.logger.warning(
                    'denying request because credentials were already harvested'
                )
                session.close()
                raise errors.KingPhisherAbortRequestError()
        session.close()
        return
示例#13
0
 def wrapper(handler_instance, *args, **kwargs):
     session = db_manager.Session()
     try:
         result = function(handler_instance, session, *args, **kwargs)
     finally:
         session.close()
     return result
示例#14
0
	def _do_http_method(self, *args, **kwargs):
		# This method wraps all of the default do_* HTTP verb handlers to
		# provide error handling and (for non-RPC requests) path adjustments.
		# This also is also a high level location where the throttle semaphore
		# is managed which is acquired for all RPC requests. Non-RPC requests
		# can acquire it as necessary and *should* release it when they are
		# finished with it, however if they fail to do so or encounter an error
		# the semaphore will be released here as a fail safe.
		self.connection.settimeout(smoke_zephyr.utilities.parse_timespan('20s'))  # set a timeout as a fail safe
		self.rpc_session_id = self.headers.get(server_rpc.RPC_AUTH_HEADER, None)
		# delete cached properties so they are handled per request instead of connection.
		for cache_prop in ('_campaign_id', '_message_id', '_visit_id'):
			if hasattr(self, cache_prop):
				delattr(self, cache_prop)
		self.adjust_path()
		self._session = db_manager.Session()

		http_method_handler = None
		try:
			signals.request_handle.send(self)
			http_method_handler = getattr(super(KingPhisherRequestHandler, self), 'do_' + self.command)
			http_method_handler(*args, **kwargs)
		except errors.KingPhisherAbortRequestError as error:
			if http_method_handler is None:
				self.logger.debug('http request aborted by a signal handler')
			else:
				self.logger.info('http request aborted')
			if not error.response_sent:
				self.respond_not_found()
		finally:
			if self.semaphore_acquired:
				self.logger.warning('http request failed to cleanly release resources')
				self.semaphore_release()
			self._session.close()
		self.connection.settimeout(None)
示例#15
0
def main():
	parser = argparse.ArgumentParser(description='King Phisher Interactive Database Console', conflict_handler='resolve')
	parser.add_argument('-v', '--version', action='version', version=parser.prog + ' Version: ' + version.version)
	parser.add_argument('-L', '--log', dest='loglvl', action='store', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], default='CRITICAL', help='set the logging level')
	config_group = parser.add_mutually_exclusive_group(required=True)
	config_group.add_argument('-c', '--config', dest='server_config', type=argparse.FileType('r'), help='the server configuration file')
	config_group.add_argument('-u', '--url', dest='database_url', help='the database connection url')
	arguments = parser.parse_args()

	logging.getLogger('').setLevel(logging.DEBUG)
	console_log_handler = logging.StreamHandler()
	console_log_handler.setLevel(getattr(logging, arguments.loglvl))
	console_log_handler.setFormatter(logging.Formatter("%(levelname)-8s %(message)s"))
	logging.getLogger('').addHandler(console_log_handler)

	if arguments.database_url:
		database_connection_url = arguments.database_url
	elif arguments.server_config:
		server_config = yaml.load(arguments.server_config)
		database_connection_url = server_config['server']['database']
	else:
		raise RuntimeError('no database connection was specified')

	engine = manager.init_database(database_connection_url)
	session = manager.Session()
	console = code.InteractiveConsole(dict(engine=engine, manager=manager, models=models, session=session))
	console.interact('starting interactive database console')
示例#16
0
    def rpc_campaign_message_new(self, campaign_id, email_id, target_email,
                                 company_name, first_name, last_name):
        """
		Record a message that has been sent as part of a campaign. These
		details can be retrieved later for value substitution in template
		pages.

		:param int campaign_id: The ID of the campaign.
		:param str email_id: The message id of the sent email.
		:param str target_email: The email address that the message was sent to.
		:param str company_name: The company name value for the message.
		:param str first_name: The first name of the message's recipient.
		:param str last_name: The last name of the message's recipient.
		"""
        session = db_manager.Session()
        message = db_models.Message()
        message.id = email_id
        message.campaign_id = campaign_id
        message.target_email = target_email
        message.company_name = company_name
        message.first_name = first_name
        message.last_name = last_name
        session.add(message)
        session.commit()
        session.close()
        return
示例#17
0
def downgrade():
	op.drop_table('storage_data')

	db_manager.Session.remove()
	db_manager.Session.configure(bind=op.get_bind())
	session = db_manager.Session()
	db_manager.set_meta_data('schema_version', 6, session=session)
	session.commit()
示例#18
0
def downgrade():
    op.drop_table('authenticated_sessions')

    db_manager.Session.remove()
    db_manager.Session.configure(bind=op.get_bind())
    session = db_manager.Session()
    db_manager.set_meta_data('schema_version', 5, session=session)
    session.commit()
示例#19
0
 def setUp(self):
     username = '******'
     self._session = db_manager.Session()
     self.user = self._session.query(
         db_models.User).filter_by(name=username).first()
     if self.user is None:
         self.user = db_models.User(name=username)
         self._session.add(self.user)
         self._session.commit()
示例#20
0
 def test_query_auth_middleware_no_session(self):
     self._init_db()
     session = db_manager.Session()
     result = graphql_schema.execute(
         "{ db { users { edges { node { id otpSecret } } } } }",
         context_value={'session': session})
     users = result.data['db']['users']['edges']
     self.assertEquals(len(users), 2)
     self.assertEquals(users[0]['node']['otpSecret'], 'secret')
     self.assertEquals(users[1]['node']['otpSecret'], 'secret')
示例#21
0
 def test_query_get_pageinfo(self):
     self._init_db()
     session = db_manager.Session()
     result = graphql_schema.execute(
         "{ db { users(first: 1) { total pageInfo { hasNextPage } } } }",
         context_value={'session': session})
     users = result.data['db']['users']
     self.assertEquals(users['total'], 2)
     self.assertIn('pageInfo', users)
     self.assertTrue(users['pageInfo'].get('hasNextPage', False))
示例#22
0
def downgrade():
    op.drop_column('campaigns', 'description')
    op.drop_column('messages', 'opener_ip')
    op.drop_column('messages', 'opener_user_agent')

    db_manager.Session.remove()
    db_manager.Session.configure(bind=op.get_bind())
    session = db_manager.Session()
    db_manager.set_meta_data('schema_version', 3, session=session)
    session.commit()
示例#23
0
    def get_template_vars_client(self):
        """
		Build a dictionary of variables for a client with an associated
		campaign.

		:return: The client specific template variables.
		:rtype: dict
		"""
        client_vars = {'address': self.get_client_ip()}
        if not self.message_id:
            return client_vars
        visit_count = 0
        result = None
        session = db_manager.Session()
        if self.message_id == self.config.get('server.secret_id'):
            client_vars['company_name'] = 'Wonderland Inc.'
            client_vars['company'] = {'name': 'Wonderland Inc.'}
            result = ('*****@*****.**', 'Alice', 'Liddle', 0)
        elif self.message_id:
            message = db_manager.get_row_by_id(session, db_models.Message,
                                               self.message_id)
            if message:
                if message.campaign.company:
                    client_vars['company_name'] = message.campaign.company.name
                    client_vars['company'] = {
                        'name':
                        message.campaign.company.name,
                        'url_email':
                        message.campaign.company.url_email,
                        'url_main':
                        message.campaign.company.url_main,
                        'url_remote_access':
                        message.campaign.company.url_remote_access
                    }
                result = (message.target_email, message.first_name,
                          message.last_name, message.trained)
        if not result:
            session.close()
            return client_vars
        client_vars['email_address'] = result[0]
        client_vars['first_name'] = result[1]
        client_vars['last_name'] = result[2]
        client_vars['is_trained'] = result[3]
        client_vars['message_id'] = self.message_id

        if self.visit_id:
            visit = db_manager.get_row_by_id(session, db_models.Visit,
                                             self.visit_id)
            client_vars['visit_id'] = visit.id
            visit_count = visit.visit_count
        # increment the count preemptively
        client_vars['visit_count'] = visit_count + 1

        session.close()
        return client_vars
示例#24
0
def main():
	parser = argparse.ArgumentParser(description='King Phisher TOTP Enrollment Utility', conflict_handler='resolve')
	utilities.argp_add_args(parser)
	config_group = parser.add_mutually_exclusive_group(required=True)
	config_group.add_argument('-c', '--config', dest='server_config', type=argparse.FileType('r'), help='the server configuration file')
	config_group.add_argument('-u', '--url', dest='database_url', help='the database connection url')
	parser.add_argument('--otp', dest='otp_secret', help='a specific otp secret')
	parser.add_argument('user', help='the user to mange')
	parser.add_argument('action', choices=('remove', 'set', 'show'), help='the action to preform')
	parser.epilog = PARSER_EPILOG
	arguments = parser.parse_args()

	utilities.configure_stream_logger(arguments.loglvl, arguments.logger)

	if arguments.database_url:
		database_connection_url = arguments.database_url
	elif arguments.server_config:
		server_config = yaml.load(arguments.server_config)
		database_connection_url = server_config['server']['database']
	else:
		raise RuntimeError('no database connection was specified')

	manager.init_database(database_connection_url)
	session = manager.Session()
	user = session.query(models.User).filter_by(id=arguments.user).first()
	if not user:
		color.print_error("invalid user id: {0}".format(arguments.user))
		return

	for case in utilities.switch(arguments.action):
		if case('remove'):
			user.otp_secret = None
			break
		if case('set'):
			if user.otp_secret:
				color.print_error("the specified user already has an otp secret set")
				return
			if arguments.otp_secret:
				new_otp = arguments.otp_secret
			else:
				new_otp = pyotp.random_base32()
			if len(new_otp) != 16:
				color.print_error("invalid otp secret length, must be 16")
				return
			user.otp_secret = new_otp
			break

	if user.otp_secret:
		color.print_status("user: {0} otp: {1}".format(user.id, user.otp_secret))
		totp = pyotp.TOTP(user.otp_secret)
		uri = totp.provisioning_uri(user.id + '@king-phisher') + '&issuer=King%20Phisher'
		color.print_status("provisioning uri: {0}".format(uri))
	else:
		color.print_status("user: {0} otp: N/A".format(user.id))
	session.commit()
示例#25
0
	def rpc_test_login(self, username, password, otp=None):
		session = db_manager.Session()
		user = session.query(db_models.User).filter_by(name=username).first()
		if not user:
			user = db_models.User(name=username)
		user.last_login = db_models.current_timestamp()
		session.add(user)
		session.commit()
		session_id = self.server.session_manager.put(user)
		session.close()
		return True, constants.ConnectionErrorReason.SUCCESS, session_id
示例#26
0
	def _init_db(self):
		try:
			db_manager.init_database('sqlite://')
		except Exception as error:
			self.fail("failed to initialize the database (error: {0})".format(error.__class__.__name__))
		alice = db_models.User(id='alice', otp_secret='secret')
		calie = db_models.User(id='calie', otp_secret='secret')
		session = db_manager.Session()
		session.add(alice)
		session.add(calie)
		session.commit()
		session.close()
示例#27
0
    def _respond_file_check_id(self):
        if re.match(r'^/\.well-known/acme-challenge/[a-zA-Z0-9\-_]{40,50}$',
                    self.request_path):
            self.server.logger.info(
                'received request for .well-known/acme-challenge')
            return
        if not self.config.get('server.require_id'):
            return

        self.semaphore_acquire()
        if self.message_id == self.config.get('server.secret_id'):
            self.semaphore_release()
            self.server.logger.debug(
                'request received with the correct secret id')
            return
        # a valid campaign_id requires a valid message_id
        if not self.campaign_id:
            self.semaphore_release()
            self.server.logger.warning(
                'denying request due to lack of a valid id')
            raise errors.KingPhisherAbortRequestError()

        session = db_manager.Session()
        campaign = db_manager.get_row_by_id(session, db_models.Campaign,
                                            self.campaign_id)
        query = session.query(db_models.LandingPage)
        query = query.filter_by(campaign_id=self.campaign_id,
                                hostname=self.vhost)
        if query.count() == 0:
            session.close()
            self.semaphore_release()
            self.server.logger.warning(
                'denying request with not found due to invalid hostname')
            raise errors.KingPhisherAbortRequestError()
        if campaign.has_expired:
            session.close()
            self.semaphore_release()
            self.server.logger.warning(
                'denying request because the campaign has expired')
            raise errors.KingPhisherAbortRequestError()
        if campaign.reject_after_credentials and self.visit_id is None:
            query = session.query(db_models.Credential)
            query = query.filter_by(message_id=self.message_id)
            if query.count():
                session.close()
                self.semaphore_release()
                self.server.logger.warning(
                    'denying request because credentials were already harvested'
                )
                raise errors.KingPhisherAbortRequestError()
        session.close()
        self.semaphore_release()
        return
示例#28
0
def upgrade():
    op.add_column('campaigns',
                  sqlalchemy.Column('description', sqlalchemy.String))
    op.add_column('messages', sqlalchemy.Column('opener_ip',
                                                sqlalchemy.String))
    op.add_column('messages',
                  sqlalchemy.Column('opener_user_agent', sqlalchemy.String))

    db_manager.Session.remove()
    db_manager.Session.configure(bind=op.get_bind())
    session = db_manager.Session()
    db_manager.set_meta_data('schema_version', 4, session=session)
    session.commit()
示例#29
0
	def rpc_database_delete_row_by_id(self, row_id):
		"""
		Delete a row from a table with the specified value in the id column.

		:param row_id: The id value.
		"""
		table = DATABASE_TABLE_OBJECTS.get(self.path.split('/')[-2])
		assert(table)
		session = db_manager.Session()
		session.delete(db_manager.get_row_by_id(session, table, row_id))
		session.commit()
		session.close()
		return
示例#30
0
	def test_query_auth_middleware_session(self):
		self._init_db()
		session = db_manager.Session()
		rpc_session = aaa.AuthenticatedSession('alice')
		result = graphql.schema.execute(
			"{ db { users { edges { node { id, otpSecret } } } } }",
			context_value={'rpc_session': rpc_session, 'session': session}
		)
		users = result.data['db']['users']['edges']
		self.assertEquals(len(users), 2)
		self.assertEquals(users[0]['node']['id'], 'alice')
		self.assertEquals(users[0]['node']['otpSecret'], 'secret')
		self.assertIsNone(users[1]['node']['otpSecret'])