mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +00:00 
			
		
		
		
	Fix user role caching issues (#8973)
* Fix user role caching issues * Handle null user case * Fix typo * More spelling fixes
This commit is contained in:
		@@ -321,7 +321,7 @@ class StockLocationFilter(rest_filters.FilterSet):
 | 
			
		||||
    def filter_parent(self, queryset, name, value):
 | 
			
		||||
        """Filter by parent location.
 | 
			
		||||
 | 
			
		||||
        Note that the filtering behaviour here varies,
 | 
			
		||||
        Note that the filtering behavior here varies,
 | 
			
		||||
        depending on whether the 'cascade' value is set.
 | 
			
		||||
 | 
			
		||||
        So, we have to check the "cascade" value here.
 | 
			
		||||
 
 | 
			
		||||
@@ -328,12 +328,12 @@ def default_delete_on_deplete():
 | 
			
		||||
    """Return a default value for the 'delete_on_deplete' field.
 | 
			
		||||
 | 
			
		||||
    Prior to 2022-12-24, this field was set to True by default.
 | 
			
		||||
    Now, there is a user-configurable setting to govern default behaviour.
 | 
			
		||||
    Now, there is a user-configurable setting to govern default behavior.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        return get_global_setting('STOCK_DELETE_DEPLETED_DEFAULT', True)
 | 
			
		||||
    except (IntegrityError, OperationalError):
 | 
			
		||||
        # Revert to original default behaviour
 | 
			
		||||
        # Revert to original default behavior
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -561,7 +561,7 @@ class StockItemListTest(StockAPITestCase):
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(response.status_code, status.HTTP_200_OK)
 | 
			
		||||
 | 
			
		||||
        # Return JSON-ified data
 | 
			
		||||
        # Return JSON data
 | 
			
		||||
        return response.data
 | 
			
		||||
 | 
			
		||||
    def test_top_level_filtering(self):
 | 
			
		||||
@@ -1305,7 +1305,7 @@ class StockItemTest(StockAPITestCase):
 | 
			
		||||
            expected_code=201,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def test_stock_item_create_withsupplierpart(self):
 | 
			
		||||
    def test_stock_item_create_with_supplier_part(self):
 | 
			
		||||
        """Test creation of a StockItem via the API, including SupplierPart data."""
 | 
			
		||||
        # POST with non-existent supplier part
 | 
			
		||||
        response = self.post(
 | 
			
		||||
@@ -1543,7 +1543,7 @@ class StockItemTest(StockAPITestCase):
 | 
			
		||||
        self.assertEqual(data['purchase_price_currency'], 'NZD')
 | 
			
		||||
 | 
			
		||||
    def test_install(self):
 | 
			
		||||
        """Test that stock item can be installed into antoher item, via the API."""
 | 
			
		||||
        """Test that stock item can be installed into another item, via the API."""
 | 
			
		||||
        # Select the "parent" stock item
 | 
			
		||||
        parent_part = part.models.Part.objects.get(pk=100)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,6 @@ from django.contrib.auth import get_user_model
 | 
			
		||||
from django.contrib.auth.models import Group, Permission, User
 | 
			
		||||
from django.contrib.contenttypes.fields import GenericForeignKey
 | 
			
		||||
from django.contrib.contenttypes.models import ContentType
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.core.validators import MinLengthValidator
 | 
			
		||||
from django.db import models
 | 
			
		||||
from django.db.models import Q, UniqueConstraint
 | 
			
		||||
@@ -21,6 +20,7 @@ from django.utils.translation import gettext_lazy as _
 | 
			
		||||
import structlog
 | 
			
		||||
from rest_framework.authtoken.models import Token as AuthToken
 | 
			
		||||
 | 
			
		||||
import InvenTree.cache
 | 
			
		||||
import InvenTree.helpers
 | 
			
		||||
import InvenTree.models
 | 
			
		||||
from common.settings import get_global_setting
 | 
			
		||||
@@ -666,28 +666,20 @@ def update_group_roles(group, debug=False):
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def clear_user_role_cache(user: User):
 | 
			
		||||
    """Remove user role permission information from the cache.
 | 
			
		||||
 | 
			
		||||
    - This function is called whenever the user / group is updated
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        user: The User object to be expunged from the cache
 | 
			
		||||
    """
 | 
			
		||||
    for role in RuleSet.get_ruleset_models():
 | 
			
		||||
        for perm in ['add', 'change', 'view', 'delete']:
 | 
			
		||||
            key = f'role_{user.pk}_{role}_{perm}'
 | 
			
		||||
            cache.delete(key)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_user_permission(user: User, model, permission):
 | 
			
		||||
def check_user_permission(user: User, model, permission) -> bool:
 | 
			
		||||
    """Check if the user has a particular permission against a given model type.
 | 
			
		||||
 | 
			
		||||
    Arguments:
 | 
			
		||||
        user: The user object to check
 | 
			
		||||
        model: The model class to check (e.g. Part)
 | 
			
		||||
        model: The model class to check (e.g. 'part')
 | 
			
		||||
        permission: The permission to check (e.g. 'view' / 'delete')
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        bool: True if the user has the specified permission
 | 
			
		||||
    """
 | 
			
		||||
    if not user:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    if user.is_superuser:
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
@@ -695,21 +687,26 @@ def check_user_permission(user: User, model, permission):
 | 
			
		||||
    return user.has_perm(permission_name)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_user_role(user: User, role, permission):
 | 
			
		||||
def check_user_role(user: User, role, permission) -> bool:
 | 
			
		||||
    """Check if a user has a particular role:permission combination.
 | 
			
		||||
 | 
			
		||||
    If the user is a superuser, this will return True
 | 
			
		||||
    Arguments:
 | 
			
		||||
        user: The user object to check
 | 
			
		||||
        role: The role to check (e.g. 'part' / 'stock')
 | 
			
		||||
        permission: The permission to check (e.g. 'view' / 'delete')
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        bool: True if the user has the specified role:permission combination
 | 
			
		||||
    """
 | 
			
		||||
    if not user:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    if user.is_superuser:
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    # First, check the cache
 | 
			
		||||
    key = f'role_{user.pk}_{role}_{permission}'
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        result = cache.get(key)
 | 
			
		||||
    except Exception:  # pragma: no cover
 | 
			
		||||
        result = None
 | 
			
		||||
    # First, check the session cache
 | 
			
		||||
    cache_key = f'role_{user.pk}_{role}_{permission}'
 | 
			
		||||
    result = InvenTree.cache.get_session_cache(cache_key)
 | 
			
		||||
 | 
			
		||||
    if result is not None:
 | 
			
		||||
        return result
 | 
			
		||||
@@ -736,11 +733,8 @@ def check_user_role(user: User, role, permission):
 | 
			
		||||
                    result = True
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
    # Save result to cache
 | 
			
		||||
    try:
 | 
			
		||||
        cache.set(key, result, timeout=3600)
 | 
			
		||||
    except Exception:  # pragma: no cover
 | 
			
		||||
        pass
 | 
			
		||||
    # Save result to session-cache
 | 
			
		||||
    InvenTree.cache.set_session_cache(cache_key, result)
 | 
			
		||||
 | 
			
		||||
    return result
 | 
			
		||||
 | 
			
		||||
@@ -923,12 +917,6 @@ def delete_owner(sender, instance, **kwargs):
 | 
			
		||||
    owner.delete()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(post_save, sender=get_user_model(), dispatch_uid='clear_user_cache')
 | 
			
		||||
def clear_user_cache(sender, instance, **kwargs):
 | 
			
		||||
    """Callback function when a user object is saved."""
 | 
			
		||||
    clear_user_role_cache(instance)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@receiver(post_save, sender=Group, dispatch_uid='create_missing_rule_sets')
 | 
			
		||||
def create_missing_rule_sets(sender, instance, **kwargs):
 | 
			
		||||
    """Called *after* a Group object is saved.
 | 
			
		||||
@@ -936,6 +924,3 @@ def create_missing_rule_sets(sender, instance, **kwargs):
 | 
			
		||||
    As the linked RuleSet instances are saved *before* the Group, then we can now use these RuleSet values to update the group permissions.
 | 
			
		||||
    """
 | 
			
		||||
    update_group_roles(instance)
 | 
			
		||||
 | 
			
		||||
    for user in get_user_model().objects.filter(groups__name=instance.name):
 | 
			
		||||
        clear_user_role_cache(user)
 | 
			
		||||
 
 | 
			
		||||
@@ -206,7 +206,7 @@ class UserTokenTests(InvenTreeAPITestCase):
 | 
			
		||||
 | 
			
		||||
        self.client.get(me, expected_code=200)
 | 
			
		||||
 | 
			
		||||
    def test_buildin_token(self):
 | 
			
		||||
    def test_builtin_token(self):
 | 
			
		||||
        """Test the built-in token authentication."""
 | 
			
		||||
        response = self.post(
 | 
			
		||||
            reverse('rest_login'),
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user