Source code for synapse.lib.httpapi

import json
import base64
import asyncio
import logging

from urllib.parse import urlparse

import tornado.web as t_web
import tornado.websocket as t_websocket

import synapse.exc as s_exc
import synapse.common as s_common

import synapse.lib.base as s_base
import synapse.lib.msgpack as s_msgpack

logger = logging.getLogger(__name__)

[docs] class Sess(s_base.Base): async def __anit__(self, cell, iden, info): await s_base.Base.__anit__(self) self.user = None self.socks = set() self.cell = cell # for transient session info self.locl = {} # for persistent session info self.iden = iden self.info = info user = self.info.get('user') if user is not None: self.user = self.cell.auth.user(user)
[docs] async def set(self, name, valu): await self.cell.setHttpSessInfo(self.iden, name, valu) self.info[name] = valu
[docs] async def update(self, vals: dict): await self.cell.updateHttpSessInfo(self.iden, vals) for name, valu in vals.items(): self.info[name] = valu
[docs] async def login(self, user): self.user = user await self.set('user', user.iden) await self.fire('sess:login')
[docs] async def logout(self): self.user = None await self.set('user', None) await self.fire('sess:logout')
[docs] def addWebSock(self, sock): self.socks.add(sock)
[docs] def delWebSock(self, sock): self.socks.discard(sock)
[docs] class HandlerBase:
[docs] def initialize(self, cell): self.cell = cell self._web_sess = None self._web_user = None # Deprecated for new handlers self.web_useriden = None # The user iden at the time of authentication. self.web_username = None # The user name at the time of authentication. # this can't live in set_default_headers() due to call ordering in tornado headers = self.getCustomHeaders() if headers is not None: for name, valu in headers.items(): self.add_header(name, valu)
[docs] def getCustomHeaders(self): return self.cell.conf.get('https:headers')
[docs] def set_default_headers(self): self.clear_header('Server') self.add_header('X-Content-Type-Options', 'nosniff') origin = self.request.headers.get('origin') if origin is not None and self.isOrigHost(origin): self.add_header('Access-Control-Allow-Origin', origin) self.add_header('Access-Control-Allow-Credentials', 'true') self.add_header('Access-Control-Allow-Headers', 'Content-Type')
[docs] def getAuthCell(self): ''' Return a reference to the cell used for auth operations. ''' return self.cell
[docs] def options(self): self.set_status(204) self.finish()
[docs] def isOrigHost(self, origin): host = urlparse(origin).hostname hosttag = self.request.headers.get('host') if ':' in hosttag: hosttag, hostport = hosttag.split(':', 1) return host == hosttag
[docs] def check_origin(self, origin): return self.isOrigHost(origin)
[docs] def getJsonBody(self, validator=None): return self.loadJsonMesg(self.request.body, validator=validator)
[docs] def sendRestErr(self, code, mesg): self.set_header('Content-Type', 'application/json') return self.write({'status': 'err', 'code': code, 'mesg': mesg})
[docs] def sendRestExc(self, e): self.set_header('Content-Type', 'application/json') return self.sendRestErr(e.__class__.__name__, str(e))
[docs] def sendRestRetn(self, valu): self.set_header('Content-Type', 'application/json') return self.write({'status': 'ok', 'result': valu})
[docs] def loadJsonMesg(self, byts, validator=None): try: item = json.loads(byts) if validator is not None: validator(item) return item except s_exc.SchemaViolation as e: self.sendRestErr('SchemaViolation', str(e)) return None except Exception: self.sendRestErr('SchemaViolation', 'Invalid JSON content.') return None
[docs] def logAuthIssue(self, mesg=None, user=None, username=None, level=logging.WARNING): ''' Helper to log issues related to request authentication. Args: mesg (str): Additional message to log. user (str): User iden, if available. username (str): Username, if available. level (int): Logging level to log the message at. Defaults to logging.WARNING. Returns: None ''' uri = self.request.uri remote_ip = self.request.remote_ip enfo = {'uri': uri, 'remoteip': remote_ip, } errm = f'Failed to authenticate request to {uri} from {remote_ip} ' if mesg: errm = f'{errm}: {mesg}' if user: errm = f'{errm}: user={user}' enfo['user'] = user if username: errm = f'{errm} ({username})' enfo['username'] = username logger.log(level, msg=errm, extra={'synapse': enfo})
[docs] def sendAuthRequired(self): self.set_header('WWW-Authenticate', 'Basic realm=synapse') self.set_status(401) self.sendRestErr('NotAuthenticated', 'The session is not logged in.')
[docs] async def reqAuthUser(self): if await self.authenticated(): return True self.sendAuthRequired() return False
[docs] async def isUserAdmin(self): ''' Check if the current authenticated user is an admin or not. Returns: bool: True if the user is an admin, false otherwise. ''' iden = await self.useriden() if iden is None: return False authcell = self.getAuthCell() udef = await authcell.getUserDef(iden, packroles=False) if not udef.get('admin'): return False return True
[docs] async def reqAuthAdmin(self): ''' Require the current authenticated user to be an admin. Notes: If this returns False, an error message has already been sent and no additional processing for the request should be done. Returns: bool: True if the user is an admin, false otherwise. ''' iden = await self.useriden() if iden is None: self.sendAuthRequired() return False authcell = self.getAuthCell() udef = await authcell.getUserDef(iden, packroles=False) if not udef.get('admin'): self.sendRestErr('AuthDeny', f'User {self.web_useriden} ({self.web_username}) is not an admin.') return False return True
[docs] async def sess(self, gen=True): ''' Get the heavy Session object for the request. Args: gen (bool): If set to True, generate a new session if there is no sess cookie. Notes: This stores the identifier in the ``sess`` cookie for with a 14 day expiration, stored in the Cell. Valid requests with that ``sess`` cookie will resolve to the same Session object. Returns: Sess: A heavy session object. If the sess cookie is invalid or gen is false, this returns None. ''' if self._web_sess is None: iden = self.get_secure_cookie('sess', max_age_days=14) if iden is None: if gen: iden = s_common.guid().encode() opts = {'expires_days': 14, 'secure': True, 'httponly': True} self.set_secure_cookie('sess', iden, **opts) else: return None self._web_sess = await self.cell.genHttpSess(iden) return self._web_sess
[docs] async def useriden(self): ''' Get the user iden of the current session user. Note: This function will pull the iden from the current session, or attempt to resolve the useriden with basic authentication. Returns: str: The iden of the current session user. ''' if self.web_useriden is not None: return self.web_useriden sess = await self.sess(gen=False) if sess is not None: iden = sess.info.get('user') name = sess.info.get('username', '<no username>') self.web_useriden = iden self.web_username = name return iden # Check for API Keys key = self.request.headers.get('X-API-KEY') if key is not None: return await self.handleApiKeyAuth() return await self.handleBasicAuth()
[docs] async def handleBasicAuth(self): ''' Handle basic authentication in the handler. Notes: Implementors may override this to disable or implement their own basic auth schemes. This is expected to set web_useriden and web_username upon successful authentication. Returns: str: The user iden of the logged in user. ''' authcell = self.getAuthCell() auth = self.request.headers.get('Authorization') if auth is None: return None if not auth.startswith('Basic '): return None _, blob = auth.split(None, 1) try: text = base64.b64decode(blob).decode('utf8') name, passwd = text.split(':', 1) except Exception: logger.exception('invalid basic auth header') return None udef = await authcell.getUserDefByName(name) if udef is None: self.logAuthIssue(mesg='No such user.', username=name) return None if udef.get('locked'): self.logAuthIssue(mesg='User is locked.', user=udef.get('iden'), username=name) return None if not await authcell.tryUserPasswd(name, passwd): self.logAuthIssue(mesg='Incorrect password.', user=udef.get('iden'), username=name) return None self.web_useriden = udef.get('iden') self.web_username = udef.get('name') return self.web_useriden
[docs] async def handleApiKeyAuth(self): authcell = self.getAuthCell() key = self.request.headers.get('X-API-KEY') isok, info = await authcell.checkUserApiKey(key) # errfo or dict with tdef + udef if isok is False: self.logAuthIssue(mesg=info.get('mesg'), user=info.get('user'), username=info.get('name')) return udef = info.get('udef') self.web_useriden = udef.get('iden') self.web_username = udef.get('name') return self.web_useriden
[docs] async def allowed(self, perm, default=False, gateiden=None): ''' Check if the authenticated user has the given permission. Args: perm (tuple): The permission tuple to check. default (boolean): The default value for the permission. gateiden (str): The gateiden to check the permission against. Notes: This API sets up HTTP response values if it returns False. Returns: bool: True if the user has the requested permission. ''' authcell = self.getAuthCell() useriden = await self.useriden() if useriden is None: self.sendAuthRequired() return False if await authcell.isUserAllowed(useriden, perm, gateiden=gateiden, default=default): return True self.set_status(403) mesg = f'User ({self.web_username}) must have permission {".".join(perm)}' if default: mesg = f'User ({self.web_username}) is denied the permission {".".join(perm)}' if gateiden: mesg = f'{mesg} on object {gateiden}' self.sendRestErr('AuthDeny', mesg) return False
[docs] async def authenticated(self): ''' Check if the request has an authenticated user or not. Returns: bool: True if the request has an authenticated user, false otherwise. ''' return await self.useriden() is not None
[docs] async def getUseridenBody(self, validator=None): ''' Helper function to confirm that there is an auth user and a valid JSON body in the request. Args: validator: Validator function run on the deserialized JSON body. Returns: (str, object): The user definition and body of the request as deserialized JSON, or a tuple of s_common.novalu objects if there was no user or json body. ''' if not await self.reqAuthUser(): return (s_common.novalu, s_common.novalu) body = self.getJsonBody(validator=validator) if body is None: return (s_common.novalu, s_common.novalu) useriden = await self.useriden() return (useriden, body)
[docs] class WebSocket(HandlerBase, t_websocket.WebSocketHandler):
[docs] async def xmit(self, name, **info): await self.write_message(json.dumps({'type': name, 'data': info}))
async def _reqUserAllow(self, perm): iden = await self.useriden() if iden is None: mesg = 'Session is not authenticated.' raise s_exc.AuthDeny(mesg=mesg, perm=perm) authcell = self.getAuthCell() if not await authcell.isUserAllowed(iden, perm): ptxt = '.'.join(perm) mesg = f'Permission denied: {ptxt}.' raise s_exc.AuthDeny(mesg=mesg, perm=perm)
[docs] class Handler(HandlerBase, t_web.RequestHandler):
[docs] def prepare(self): self.task = asyncio.current_task()
[docs] def on_connection_close(self): if hasattr(self, 'task'): self.task.cancel()
async def _reqValidOpts(self, opts): if opts is None: opts = {} useriden = await self.useriden() opts.setdefault('user', useriden) if opts.get('user') != useriden: if not await self.allowed(('impersonate',)): return None return opts
[docs] class RobotHandler(HandlerBase, t_web.RequestHandler):
[docs] async def get(self): self.write('User-agent: *\n') self.write('Disallow: /\n')
[docs] @t_web.stream_request_body class StreamHandler(Handler): ''' Subclass for Tornado streaming uploads. Notes: - Async method prepare() is called after headers are read but before body processing. - Sync method on_finish() can be used to cleanup after a request. - Sync method on_connection_close() can be used to cleanup after a client disconnect. - Async methods post(), put(), etc are called after the streaming has completed. '''
[docs] async def data_received(self, chunk): raise s_exc.NoSuchImpl(mesg='data_received must be implemented by subclasses.', name='data_received')
[docs] class StormHandler(Handler):
[docs] def getCore(self): # add an abstraction to allow subclasses to dictate how # a reference to the cortex is returned from the handler. return self.cell
[docs] class StormNodesV1(StormHandler):
[docs] async def post(self): return await self.get()
[docs] async def get(self): user, body = await self.getUseridenBody() if body is s_common.novalu: return s_common.deprecated('HTTP API /api/v1/storm/nodes', curv='2.110.0') # dont allow a user to be specified opts = body.get('opts') query = body.get('query') stream = body.get('stream') jsonlines = stream == 'jsonlines' opts = await self._reqValidOpts(opts) if opts is None: return view = self.cell._viewFromOpts(opts) taskinfo = {'query': query, 'view': view.iden} await self.cell.boss.promote('storm', user=user, info=taskinfo) async for pode in view.iterStormPodes(query, opts=opts): self.write(json.dumps(pode)) if jsonlines: self.write("\n") await self.flush()
[docs] class StormV1(StormHandler):
[docs] async def post(self): return await self.get()
[docs] async def get(self): if not await self.reqAuthUser(): return body = self.getJsonBody() if body is None: return opts = body.get('opts') query = body.get('query') stream = body.get('stream') jsonlines = stream == 'jsonlines' # Maintain backwards compatibility with 0.1.x output opts = await self._reqValidOpts(opts) if opts is None: return opts.setdefault('editformat', 'nodeedits') async for mesg in self.getCore().storm(query, opts=opts): self.write(json.dumps(mesg)) if jsonlines: self.write("\n") await self.flush()
[docs] class StormCallV1(StormHandler):
[docs] async def post(self): return await self.get()
[docs] async def get(self): if not await self.reqAuthUser(): return body = self.getJsonBody() if body is None: return opts = body.get('opts') query = body.get('query') opts = await self._reqValidOpts(opts) if opts is None: return try: ret = await self.getCore().callStorm(query, opts=opts) except s_exc.SynErr as e: mesg = e.get('mesg', str(e)) return self.sendRestErr(e.__class__.__name__, mesg) except asyncio.CancelledError: # pragma: no cover raise except Exception as e: mesg = str(e) return self.sendRestErr(e.__class__.__name__, mesg) else: return self.sendRestRetn(ret)
[docs] class StormExportV1(StormHandler):
[docs] async def post(self): return await self.get()
[docs] async def get(self): if not await self.reqAuthUser(): return body = self.getJsonBody() if body is None: return opts = body.get('opts') query = body.get('query') opts = await self._reqValidOpts(opts) if opts is None: return try: self.set_header('Content-Type', 'application/x-synapse-nodes') async for pode in self.getCore().exportStorm(query, opts=opts): self.write(s_msgpack.en(pode)) await self.flush() except Exception as e: return self.sendRestExc(e)
[docs] class ReqValidStormV1(StormHandler):
[docs] async def post(self): return await self.get()
[docs] async def get(self): _, body = await self.getUseridenBody() if body is s_common.novalu: return opts = body.get('opts', {}) query = body.get('query') try: valid = await self.cell.reqValidStorm(query, opts) except s_exc.SynErr as e: mesg = e.get('mesg', str(e)) return self.sendRestErr(e.__class__.__name__, mesg) else: return self.sendRestRetn(valid)
[docs] class BeholdSockV1(WebSocket):
[docs] async def onInitMessage(self, byts): try: mesg = json.loads(byts) if mesg.get('type') != 'call:init': raise s_exc.BadMesgFormat('Invalid initial message') admin = await self.isUserAdmin() if not admin: await self.xmit('errx', code='AuthDeny', mesg='Beholder API requires admin privs') return async with self.cell.beholder() as beholder: await self.xmit('init') async for mesg in beholder: await self.xmit('iter', **mesg) await self.xmit('fini') except s_exc.SynErr as e: text = e.get('mesg', str(e)) await self.xmit('errx', code=e.__class__.__name__, mesg=text) except asyncio.CancelledError: # pragma: no cover TODO: remove once >= py 3.8 only raise except Exception as e: await self.xmit('errx', code=e.__class__.__name__, mesg=str(e))
[docs] async def on_message(self, byts): self.cell.schedCoro(self.onInitMessage(byts))
[docs] class LoginV1(Handler):
[docs] async def post(self): body = self.getJsonBody() if body is None: return name = body.get('user') passwd = body.get('passwd') authcell = self.getAuthCell() udef = await authcell.getUserDefByName(name) if udef is None: self.logAuthIssue(mesg='No such user.', username=name) return self.sendRestErr('AuthDeny', 'No such user.') if udef.get('locked'): self.logAuthIssue(mesg='User is locked.', user=udef.get('iden'), username=name) return self.sendRestErr('AuthDeny', 'User is locked.') if not await authcell.tryUserPasswd(name, passwd): self.logAuthIssue(mesg='Incorrect password.', user=udef.get('iden'), username=name) return self.sendRestErr('AuthDeny', 'Incorrect password.') iden = udef.get('iden') sess = await self.sess() await sess.set('user', iden) await sess.set('username', name) await sess.fire('sess:login') self.web_useriden = iden self.web_username = name return self.sendRestRetn(await authcell.getUserDef(iden))
[docs] class LogoutV1(Handler):
[docs] async def get(self): sess = await self.sess(gen=False) if sess is not None: self.web_useriden = sess.info.get('user') self.web_username = sess.info.get('username', '<no username>') await self.getAuthCell().delHttpSess(sess.iden) self.clear_cookie('sess') self.sendRestRetn(True)
[docs] class AuthUsersV1(Handler):
[docs] async def get(self): if not await self.reqAuthUser(): return try: archived = int(self.get_argument('archived', default='0')) if archived not in (0, 1): return self.sendRestErr('BadHttpParam', 'The parameter "archived" must be 0 or 1 if specified.') except Exception: return self.sendRestErr('BadHttpParam', 'The parameter "archived" must be 0 or 1 if specified.') users = await self.getAuthCell().getUserDefs() if not archived: users = [udef for udef in users if not udef.get('archived')] self.sendRestRetn(users) return
[docs] class AuthRolesV1(Handler):
[docs] async def get(self): if not await self.reqAuthUser(): return self.sendRestRetn(await self.getAuthCell().getRoleDefs())
[docs] class AuthUserV1(Handler):
[docs] async def get(self, iden): if not await self.reqAuthUser(): return udef = await self.getAuthCell().getUserDef(iden, packroles=False) if udef is None: self.sendRestErr('NoSuchUser', f'User {iden} does not exist.') return self.sendRestRetn(udef)
[docs] async def post(self, iden): # TODO allow user to change their own name / email via this API if not await self.reqAuthAdmin(): return authcell = self.getAuthCell() udef = await authcell.getUserDef(iden) if udef is None: self.sendRestErr('NoSuchUser', f'User {iden} does not exist.') return body = self.getJsonBody() if body is None: return name = body.get('name') if name is not None: await authcell.setUserName(iden, name=name) email = body.get('email') if email is not None: await authcell.setUserEmail(iden, email) locked = body.get('locked') if locked is not None: await authcell.setUserLocked(iden, bool(locked)) rules = body.get('rules') if rules is not None: await authcell.setUserRules(iden, rules, gateiden=None) admin = body.get('admin') if admin is not None: await authcell.setUserAdmin(iden, bool(admin), gateiden=None) archived = body.get('archived') if archived is not None: await authcell.setUserArchived(iden, bool(archived)) self.sendRestRetn(await authcell.getUserDef(iden, packroles=False))
[docs] class AuthUserPasswdV1(Handler):
[docs] async def post(self, iden): current_user, body = await self.getUseridenBody() if body is s_common.novalu: return authcell = self.getAuthCell() udef = await authcell.getUserDef(iden) if udef is None: self.sendRestErr('NoSuchUser', f'User does not exist: {iden}') return password = body.get('passwd') cdef = await authcell.getUserDef(current_user) if cdef.get('admin') or cdef.get('iden') == udef.get('iden'): try: await authcell.setUserPasswd(iden, password) except s_exc.BadArg as e: self.sendRestErr('BadArg', e.get('mesg')) return self.sendRestRetn(await authcell.getUserDef(iden, packroles=False))
[docs] class AuthRoleV1(Handler):
[docs] async def get(self, iden): if not await self.reqAuthUser(): return rdef = await self.getAuthCell().getRoleDef(iden) if rdef is None: self.sendRestErr('NoSuchRole', f'Role {iden} does not exist.') return self.sendRestRetn(rdef)
[docs] async def post(self, iden): if not await self.reqAuthAdmin(): return authcell = self.getAuthCell() rdef = await authcell.getRoleDef(iden) if rdef is None: self.sendRestErr('NoSuchRole', f'Role {iden} does not exist.') return body = self.getJsonBody() if body is None: return rules = body.get('rules') if rules is not None: await authcell.setRoleRules(iden, rules, gateiden=None) self.sendRestRetn(await authcell.getRoleDef(iden))
[docs] class AuthGrantV1(Handler): ''' /api/v1/auth/grant?user=iden&role=iden '''
[docs] async def post(self): return await self.get()
[docs] async def get(self): if not await self.reqAuthAdmin(): return body = self.getJsonBody() if body is None: return useriden = body.get('user') authcell = self.getAuthCell() udef = await authcell.getUserDef(useriden) if udef is None: self.sendRestErr('NoSuchUser', f'User iden {useriden} not found.') return roleiden = body.get('role') rdef = await authcell.getRoleDef(roleiden) if rdef is None: self.sendRestErr('NoSuchRole', f'Role iden {roleiden} not found.') return await authcell.addUserRole(useriden, roleiden) self.sendRestRetn(await authcell.getUserDef(useriden, packroles=False)) return
[docs] class AuthRevokeV1(Handler): ''' /api/v1/auth/grant?user=iden&role=iden '''
[docs] async def post(self): return await self.get()
[docs] async def get(self): if not await self.reqAuthAdmin(): return body = self.getJsonBody() if body is None: return useriden = body.get('user') authcell = self.getAuthCell() udef = await authcell.getUserDef(useriden) if udef is None: self.sendRestErr('NoSuchUser', f'User iden {useriden} not found.') return roleiden = body.get('role') rdef = await authcell.getRoleDef(roleiden) if rdef is None: self.sendRestErr('NoSuchRole', f'Role iden {roleiden} not found.') return await authcell.delUserRole(useriden, roleiden) self.sendRestRetn(await authcell.getUserDef(useriden, packroles=False)) return
[docs] class AuthAddUserV1(Handler):
[docs] async def post(self): if not await self.reqAuthAdmin(): return body = self.getJsonBody() if body is None: return name = body.get('name') if name is None: self.sendRestErr('MissingField', 'The adduser API requires a "name" argument.') return authcell = self.getAuthCell() if await authcell.getUserDefByName(name) is not None: self.sendRestErr('DupUser', f'A user named {name} already exists.') return udef = await authcell.addUser(name=name) iden = udef.get('iden') passwd = body.get('passwd', None) if passwd is not None: await authcell.setUserPasswd(iden, passwd) admin = body.get('admin', None) if admin is not None: await authcell.setUserAdmin(iden, bool(admin)) email = body.get('email', None) if email is not None: await authcell.setUserEmail(iden, email) rules = body.get('rules') if rules is not None: await authcell.setUserRules(iden, rules, gateiden=None) udef = await authcell.getUserDef(iden, packroles=False) self.sendRestRetn(udef) return
[docs] class AuthAddRoleV1(Handler):
[docs] async def post(self): if not await self.reqAuthAdmin(): return body = self.getJsonBody() if body is None: return name = body.get('name') if name is None: self.sendRestErr('MissingField', 'The addrole API requires a "name" argument.') return authcell = self.getAuthCell() if await authcell.getRoleDefByName(name) is not None: self.sendRestErr('DupRole', f'A role named {name} already exists.') return rdef = await authcell.addRole(name) iden = rdef.get('iden') rules = body.get('rules', None) if rules is not None: await authcell.setRoleRules(iden, rules, gateiden=None) self.sendRestRetn(await authcell.getRoleDef(iden)) return
[docs] class AuthDelRoleV1(Handler):
[docs] async def post(self): if not await self.reqAuthAdmin(): return body = self.getJsonBody() if body is None: return name = body.get('name') if name is None: self.sendRestErr('MissingField', 'The delrole API requires a "name" argument.') return authcell = self.getAuthCell() rdef = await authcell.getRoleDefByName(name) if rdef is None: return self.sendRestErr('NoSuchRole', f'The role {name} does not exist!') await authcell.delRole(rdef.get('iden')) self.sendRestRetn(None) return
[docs] class ModelNormV1(Handler):
[docs] async def post(self): return await self.get()
[docs] async def get(self): if not await self.reqAuthUser(): return body = self.getJsonBody() if body is None: return propname = body.get('prop') propvalu = body.get('value') typeopts = body.get('typeopts') if propname is None: self.sendRestErr('MissingField', 'The property normalization API requires a prop name.') return try: valu, info = await self.cell.getPropNorm(propname, propvalu, typeopts=typeopts) except s_exc.NoSuchProp: return self.sendRestErr('NoSuchProp', 'The property {propname} does not exist.') except Exception as e: return self.sendRestExc(e) else: self.sendRestRetn({'norm': valu, 'info': info})
[docs] class ModelV1(Handler):
[docs] async def get(self): if not await self.reqAuthUser(): return resp = await self.cell.getModelDict() return self.sendRestRetn(resp)
[docs] class HealthCheckV1(Handler):
[docs] async def get(self): if not await self.allowed(('health', )): return resp = await self.cell.getHealthCheck() return self.sendRestRetn(resp)
[docs] class ActiveV1(Handler):
[docs] async def get(self): resp = {'active': self.cell.isactive} return self.sendRestRetn(resp)
[docs] class StormVarsGetV1(Handler):
[docs] async def get(self): body = self.getJsonBody() if body is None: return varname = str(body.get('name')) defvalu = body.get('default') if not await self.allowed(('globals', 'get', varname)): return valu = await self.cell.getStormVar(varname, default=defvalu) return self.sendRestRetn(valu)
[docs] class StormVarsPopV1(Handler):
[docs] async def post(self): body = self.getJsonBody() if body is None: return varname = str(body.get('name')) defvalu = body.get('default') if not await self.allowed(('globals', 'pop', varname)): return valu = await self.cell.popStormVar(varname, default=defvalu) return self.sendRestRetn(valu)
[docs] class StormVarsSetV1(Handler):
[docs] async def post(self): body = self.getJsonBody() if body is None: return varname = str(body.get('name')) varvalu = body.get('value', s_common.novalu) if varvalu is s_common.novalu: return self.sendRestErr('BadArg', 'The "value" field is required.') if not await self.allowed(('globals', 'set', varname)): return await self.cell.setStormVar(varname, varvalu) return self.sendRestRetn(True)
[docs] class OnePassIssueV1(Handler): ''' /api/v1/auth/onepass/issue '''
[docs] async def post(self): if not await self.reqAuthAdmin(): return body = self.getJsonBody() if body is None: return iden = body.get('user') duration = body.get('duration', 600000) authcell = self.getAuthCell() try: passwd = await authcell.genUserOnepass(iden, duration) except s_exc.NoSuchUser: return self.sendRestErr('NoSuchUser', 'The user iden does not exist.') return self.sendRestRetn(passwd)
[docs] class FeedV1(Handler): ''' /api/v1/feed Examples: Example data:: { 'name': 'syn.nodes', 'view': null, 'items': [...], } '''
[docs] async def post(self): # Note: This API handler is intended to be used on a heavy Cortex object. if not await self.reqAuthUser(): return body = self.getJsonBody() if body is None: return items = body.get('items') name = body.get('name', 'syn.nodes') func = self.cell.getFeedFunc(name) if func is None: return self.sendRestErr('NoSuchFunc', f'The feed type {name} does not exist.') user = self.cell.auth.user(self.web_useriden) view = self.cell.getView(body.get('view'), user) if view is None: return self.sendRestErr('NoSuchView', 'The specified view does not exist.') wlyr = view.layers[0] perm = ('feed:data', *name.split('.')) if not user.allowed(perm, gateiden=wlyr.iden): permtext = '.'.join(perm) mesg = f'User does not have {permtext} permission on gate: {wlyr.iden}.' return self.sendRestErr('AuthDeny', mesg) try: info = {'name': name, 'view': view.iden, 'nitems': len(items)} await self.cell.boss.promote('feeddata', user=user, info=info) async with await self.cell.snap(user=user, view=view) as snap: snap.strict = False await snap.addFeedData(name, items) return self.sendRestRetn(None) except Exception as e: # pragma: no cover return self.sendRestExc(e)
[docs] class CoreInfoV1(Handler): ''' /api/v1/core/info '''
[docs] async def get(self): if not await self.reqAuthUser(): return resp = await self.cell.getCoreInfoV2() return self.sendRestRetn(resp)
[docs] class ExtApiHandler(StormHandler): ''' /api/ext/.* ''' storm_prefix = 'init { $request = $lib.cortex.httpapi.response($_http_request_info) }' # Disables the etag header from being computed and set. It is too much magic for # a user defined API to utilize.
[docs] def compute_etag(self): return None
[docs] def set_default_headers(self): self.clear_header('Server')
[docs] async def get(self, path): return await self._runHttpExt('get', path)
[docs] async def head(self, path): return await self._runHttpExt('head', path)
[docs] async def post(self, path): return await self._runHttpExt('post', path)
[docs] async def put(self, path): return await self._runHttpExt('put', path)
[docs] async def delete(self, path): return await self._runHttpExt('delete', path)
[docs] async def patch(self, path): return await self._runHttpExt('patch', path)
[docs] async def options(self, path): return await self._runHttpExt('options', path)
async def _runHttpExt(self, meth, path): core = self.getCore() adef, args = await core.getHttpExtApiByPath(path) if adef is None: self.set_status(404) self.sendRestErr('NoSuchPath', f'No Extended HTTP API endpoint matches {path}') return await self.finish() requester = '' iden = adef.get("iden") useriden = adef.get('owner') if adef.get('authenticated'): requester = await self.useriden() if requester is None: await self.reqAuthUser() return for pdef in adef.get('perms'): if not await self.allowed(pdef.get('perm'), default=pdef.get('default')): return if adef.get('runas') == 'user': useriden = requester storm = adef['methods'].get(meth) if storm is None: self.set_status(405) meths = [meth.upper() for meth in adef.get('methods')] self.set_header('Allowed', ', '.join(meths)) mesg = f'Extended HTTP API {iden} has no method for {meth.upper()}.' if meths: mesg = f'{mesg} Supports {", ".join(meths)}.' self.sendRestErr('NeedConfValu', mesg) return await self.finish() # We flatten the request headers and parameters into a flat key/valu map. # The first instance of a given key wins. request_headers = {} for key, valu in self.request.headers.get_all(): request_headers.setdefault(key.lower(), valu) params = {} for key, valus in self.request.query_arguments.items(): for valu in valus: params.setdefault(key, valu.decode()) info = { 'uri': self.request.uri, 'body': self.request.body, 'iden': iden, 'path': path, 'user': requester, 'method': self.request.method, 'params': params, 'headers': request_headers, 'args': args, 'client': self.request.remote_ip, } varz = adef.get('vars') varz['_http_request_info'] = info opts = { 'mirror': adef.get('pool', False), 'readonly': adef.get('readonly'), 'show': ( 'http:resp:body', 'http:resp:code', 'http:resp:headers', ), 'user': useriden, 'vars': varz, 'view': adef.get('view'), '_loginfo': { 'httpapi': iden } } query = '\n'.join((self.storm_prefix, storm)) rcode = False rbody = False try: async for mtyp, info in core.storm(query, opts=opts): if mtyp == 'http:resp:code': if rbody: # We've already flushed() the stream at this point, so we cannot # change the status code or the response headers. We just have to # log the error and move along. mesg = f'Extended HTTP API {iden} tried to set code after sending body.' logger.error(mesg) continue rcode = True self.set_status(info['code']) elif mtyp == 'http:resp:headers': if rbody: # We've already flushed() the stream at this point, so we cannot # change the status code or the response headers. We just have to # log the error and move along. mesg = f'Extended HTTP API {iden} tried to set headers after sending body.' logger.error(mesg) continue for hkey, hval in info['headers'].items(): self.set_header(hkey, hval) elif mtyp == 'http:resp:body': if not rcode: self.clear() self.set_status(500) self.sendRestErr('StormRuntimeError', f'Extended HTTP API {iden} must set status code before sending body.') return await self.finish() rbody = True body = info['body'] self.write(body) await self.flush() elif mtyp == 'err': errname, erfo = info mesg = f'Error executing Extended HTTP API {iden}: {errname} {erfo.get("mesg")}' logger.error(mesg) if rbody: # We've already flushed() the stream at this point, so we cannot # change the status code or the response headers. We just have to # log the error and move along. continue # Since we haven't flushed the body yet, we can clear the handler # and send the error the user. self.clear() self.set_status(500) self.sendRestErr(errname, erfo.get('mesg')) rcode = True rbody = True except Exception as e: rcode = True enfo = s_common.err(e) logger.exception(f'Extended HTTP API {iden} encountered fatal error: {enfo[1].get("mesg")}') if rbody is False: self.clear() self.set_status(500) self.sendRestErr(enfo[0], f'Extended HTTP API {iden} encountered fatal error: {enfo[1].get("mesg")}') if rcode is False: self.clear() self.set_status(500) self.sendRestErr('StormRuntimeError', f'Extended HTTP API {iden} never set status code.') await self.finish()