async def update_groups(self, new_groups: list): from app.auth import userdb # new_groups = set(filter(valid_str_only, new_groups)) valid_groups = set(await Group.filter(name__in=new_groups ).values_list('name', flat=True)) if not valid_groups: return existing_groups = set(await self.get_groups()) toadd: set = valid_groups - existing_groups toremove: set = existing_groups - valid_groups if toadd: toadd_obj = await Group.filter(name__in=toadd).only('id', 'name') if toadd_obj: await self.groups.add(*toadd_obj) if toremove: toremove_obj = await Group.filter(name__in=toremove ).only('id', 'name') if toremove_obj: await self.groups.remove(*toremove_obj) partialkey = s.CACHE_USERNAME.format(self.id) if user_dict := red.get(partialkey): user_dict = cache.restoreuser_dict(user_dict) user = userdb.usercomplete(**user_dict)
async def add_group(self, *groups) -> Optional[list]: """ Add groups to a user and update redis :param groups: Groups to add :return: list The user's groups """ from app.auth import userdb groups = list(filter(None, groups)) # groups = list(filter(valid_str_only, groups)) if not groups: return groups = await Group.filter(name__in=groups).only('id', 'name') if not groups: return await self.groups.add(*groups) names = await Group.filter(group_users__id=self.id) \ .values_list('name', flat=True) partialkey = s.CACHE_USERNAME.format(self.id) if user_dict := red.get(partialkey): user_dict = cache.restoreuser_dict(user_dict) user = userdb.usercomplete(**user_dict)
async def create_group(cls, name: str, summary: Optional[str] = ''): """ Create a group and update the cache """ if await Group.get_or_none(name=name): return group = await Group.create(name=name, summary=summary) # Update cache if groups := red.get('groups'): groups.append(name)
async def ab(): await tempdb() usermod_temp = await UserMod.get(email=VERIFIED_EMAIL_DEMO).only('id') await usermod_temp.get_and_cache(usermod_temp.id, model=True) partialkey = s.CACHE_USERNAME.format(usermod_temp.id) return red.get(partialkey)
async def get_permissions(self, perm_type: Optional[str] = None) -> list: """ Collate all the permissions a user has from groups + user :param perm_type: user or group :return: List of permission codes to match data with """ group_perms, user_perms = [], [] groups = await self.get_groups() if perm_type is None or perm_type == 'group': for group in groups: partialkey = s.CACHE_GROUPNAME.format(group) if perms := red.get(partialkey): group_perms += perms else: perms = Group.get_and_cache(group) group_perms += perms red.set(partialkey, perms)
async def get_permissions(cls, *groups, debug=False) -> Union[list, tuple]: """ Get a consolidated list of permissions for groups. Uses cache else query. :param groups: Names of groups :param debug: Return debug data for tests :return: List of permissions for that group """ debug = debug if s.DEBUG else False allperms, sources = set(), [] for group in groups: partialkey = s.CACHE_GROUPNAME.format(group) if perms := red.get(partialkey): sources.append('CACHE') else: sources.append('QUERY') perms = await cls.get_and_cache(group) # ic(group, perms) if perms: allperms.update(perms)
async def delete_group(cls, name: str): """ Delete a group :param name: Name of group :return: """ try: # Delete from db if name: if group := await Group.get_or_none(name=name).only('id'): await group.delete() # Update cache partialkey = s.CACHE_GROUPNAME.format(name) red.delete(partialkey) if groups := red.get('groups'): groups = list(filter(lambda y: y != name, groups)) else: groups = await Group.all().values_list('name', flat=True) red.set('groups', groups, clear=True) return True
async def get_perms(self) -> list: """ Get group + user perms from cache else query. :return: List of perms """ allperms = set() # Get group perms from cache for group in self.groups: # noqa group_partialkey = s.CACHE_GROUPNAME.format(group) if red.exists(group_partialkey): cached_perms = red.get(group_partialkey) allperms.update(cached_perms) else: queried_perms = await Group.get_and_cache(group) allperms.update(queried_perms) # Include any user perms if any allperms.update(self.permissions) # noqa return list(allperms)
async def get_and_cache(cls, group: str) -> list: """ Get a group's permissions and cache it for future use. Replaces data if exists. Only one group must be given so each can be cached separately. :param group: Group name :return: list """ perms = await Permission.filter(groups__name=group ).values_list('code', flat=True) perms = perms or [] if perms: # Save back to cache partialkey = s.CACHE_GROUPNAME.format(group) red.set(partialkey, perms, ttl=-1, clear=True) grouplist = red.exists('groups') and red.get('groups') or [] if group not in grouplist: grouplist.append(group) red.set('groups', grouplist, clear=True) return perms
async def get_data(cls, id, force_query=False, debug=False): """ Get the UserDBComplete data whether it be via cache or query. Checks cache first else query. :param force_query: Force use query instead of checking the cache :param id: User id :param debug: Debug data for tests :return: UserDBComplete/tuple or None """ from app.auth import userdb debug = debug if s.DEBUG else False partialkey = s.CACHE_USERNAME.format(id) if not force_query and red.exists(partialkey): source = 'CACHE' user_data = cache.restoreuser_dict(red.get(partialkey)) user = userdb.usercomplete(**user_data) else: source = 'QUERY' user = await UserMod.get_and_cache(id) if debug: return user, source return user
async def get_groups(self, force_query=False, debug=False) -> Union[list, tuple]: """ Return a user's groups as a list from the cache or not. Uses cache else query. :param force_query: Don't use cache :param debug: Return debug data for tests :return: List of groups if not debug """ from app.auth import userdb debug = debug if s.DEBUG else False partialkey = s.CACHE_USERNAME.format(self.id) if not force_query and red.exists(partialkey): user_dict = red.get(partialkey) source = 'CACHE' user_dict = cache.restoreuser_dict(user_dict) user = userdb.usercomplete(**user_dict) else: source = 'QUERY' user = await UserMod.get_and_cache(self.id) if debug: return user.groups, source return user.groups
async def get(self, id: UUID4) -> Optional[UD]: # noqa partialkey = s.CACHE_USERNAME.format(str(id)) if user_dict := red.get(partialkey): # ic('CACHE') user_dict = cache.restoreuser_dict(user_dict)
def test_get_permissions(tempdb, loop, groups, perms, remove, src): async def ab(): await tempdb() return await Group.get_permissions(*listify(groups), debug=True) groups = listify(groups) for idx, group in enumerate(groups): partialkey = s.CACHE_GROUPNAME.format(group) remove = listify(remove) if remove[idx]: red.delete(partialkey) assert not red.get(partialkey) assert not red.exists(partialkey) loop.run_until_complete(ab()) # ic(x) # allperms, sources = loop.run_until_complete(ab()) # assert Counter(allperms) == Counter(perms) # assert Counter(sources) == Counter(listify(src)) # param = [ # ('user.create', 'AdminGroup', True), # ('user.create', 'NoaddGroup', True), # ('page.create', 'ContentGroup', True), # ('page.create', 'NoaddGroup', False), # ('page.create', 'abc', False), # ('', 'abc', False), # ('page.create', '', False), # ] # @pytest.mark.parametrize('perm, group, out', param) # @pytest.mark.focus # def test_is_group(loop, perm, group, out): # async def ab(): # assert await Permission.is_group(perm, group) == out # loop.run_until_complete(ab()) # # @pytest.mark.focus # def test_abc(loop, tempdb): # from app.authentication import Option # # async def ab(): # await tempdb() # await Option.create(name='foo', value='bar') # opt = await Option.all() # ic(opt) # # loop.run_until_complete(ab())
def apik_of_uuid(uuid_): """Given a uuid (key) retrieves the db id""" return red.get(_KEY_UUID_APIK.format(uuid_))
class UserMod(DTMixin, TortoiseBaseUserModel): username = fields.CharField(max_length=50, null=True) first_name = fields.CharField(max_length=191, default='') middle_name = fields.CharField(max_length=191, default='') last_name = fields.CharField(max_length=191, default='') civil = fields.CharField(max_length=20, default='') bday = fields.DateField(null=True) mobile = fields.CharField(max_length=50, default='') telephone = fields.CharField(max_length=50, default='') avatar = fields.CharField(max_length=191, default='') status = fields.CharField(max_length=20, default='') bio = fields.CharField(max_length=191, default='') address1 = fields.CharField(max_length=191, default='') address2 = fields.CharField(max_length=191, default='') country = fields.CharField(max_length=2, default='') zipcode = fields.CharField(max_length=20, default='') timezone = fields.CharField(max_length=10, default='+00:00') website = fields.CharField(max_length=20, default='') last_login = fields.DatetimeField(null=True) groups = fields.ManyToManyField('models.Group', related_name='group_users', through='auth_user_groups', backward_key='user_id') permissions = fields.ManyToManyField('models.Permission', related_name='permission_users', through='auth_user_permissions', backward_key='user_id') full = Manager() class Meta: table = 'auth_user' manager = ActiveManager() def __str__(self): return modstr(self, 'id') @property def fullname(self): return f'{self.first_name} {self.last_name}'.strip() @property async def display_name(self): if self.username: return self.username elif self.fullname: return self.fullname.split()[0] else: emailname = self.email.split('@')[0] return ' '.join(emailname.split('.')) # @classmethod # def has_perm(cls, id: str, *perms): # partialkey = s.CACHE_USERNAME.format('id') # if red.exists(partialkey): # groups = red.get(partialkey).get('groups') async def to_dict(self, exclude: Optional[List[str]] = None, prefetch=False) -> dict: """ Converts a UserMod instance into UserModComplete. Included fields are based on UserDB + groups, options, and permissions. :param exclude: Fields not to explicitly include :param prefetch: Query used prefetch_related to save on db hits :return: UserDBComplete """ d = {} exclude = ['created_at', 'deleted_at', 'updated_at' ] if exclude is None else exclude for field in self._meta.db_fields: if hasattr(self, field) and field not in exclude: d[field] = getattr(self, field) if field == 'id': d[field] = str(d[field]) if hasattr(self, 'groups'): if prefetch: d['groups'] = [i.name for i in self.groups] else: d['groups'] = await self.groups.all().values_list('name', flat=True) if hasattr(self, 'options'): if prefetch: d['options'] = {i.name: i.value for i in self.options} else: d['options'] = { i.name: i.value for i in await self.options.all().only( 'id', 'name', 'value', 'is_active') if i.is_active } if hasattr(self, 'permissions'): if prefetch: d['permissions'] = [i.code for i in self.permissions] else: d['permissions'] = await self.permissions.all().values_list( 'code', flat=True) # ic(d) return d @classmethod async def get_and_cache(cls, id, model=False): """ Get a user's cachable data and cache it for future use. Replaces data if exists. Similar to the dependency current_user. :param id: User id as str :param model: Also return the UserMod instance :return: DOESN'T NEED cache.restoreuser() since data is from the db not redis. The id key in the hash is already formatted to a str from UUID. Can be None if user doesn't exist. """ from app.auth import userdb query = UserMod.get_or_none(pk=id) \ .prefetch_related( Prefetch('groups', queryset=Group.all().only('id', 'name')), Prefetch('options', queryset=Option.all().only('user_id', 'name', 'value')), Prefetch('permissions', queryset=Permission.filter(deleted_at=None).only('id', 'code')) ) if userdb.oauth_account_model is not None: query = query.prefetch_related("oauth_accounts") usermod = await query.only(*userdb.select_fields) if usermod: user_dict = await usermod.to_dict(prefetch=True) partialkey = s.CACHE_USERNAME.format(id) red.set(partialkey, cache.prepareuser_dict(user_dict), clear=True) if model: return userdb.usercomplete(**user_dict), usermod return userdb.usercomplete(**user_dict) @classmethod async def get_data(cls, id, force_query=False, debug=False): """ Get the UserDBComplete data whether it be via cache or query. Checks cache first else query. :param force_query: Force use query instead of checking the cache :param id: User id :param debug: Debug data for tests :return: UserDBComplete/tuple or None """ from app.auth import userdb debug = debug if s.DEBUG else False partialkey = s.CACHE_USERNAME.format(id) if not force_query and red.exists(partialkey): source = 'CACHE' user_data = cache.restoreuser_dict(red.get(partialkey)) user = userdb.usercomplete(**user_data) else: source = 'QUERY' user = await UserMod.get_and_cache(id) if debug: return user, source return user async def get_permissions(self, perm_type: Optional[str] = None) -> list: """ Collate all the permissions a user has from groups + user :param perm_type: user or group :return: List of permission codes to match data with """ group_perms, user_perms = [], [] groups = await self.get_groups() if perm_type is None or perm_type == 'group': for group in groups: partialkey = s.CACHE_GROUPNAME.format(group) if perms := red.get(partialkey): group_perms += perms else: perms = Group.get_and_cache(group) group_perms += perms red.set(partialkey, perms) if perm_type is None or perm_type == 'user': partialkey = s.CACHE_USERNAME.format(self.id) if user_dict := red.get(partialkey): user_dict = cache.restoreuser_dict(user_dict) user_perms = user_dict.get('permissions') else: user = await UserMod.get_and_cache(self.id) user_perms = user.permissions