diff --git a/aleksis/core/managers.py b/aleksis/core/managers.py
index aed4cab72d4b0cd53d3544b20f0180a7087dfd2c..a3feacc6cf17731a6d6f38ca5ace42677ed230f1 100644
--- a/aleksis/core/managers.py
+++ b/aleksis/core/managers.py
@@ -7,6 +7,7 @@ from django.db.models import QuerySet
 from django.db.models.manager import Manager
 
 from calendarweek import CalendarWeek
+from django_cte import CTEManager, CTEQuerySet
 from polymorphic.managers import PolymorphicManager
 
 
@@ -84,7 +85,7 @@ class SchoolTermRelatedQuerySet(QuerySet):
             return None
 
 
-class GroupManager(CurrentSiteManagerWithoutMigrations):
+class GroupManager(CurrentSiteManagerWithoutMigrations, CTEManager):
     """Manager adding specific methods to groups."""
 
     def get_queryset(self):
@@ -92,7 +93,7 @@ class GroupManager(CurrentSiteManagerWithoutMigrations):
         return super().get_queryset().select_related("school_term")
 
 
-class GroupQuerySet(SchoolTermRelatedQuerySet):
+class GroupQuerySet(SchoolTermRelatedQuerySet, CTEQuerySet):
     pass
 
 
diff --git a/aleksis/core/models.py b/aleksis/core/models.py
index 73ffab18ab217e1698ccc4fbdea9b25c1de97dfb..ee50301394b0f12444bb06262b1bf17ec0a22d62 100644
--- a/aleksis/core/models.py
+++ b/aleksis/core/models.py
@@ -28,6 +28,7 @@ import jsonstore
 from cachalot.api import cachalot_disabled
 from cache_memoize import cache_memoize
 from django_celery_results.models import TaskResult
+from django_cte import CTEQuerySet, With
 from dynamic_preferences.models import PerInstancePreferenceModel
 from invitations import signals
 from invitations.adapters import get_invitations_adapter
@@ -486,6 +487,25 @@ class Group(SchoolTermRelatedExtensibleModel):
 
         return stats
 
+    @property
+    def parent_groups_recursive(self) -> CTEQuerySet:
+        """Get all parent groups recursively."""
+
+        def _make_cte(cte):
+            Through = self.parent_groups.through
+            return (
+                Through.objects.values("to_group_id")
+                .filter(from_group=self)
+                .union(cte.join(Through, from_group=cte.col.to_group_id), all=True)
+            )
+
+        cte = With.recursive(_make_cte)
+        return cte.join(Group, id=cte.col.to_group_id).with_cte(cte)
+
+    @property
+    def child_groups_recursive(self) -> CTEQuerySet:
+        """Get all child groups recursively."""
+
     def __str__(self) -> str:
         if self.school_term:
             return f"{self.name} ({self.short_name}) ({self.school_term})"
diff --git a/pyproject.toml b/pyproject.toml
index 357eff863631c377b33777678a28dba88fb81f0d..fd960153b97c4b078931eb9890d38af9cb746152 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -116,6 +116,7 @@ django-titofisto = "^0.2.0"
 haystack-redis = "^0.0.1"
 python-gnupg = "^0.4.7"
 sentry-sdk = {version = "^1.4.3", optional = true}
+django-cte = "^1.1.5"
 
 [tool.poetry.extras]
 ldap = ["django-auth-ldap"]