import base64
import asyncio
import logging
from http import HTTPStatus
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.json as s_json
import synapse.lib.msgpack as s_msgpack
import synapse.lib.schemas as s_schemas
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(HTTPStatus.NO_CONTENT)
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 sendRestErr(self, code: str, mesg: str, *, status_code: int | HTTPStatus | None =None) -> None:
'''
Send a JSON REST error message with a code and message.
Args:
code: The error code.
mesg: The error message.
status_code: The HTTP status code. This is optional.
Notes:
If the status code is not provided or set prior to calling this API, the response
will have an HTTP status code of 200 (OK).
This does write the response stream. No further content should be written
in the response after calling this.
'''
if status_code is not None:
self.set_status(status_code)
self.set_header('Content-Type', 'application/json')
return self.write({'status': 'err', 'code': code, 'mesg': mesg})
[docs]
def sendRestExc(self, e: Exception, *, status_code: int | HTTPStatus | None = None) -> None:
'''
Send a JSON REST error message based on the exception.
Args:
e: The exception to send. The exception class name will be used as the error code.
status_code: The HTTP status code. This is optional.
Notes:
If the status code is not provided or set prior to calling this API, the response
will have an HTTP status code of 200.
This does write the response stream. No further content should be written
in the response after calling this.
'''
mesg = str(e)
if isinstance(e, s_exc.SynErr):
mesg = e.get('mesg', mesg)
self.set_header('Content-Type', 'application/json')
return self.sendRestErr(e.__class__.__name__, mesg, status_code=status_code)
[docs]
def sendRestRetn(self, valu, *, status_code: int | HTTPStatus | None = None) -> None:
'''
Send a successful JSON REST response.
Args:
valu: The JSON compatible value to send.
status_code: The HTTP status code. This is optional.
Notes:
If the status code is not provided or set prior to calling this API, the response
will have an HTTP status code of 200.
This does write the response stream. No further content should be written
in the response after calling this.
'''
if status_code is not None:
self.set_status(status_code)
self.set_header('Content-Type', 'application/json')
return self.write({'status': 'ok', 'result': valu})
[docs]
def getJsonBody(self, validator=None):
return self.loadJsonMesg(self.request.body, validator=validator)
[docs]
def loadJsonMesg(self, byts, validator=None):
try:
item = s_json.loads(byts)
if validator is not None:
validator(item)
return item
except s_exc.SchemaViolation as e:
self.sendRestErr('SchemaViolation', str(e), status_code=HTTPStatus.BAD_REQUEST)
return None
except Exception:
self.sendRestErr('SchemaViolation', 'Invalid JSON content.',
status_code=HTTPStatus.BAD_REQUEST)
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.sendRestErr('NotAuthenticated', 'The session is not logged in.',
status_code=HTTPStatus.UNAUTHORIZED)
[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.',
status_code=HTTPStatus.FORBIDDEN)
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
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, status_code=HTTPStatus.FORBIDDEN)
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(s_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()
[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
async def _reqValidOpts(self, opts: dict | None) -> dict | None:
'''
Creates or validates an opts dict with the current session useriden.
If the session useriden differs from the user key, validate the user
has the impersonate permission ( that may require a round trip to authcell ).
Notes:
This API sets up HTTP response values if it returns None.
Args:
opts: The opts dictionary to validate.
Returns:
Opts dict if allowed; None if not allowed.
'''
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
def _handleStormErr(self, err: Exception):
if isinstance(err, s_exc.AuthDeny):
return self.sendRestExc(err, status_code=HTTPStatus.FORBIDDEN)
if isinstance(err, s_exc.NoSuchView):
return self.sendRestExc(err, status_code=HTTPStatus.NOT_FOUND)
return self.sendRestExc(err, status_code=HTTPStatus.BAD_REQUEST)
[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')
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
flushed = False
try:
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(s_json.dumps(pode, newline=jsonlines))
await self.flush()
flushed = True
except Exception as e:
if not flushed:
return self._handleStormErr(e)
[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')
flushed = None
try:
async for mesg in self.getCore().storm(query, opts=opts):
self.write(s_json.dumps(mesg, newline=jsonlines))
await self.flush()
flushed = True
except Exception as e:
if not flushed:
return self._handleStormErr(e)
[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 Exception as e:
return self._handleStormErr(e)
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
flushed = False
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()
flushed = True
except Exception as e:
if not flushed:
return self._handleStormErr(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:
ret = await self.cell.reqValidStorm(query, opts)
except Exception as e:
return self._handleStormErr(e)
else:
return self.sendRestRetn(ret)
[docs]
class BeholdSockV1(WebSocket):
[docs]
async def onInitMessage(self, byts):
try:
mesg = s_json.loads(byts)
if mesg.get('type') != 'call:init':
raise s_exc.BadMesgFormat(mesg='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(validator=s_schemas.reqValidHttpLoginV1)
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.', status_code=HTTPStatus.NOT_FOUND)
if udef.get('locked'):
self.logAuthIssue(mesg='User is locked.', user=udef.get('iden'), username=name)
return self.sendRestErr('AuthDeny', 'User is locked.', status_code=HTTPStatus.FORBIDDEN)
if not await authcell.tryUserPasswd(name, passwd):
self.logAuthIssue(mesg='Incorrect password.', user=udef.get('iden'), username=name)
return self.sendRestErr('AuthDeny', 'Incorrect password.', status_code=HTTPStatus.FORBIDDEN)
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.',
status_code=HTTPStatus.BAD_REQUEST)
except Exception:
return self.sendRestErr('BadHttpParam', 'The parameter "archived" must be 0 or 1 if specified.',
status_code=HTTPStatus.BAD_REQUEST)
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.',
status_code=HTTPStatus.NOT_FOUND)
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.',
status_code=HTTPStatus.NOT_FOUND)
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}',
status_code=HTTPStatus.NOT_FOUND)
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'), status_code=HTTPStatus.BAD_REQUEST)
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.', status_code=HTTPStatus.NOT_FOUND)
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.', status_code=HTTPStatus.NOT_FOUND)
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.',
status_code=HTTPStatus.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.',
status_code=HTTPStatus.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.',
status_code=HTTPStatus.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.',
status_code=HTTPStatus.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.',
status_code=HTTPStatus.BAD_REQUEST)
return
authcell = self.getAuthCell()
if await authcell.getUserDefByName(name) is not None:
self.sendRestErr('DupUser', f'A user named {name} already exists.',
status_code=HTTPStatus.BAD_REQUEST)
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.',
status_code=HTTPStatus.BAD_REQUEST)
return
authcell = self.getAuthCell()
if await authcell.getRoleDefByName(name) is not None:
self.sendRestErr('DupRole', f'A role named {name} already exists.',
status_code=HTTPStatus.BAD_REQUEST)
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.',
status_code=HTTPStatus.BAD_REQUEST)
return
authcell = self.getAuthCell()
rdef = await authcell.getRoleDefByName(name)
if rdef is None:
return self.sendRestErr('NoSuchRole', f'The role {name} does not exist!',
status_code=HTTPStatus.NOT_FOUND)
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.',
status_code=HTTPStatus.BAD_REQUEST)
return
try:
valu, info = await self.cell.getPropNorm(propname, propvalu, typeopts=typeopts)
except s_exc.NoSuchProp:
return self.sendRestErr('NoSuchProp', f'The property {propname} does not exist.',
status_code=HTTPStatus.NOT_FOUND)
except Exception as e:
return self.sendRestExc(e, status_code=HTTPStatus.BAD_REQUEST)
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 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.',
status_code=HTTPStatus.NOT_FOUND)
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.',
status_code=HTTPStatus.BAD_REQUEST)
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.',
status_code=HTTPStatus.NOT_FOUND)
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, status_code=HTTPStatus.FORBIDDEN)
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, status_code=HTTPStatus.BAD_REQUEST)
[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.sendRestErr('NoSuchPath', f'No Extended HTTP API endpoint matches {path}',
status_code=HTTPStatus.NOT_FOUND)
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:
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, status_code=HTTPStatus.METHOD_NOT_ALLOWED)
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.sendRestErr('StormRuntimeError',
f'Extended HTTP API {iden} must set status code before sending body.',
status_code=HTTPStatus.INTERNAL_SERVER_ERROR)
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.sendRestErr(errname, erfo.get('mesg'), status_code=HTTPStatus.INTERNAL_SERVER_ERROR)
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.sendRestErr(enfo[0],
f'Extended HTTP API {iden} encountered fatal error: {enfo[1].get("mesg")}',
status_code=HTTPStatus.INTERNAL_SERVER_ERROR)
if rcode is False:
self.clear()
self.sendRestErr('StormRuntimeError', f'Extended HTTP API {iden} never set status code.',
status_code=HTTPStatus.INTERNAL_SERVER_ERROR)
await self.finish()