diff --git a/aleksis/core/mixins.py b/aleksis/core/mixins.py
index 8bff9408655273aaad6b05901f8f1dd3e38706c1..88a01c15cf28489fde10c788895a35758bf857db 100644
--- a/aleksis/core/mixins.py
+++ b/aleksis/core/mixins.py
@@ -43,6 +43,8 @@ class _ExtensibleModelBase(models.base.ModelBase):
             # Register all non-abstract models with django-reversion
             mcls = reversion.register(mcls)
 
+            mcls.extra_permissions = []
+
         return mcls
 
 
@@ -100,6 +102,8 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
     objects = CurrentSiteManager()
     objects_all_sites = models.Manager()
 
+    extra_permissions = []
+    
     def get_absolute_url(self) -> str:
         """Get the URL o a view representing this model instance."""
         pass
@@ -226,6 +230,11 @@ class ExtensibleModel(models.Model, metaclass=_ExtensibleModelBase):
         """Collect all fields that can be synced on a model."""
         return lazy(cls.syncable_fields_choices, tuple)
 
+    @classmethod
+    def add_permission(cls, name: str, verbose_name: str):
+        """Dynamically add a new permission to a model."""
+        cls.extra_permissions.append((name, verbose_name))
+
     class Meta:
         abstract = True
 
diff --git a/aleksis/core/util/apps.py b/aleksis/core/util/apps.py
index d263feb9a2ba0c9e36d821a56e12f77726a7a672..1d2b76881e0ca9d04ed972200dde6ab229687c42 100644
--- a/aleksis/core/util/apps.py
+++ b/aleksis/core/util/apps.py
@@ -189,6 +189,9 @@ class AppConfig(django.apps.AppConfig):
         pass
 
     def _maintain_default_data(self):
+        from django.contrib.auth.models import Permission
+        from django.contrib.contenttypes.models import ContentType
+
         if not self.models_module:
             # This app does not have any models, so bail out early
             return
@@ -197,3 +200,12 @@ class AppConfig(django.apps.AppConfig):
             if hasattr(model, "maintain_default_data"):
                 # Method implemented by each model object; can be left out
                 model.maintain_default_data()
+            if hasattr(model, "extra_permissions"):
+                ct = ContentType.objects.get_for_model(model)
+                for perm, verbose_name in model.extra_permissions:
+                    Permission.objects.get_or_create(
+                        codename=perm,
+                        name=verbose_name,
+                        content_type=ct
+                    )
+