diff --git a/docs/synapse/httpapi.rst b/docs/synapse/httpapi.rst index 928e890e71..5e9b97e61c 100644 --- a/docs/synapse/httpapi.rst +++ b/docs/synapse/httpapi.rst @@ -120,6 +120,22 @@ session may then be used to call other HTTP API endpoints as the authenticated u *Returns* The newly created role dictionary. +/api/v1/auth/delrole +~~~~~~~~~~~~~~~~~~~~ + +*Method* + POST + + This API endpoint allows the caller to delete a role from the system. + + *Input* + This API expects the following JSON body:: + + { "name": "myrole" } + + *Returns* + null + /api/v1/auth/user/ ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/synapse/lib/cell.py b/synapse/lib/cell.py index f75c623a42..87ab67e906 100644 --- a/synapse/lib/cell.py +++ b/synapse/lib/cell.py @@ -450,6 +450,8 @@ async def fini(): self.addHttpApi('/api/v1/auth/adduser', s_httpapi.AuthAddUserV1, {'cell': self}) self.addHttpApi('/api/v1/auth/addrole', s_httpapi.AuthAddRoleV1, {'cell': self}) + self.addHttpApi('/api/v1/auth/delrole', s_httpapi.AuthDelRoleV1, {'cell': self}) + self.addHttpApi('/api/v1/auth/user/(.*)', s_httpapi.AuthUserV1, {'cell': self}) self.addHttpApi('/api/v1/auth/role/(.*)', s_httpapi.AuthRoleV1, {'cell': self}) diff --git a/synapse/lib/hive.py b/synapse/lib/hive.py index ded6a712d1..3d5256d912 100644 --- a/synapse/lib/hive.py +++ b/synapse/lib/hive.py @@ -817,6 +817,7 @@ async def __anit__(self, auth, node): self.info.setdefault('admin', False) self.info.setdefault('passwd', None) self.info.setdefault('locked', False) + self.info.setdefault('archived', False) self.roles = self.info.get('roles', onedit=self._onRolesEdit) self.admin = self.info.get('admin', onedit=self._onAdminEdit) @@ -940,6 +941,11 @@ async def setAdmin(self, admin): async def setLocked(self, locked): await self.info.set('locked', locked) + async def setArchived(self, archived): + await self.info.set('archived', archived) + if archived: + await self.setLocked(True) + def tryPasswd(self, passwd): if self.locked: diff --git a/synapse/lib/httpapi.py b/synapse/lib/httpapi.py index 894c2aecc0..46c1ac25db 100644 --- a/synapse/lib/httpapi.py +++ b/synapse/lib/httpapi.py @@ -280,7 +280,21 @@ async def get(self): if not await self.reqAuthUser(): return - self.sendRestRetn([u.pack() for u in self.cell.auth.users()]) + 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 as e: + return self.sendRestErr('BadHttpParam', 'The parameter "archived" must be 0 or 1 if specified.') + + if archived: + self.sendRestRetn([u.pack() for u in self.cell.auth.users()]) + return + + self.sendRestRetn([u.pack() for u in self.cell.auth.users() if not u.info.get('archived')]) + return class AuthRolesV1(Handler): @@ -340,6 +354,10 @@ async def post(self, iden): if admin is not None: await user.setAdmin(bool(admin)) + archived = body.get('archived') + if archived is not None: + await user.setArchived(bool(archived)) + self.sendRestRetn(user.pack()) class AuthRoleV1(Handler): @@ -513,6 +531,31 @@ async def post(self): self.sendRestRetn(role.pack()) return +class AuthDelRoleV1(Handler): + + 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 + + role = self.cell.auth.getRoleByName(name) + if role is None: + return self.sendRestErr('NoSuchRole', f'The role {name} does not exist!') + + await self.cell.auth.delRole(name) + + self.sendRestRetn(None) + return + class ModelNormV1(Handler): async def get(self): @@ -539,8 +582,5 @@ async def get(self): valu, info = prop.type.norm(propvalu) self.sendRestRetn({'norm': valu, 'info': info}) - except asyncio.CancelledError: - raise - except Exception as e: return self.sendRestExc(e) diff --git a/synapse/tests/test_lib_httpapi.py b/synapse/tests/test_lib_httpapi.py index 3ba559c848..3883f660c3 100644 --- a/synapse/tests/test_lib_httpapi.py +++ b/synapse/tests/test_lib_httpapi.py @@ -5,6 +5,125 @@ class HttpApiTest(s_tests.SynTest): + async def test_http_user_archived(self): + + async with self.getTestCore() as core: + + host, port = await core.addHttpsPort(0, host='127.0.0.1') + + root = core.auth.getUserByName('root') + await root.setPasswd('secret') + + newb = await core.auth.addUser('newb') + + async with self.getHttpSess(auth=('root', 'secret'), port=port) as sess: + + async with sess.get(f'https://localhost:{port}/api/v1/auth/users') as resp: + item = await resp.json() + users = item.get('result') + self.isin('newb', [u.get('name') for u in users]) + + info = {'archived': True} + async with sess.post(f'https://localhost:{port}/api/v1/auth/user/{newb.iden}', json=info) as resp: + retn = await resp.json() + self.eq('ok', retn.get('status')) + + self.true(newb.locked) + + async with sess.get(f'https://localhost:{port}/api/v1/auth/users') as resp: + item = await resp.json() + users = item.get('result') + self.notin('newb', [u.get('name') for u in users]) + + async with sess.get(f'https://localhost:{port}/api/v1/auth/users?archived=asdf') as resp: + item = await resp.json() + self.eq('err', item.get('status')) + self.eq('BadHttpParam', item.get('code')) + + async with sess.get(f'https://localhost:{port}/api/v1/auth/users?archived=99') as resp: + item = await resp.json() + self.eq('err', item.get('status')) + self.eq('BadHttpParam', item.get('code')) + + async with sess.get(f'https://localhost:{port}/api/v1/auth/users?archived=0') as resp: + item = await resp.json() + users = item.get('result') + self.notin('newb', [u.get('name') for u in users]) + + async with sess.get(f'https://localhost:{port}/api/v1/auth/users?archived=1') as resp: + item = await resp.json() + users = item.get('result') + self.isin('newb', [u.get('name') for u in users]) + + info = {'archived': False} + async with sess.post(f'https://localhost:{port}/api/v1/auth/user/{newb.iden}', json=info) as resp: + retn = await resp.json() + self.eq('ok', retn.get('status')) + + async with sess.get(f'https://localhost:{port}/api/v1/auth/users') as resp: + item = await resp.json() + users = item.get('result') + self.isin('newb', [u.get('name') for u in users]) + + async def test_http_delrole(self): + + async with self.getTestCore() as core: + + host, port = await core.addHttpsPort(0, host='127.0.0.1') + + root = core.auth.getUserByName('root') + await root.setPasswd('secret') + + newb = await core.auth.addUser('bob') + await newb.setPasswd('secret') + + bobs = await core.auth.addRole('bobs') + + await newb.grant('bobs') + + async with self.getHttpSess() as sess: + + info = {'name': 'bobs'} + async with sess.post(f'https://localhost:{port}/api/v1/auth/delrole', json=info) as resp: + item = await resp.json() + self.eq('err', item.get('status')) + self.eq('NotAuthenticated', item.get('code')) + + async with self.getHttpSess(auth=('bob', 'secret'), port=port) as sess: + + info = {'name': 'bobs'} + async with sess.post(f'https://localhost:{port}/api/v1/auth/delrole', json=info) as resp: + item = await resp.json() + self.eq('err', item.get('status')) + self.eq('AuthDeny', item.get('code')) + + async with self.getHttpSess(auth=('root', 'secret'), port=port) as sess: + + info = {} + async with sess.post(f'https://localhost:{port}/api/v1/auth/delrole', json=info) as resp: + item = await resp.json() + self.eq('err', item.get('status')) + self.eq('MissingField', item.get('code')) + + async with sess.post(f'https://localhost:{port}/api/v1/auth/delrole', data=b'asdf') as resp: + item = await resp.json() + self.eq('err', item.get('status')) + self.eq('BadJson', item.get('code')) + + info = {'name': 'newp'} + async with sess.post(f'https://localhost:{port}/api/v1/auth/delrole', json=info) as resp: + item = await resp.json() + self.eq('err', item.get('status')) + self.eq('NoSuchRole', item.get('code')) + + info = {'name': 'bobs'} + async with sess.post(f'https://localhost:{port}/api/v1/auth/delrole', json=info) as resp: + item = await resp.json() + self.eq('ok', item.get('status')) + + self.len(0, newb.getRoles()) + self.none(core.auth.getRoleByName('bobs')) + async def test_http_auth(self): ''' Test the HTTP api for cell auth. diff --git a/synapse/tests/utils.py b/synapse/tests/utils.py index 9962410e9e..9c6a51254a 100644 --- a/synapse/tests/utils.py +++ b/synapse/tests/utils.py @@ -989,10 +989,24 @@ def getAsyncLoggerStream(self, logname, mesg=''): slogger.removeHandler(handler) @contextlib.asynccontextmanager - async def getHttpSess(self): + async def getHttpSess(self, auth=None, port=None): + jar = aiohttp.CookieJar(unsafe=True) conn = aiohttp.TCPConnector(ssl=False) + async with aiohttp.ClientSession(cookie_jar=jar, connector=conn) as sess: + + if auth is not None: + + if port is None: # pragma: no cover + raise Exception('getHttpSess requires port for auth') + + user, passwd = auth + async with sess.post(f'https://localhost:{port}/api/v1/login', json={'user': user, 'passwd': passwd}) as resp: + retn = await resp.json() + self.eq('ok', retn.get('status')) + self.eq(user, retn['result']['name']) + yield sess @contextlib.contextmanager