def test_generate_attr_LOCALE(preserve_state): with preserve_state(config, 'Config'): config.Config.locales = ['ar_AE', 'en_AE'] config.Config.locale = 'ar_AE' attr_val = utils.generate_attr(attr_type=ATTR.LOCALE()) assert len(attr_val.keys()) == 2 assert set(attr_val.keys()) == {'ar_AE', 'en_AE'}
async def test_validate_doc_allow_update_locale_dict_dot_notated(preserve_state): with preserve_state(config, 'Config'): config.Config.locales = ['ar_AE', 'en_AE'] config.Config.locale = 'ar_AE' attrs = { 'attr_locale': ATTR.LOCALE(), } doc = {'attr_locale.ar_AE': 'ar_AE value'} await utils.validate_doc(doc=doc, attrs=attrs, mode='update') assert doc == {'attr_locale.ar_AE': 'ar_AE value'}
async def test_validate_attr_LOCALE_None(preserve_state): with preserve_state(config, 'Config'): config.Config.locales = ['ar_AE', 'en_AE', 'de_DE'] config.Config.locale = 'ar_AE' with pytest.raises(InvalidAttrException): await validate_attr( attr_name='test_validate_attr_LOCALE', attr_type=ATTR.LOCALE(), attr_val=None, mode='create', )
async def test_validate_attr_LOCALE_None_allow_none(preserve_state): with preserve_state(config, 'Config'): config.Config.locales = ['ar_AE', 'en_AE', 'de_DE'] config.Config.locale = 'ar_AE' attr_val = await validate_attr( attr_name='test_validate_attr_LOCALE', attr_type=ATTR.LOCALE(), attr_val=None, mode='update', ) assert attr_val == None
async def test_validate_attr_LOCALE_default_int(preserve_state): with preserve_state(config, 'Config'): config.Config.locales = ['ar_AE', 'en_AE', 'de_DE'] config.Config.locale = 'ar_AE' attr_type = ATTR.LOCALE() attr_type._default = 'test_validate_attr_LOCALE' attr_val = await validate_attr( attr_name='test_validate_attr_LOCALE', attr_type=attr_type, attr_val=1, mode='create', ) assert attr_val == 'test_validate_attr_LOCALE'
async def test_validate_doc_allow_update_list_typed_dict_locale_dot_notated( preserve_state, ): with preserve_state(config, 'Config'): config.Config.locales = ['en_GB', 'jp_JP'] config.Config.locale = 'en_GB' attrs = { 'val': ATTR.LIST( list=[ATTR.TYPED_DICT(dict={'address': ATTR.LOCALE(), 'coords': ATTR.GEO()})] ) } doc = {'val.0.address.jp_JP': 'new_address'} await utils.validate_doc(doc=doc, attrs=attrs, mode='update') assert doc == {'val.0.address.jp_JP': 'new_address'}
async def test_validate_attr_LOCALE_locale_all(preserve_state): with preserve_state(config, 'Config'): config.Config.locales = ['ar_AE', 'en_AE', 'de_DE'] config.Config.locale = 'ar_AE' locale_attr_val = { 'ar_AE': 'str', 'en_AE': 'str', 'de_DE': 'str', } attr_val = await validate_attr( attr_name='test_validate_attr_LOCALE', attr_type=ATTR.LOCALE(), attr_val=locale_attr_val, mode='create', ) assert attr_val == locale_attr_val
async def test_validate_attr_LOCALE_locale_min_strategy_none(preserve_state): with preserve_state(config, 'Config'): config.Config.locales = ['ar_AE', 'en_AE', 'de_DE'] config.Config.locale = 'ar_AE' config.Config.locale_strategy = LOCALE_STRATEGY.NONE_VALUE locale_attr_val = { 'ar_AE': 'str', 'en_AE': None, 'de_DE': None, } attr_val = await validate_attr( attr_name='test_validate_attr_LOCALE', attr_type=ATTR.LOCALE(), attr_val={ 'ar_AE': 'str', }, mode='create', ) assert attr_val == locale_attr_val
async def test_validate_attr_LOCALE_locale_min_strategy_callable( preserve_state): with preserve_state(config, 'Config'): config.Config.locales = ['ar_AE', 'en_AE', 'de_DE'] config.Config.locale = 'ar_AE' config.Config.locale_strategy = ( lambda attr_val, locale: f'DEFAULT:{locale}:{attr_val[config.Config.locale]}') locale_attr_val = { 'ar_AE': 'str', 'en_AE': 'DEFAULT:en_AE:str', 'de_DE': 'DEFAULT:de_DE:str', } attr_val = await validate_attr( attr_name='test_validate_attr_LOCALE', attr_type=ATTR.LOCALE(), attr_val={ 'ar_AE': 'str', }, mode='create', ) assert attr_val == locale_attr_val
class Group(BaseModule): '''`Group` module provides data type and controller for groups in Nawah eco-system.''' collection = 'groups' attrs = { 'user': ATTR.ID(desc='`_id` of `User` doc the doc belongs to.'), 'name': ATTR.LOCALE(desc='Name of the groups as `LOCALE`.'), 'desc': ATTR.LOCALE( desc= 'Description of the group as `LOCALE`. This can be used for dynamic generated groups that are meant to be exposed to end-users.' ), 'privileges': ATTR.KV_DICT( desc='Privileges that any user is a member of the group has.', key=ATTR.STR(), val=ATTR.LIST(list=[ATTR.STR()]), ), 'settings': ATTR.KV_DICT( desc= '`Setting` docs to be created, or required for members users when added to the group.', key=ATTR.STR(), val=ATTR.ANY(), ), 'create_time': ATTR.DATETIME( desc='Python `datetime` ISO format of the doc creation.'), } defaults = { 'desc': {locale: '' for locale in Config.locales}, 'privileges': {}, 'settings': {}, } methods = { 'read': METHOD(permissions=[PERM(privilege='admin')]), 'create': METHOD(permissions=[PERM(privilege='admin')]), 'update': METHOD( permissions=[ PERM(privilege='admin'), PERM( privilege='update', query_mod={'user': '******'}, doc_mod={'privileges': None}, ), ], query_args={'_id': ATTR.ID()}, ), 'delete': METHOD( permissions=[ PERM(privilege='admin'), PERM(privilege='delete', query_mod={'user': '******'}), ], query_args={'_id': ATTR.ID()}, ), } async def pre_update(self, skip_events, env, query, doc, payload): # [DOC] Make sure no attrs overwriting would happen if 'attrs' in doc.keys(): results = await self.read(skip_events=[Event.PERM], env=env, query=query) if not results.args.count: raise self.exception(status=400, msg='Group is invalid.', args={'code': 'INVALID_GROUP'}) if results.args.count > 1: raise self.exception( status=400, msg= 'Updating group attrs can be done only to individual groups.', args={'code': 'MULTI_ATTRS_UPDATE'}, ) results.args.docs[0]['attrs'].update({ attr: doc['attrs'][attr] for attr in doc['attrs'].keys() if doc['attrs'][attr] != None and doc['attrs'][attr] != '' }) doc['attrs'] = results.args.docs[0]['attrs'] return (skip_events, env, query, doc, payload)
class User(BaseModule): '''`User` module provides data type and controller for users in Nawah eco-system. The permissions of the module methods are designed to be as secure for exposed calls, and as flexible for privileged-access.''' collection = 'users' attrs = { 'name': ATTR.LOCALE(desc='Name of the user as `LOCALE`.'), 'locale': ATTR.LOCALES(desc='Default locale of the user.'), 'create_time': ATTR.DATETIME( desc='Python `datetime` ISO format of the doc creation.'), 'login_time': ATTR.DATETIME(desc='Python `datetime` ISO format of the last login.'), 'groups': ATTR.LIST( desc='List of `_id` for every group the user is member of.', list=[ATTR.ID(desc='`_id` of Group doc the user is member of.')], ), 'privileges': ATTR.KV_DICT( desc= 'Privileges of the user. These privileges are always available to the user regardless of whether groups user is part of have them or not.', key=ATTR.STR(), val=ATTR.LIST(list=[ATTR.STR()]), ), 'status': ATTR.LITERAL( desc= 'Status of the user to determine whether user has access to the app or not.', literal=['active', 'banned', 'deleted', 'disabled_password'], ), } defaults = { 'login_time': None, 'status': 'active', 'groups': [], 'privileges': {} } unique_attrs = [] methods = { 'read': METHOD(permissions=[ PERM(privilege='admin'), PERM(privilege='read', query_mod={'_id': '$__user'}), ]), 'create': METHOD(permissions=[PERM(privilege='admin')]), 'update': METHOD( permissions=[ PERM(privilege='admin', doc_mod={'groups': None}), PERM( privilege='update', query_mod={'_id': '$__user'}, doc_mod={ 'groups': None, 'privileges': None }, ), ], query_args={'_id': ATTR.ID()}, ), 'delete': METHOD( permissions=[ PERM(privilege='admin'), PERM(privilege='delete', query_mod={'_id': '$__user'}), ], query_args={'_id': ATTR.ID()}, ), 'read_privileges': METHOD( permissions=[ PERM(privilege='admin'), PERM(privilege='read', query_mod={'_id': '$__user'}), ], query_args={'_id': ATTR.ID()}, ), 'add_group': METHOD( permissions=[PERM(privilege='admin')], query_args={'_id': ATTR.ID()}, doc_args=[{ 'group': ATTR.ID() }, { 'group': ATTR.LIST(list=[ATTR.ID()]) }], ), 'delete_group': METHOD( permissions=[PERM(privilege='admin')], query_args={ '_id': ATTR.ID(), 'group': ATTR.ID() }, ), 'retrieve_file': METHOD(permissions=[PERM(privilege='__sys')], get_method=True), 'create_file': METHOD(permissions=[PERM(privilege='__sys')]), 'delete_file': METHOD(permissions=[PERM(privilege='__sys')]), } async def on_read(self, results, skip_events, env, query, doc, payload): for i in range(len(results['docs'])): user = results['docs'][i] for auth_attr in Config.user_attrs.keys(): del user[f'{auth_attr}_hash'] if len(Config.user_doc_settings): setting_results = await Registry.module('setting').read( skip_events=[Event.PERM, Event.ARGS], env=env, query=[{ 'user': user._id, 'var': { '$in': Config.user_doc_settings } }], ) user_doc_settings = copy.copy(Config.user_doc_settings) if setting_results.args.count: for setting_doc in setting_results.args.docs: user_doc_settings.remove(setting_doc['var']) user[setting_doc['var']] = setting_doc['val'] # [DOC] Forward-compatibility: If user was created before presence of any user_doc_settings, add them with default value for setting_attr in user_doc_settings: user[setting_attr] = Config.user_settings[ setting_attr].default # [DOC] Set NAWAH_VALUES.NONE_VALUE to None if it was default if user[setting_attr] == NAWAH_VALUES.NONE_VALUE: user[setting_attr] = None return (results, skip_events, env, query, doc, payload) async def pre_create(self, skip_events, env, query, doc, payload): if Event.ARGS not in skip_events: doc['groups'] = [ObjectId('f00000000000000000000013')] user_settings = {} for attr in Config.user_settings.keys(): if Config.user_settings[attr].type == 'user_sys': user_settings[attr] = copy.deepcopy( Config.user_settings[attr].default) else: if attr in doc.keys(): try: await validate_attr( mode='create', attr_name=attr, attr_type=Config.user_settings[attr].val_type, attr_val=doc[attr], ) user_settings[attr] = doc[attr] except: raise self.exception( status=400, msg= f'Invalid settings attr \'{attr}\' for \'create\' request on module \'CORE_USER\'', args={'code': 'INVALID_ATTR'}, ) else: if Config.user_settings[ attr].default == NAWAH_VALUES.NONE_VALUE: raise self.exception( status=400, msg= f'Missing settings attr \'{attr}\' for \'create\' request on module \'CORE_USER\'', args={'code': 'MISSING_ATTR'}, ) else: user_settings[attr] = copy.deepcopy( Config.user_settings[attr].default) payload['user_settings'] = user_settings return (skip_events, env, query, doc, payload) async def on_create(self, results, skip_events, env, query, doc, payload): if 'user_settings' in payload.keys(): for setting in payload['user_settings'].keys(): setting_results = await Registry.module('setting').create( skip_events=[Event.PERM, Event.ARGS], env=env, doc={ 'user': results['docs'][0]._id, 'var': setting, 'val_type': encode_attr_type( attr_type=Config.user_settings[setting].val_type), 'val': payload['user_settings'][setting], 'type': Config.user_settings[setting].type, }, ) if setting_results.status != 200: return setting_results return (results, skip_events, env, query, doc, payload) async def read_privileges(self, skip_events=[], env={}, query=[], doc={}): # [DOC] Confirm _id is valid results = await self.read(skip_events=[Event.PERM], env=env, query=[{ '_id': query['_id'][0] }]) if not results.args.count: raise self.exception(status=400, msg='User is invalid.', args={'code': 'INVALID_USER'}) user = results.args.docs[0] for group in user.groups: group_results = await Registry.module('group').read( skip_events=[Event.PERM], env=env, query=[{ '_id': group }]) group = group_results.args.docs[0] for privilege in group.privileges.keys(): if privilege not in user.privileges.keys(): user.privileges[privilege] = [] for i in range(len(group.privileges[privilege])): if group.privileges[privilege][i] not in user.privileges[ privilege]: user.privileges[privilege].append( group.privileges[privilege][i]) return results async def add_group(self, skip_events=[], env={}, query=[], doc={}): # [DOC] Check for list group attr if type(doc['group']) == list: for i in range(0, len(doc['group']) - 1): await self.add_group( skip_events=skip_events, env=env, query=query, doc={'group': doc['group'][i]}, ) doc['group'] = doc['group'][-1] # [DOC] Confirm all basic args are provided doc['group'] = ObjectId(doc['group']) # [DOC] Confirm group is valid results = await Registry.module('group').read(skip_events=[Event.PERM], env=env, query=[{ '_id': doc['group'] }]) if not results.args.count: raise self.exception(status=400, msg='Group is invalid.', args={'code': 'INVALID_GROUP'}) # [DOC] Get user details results = await self.read(skip_events=[Event.PERM], env=env, query=query) if not results.args.count: raise self.exception(status=400, msg='User is invalid.', args={'code': 'INVALID_USER'}) user = results.args.docs[0] # [DOC] Confirm group was not added before if doc['group'] in user.groups: raise self.exception( status=400, msg='User is already a member of the group.', args={'code': 'GROUP_ADDED'}, ) user.groups.append(doc['group']) # [DOC] Update the user results = await self.update(skip_events=[Event.PERM], env=env, query=query, doc={'groups': user.groups}) # [DOC] if update fails, return update results if results.status != 200: return results # [DOC] Check if the updated User doc belongs to current session and update it if env['session'].user._id == user._id: user_results = await self.read_privileges(skip_events=[Event.PERM], env=env, query=[{ '_id': user._id }]) env['session']['user'] = user_results.args.docs[0] return results async def delete_group(self, skip_events=[], env={}, query=[], doc={}): # [DOC] Confirm group is valid results = await Registry.module('group').read(skip_events=[Event.PERM], env=env, query=[{ '_id': query['group'][0] }]) if not results.args.count: raise self.exception(status=400, msg='Group is invalid.', args={'code': 'INVALID_GROUP'}) # [DOC] Get user details results = await self.read(skip_events=[Event.PERM], env=env, query=[{ '_id': query['_id'][0] }]) if not results.args.count: raise self.exception(status=400, msg='User is invalid.', args={'code': 'INVALID_USER'}) user = results.args.docs[0] # [DOC] Confirm group was not added before if query['group'][0] not in user.groups: raise self.exception( status=400, msg='User is not a member of the group.', args={'code': 'GROUP_NOT_ADDED'}, ) # [DOC] Update the user results = await self.update( skip_events=[Event.PERM], env=env, query=[{ '_id': query['_id'][0] }], doc={'groups': { '$del_val': [query['group'][0]] }}, ) # [DOC] if update fails, return update results if results.status != 200: return results # [DOC] Check if the updated User doc belongs to current session and update it if env['session'].user._id == user._id: user_results = await self.read_privileges(skip_events=[Event.PERM], env=env, query=[{ '_id': user._id }]) env['session']['user'] = user_results.args.docs[0] return results