Repository: netbox-community/netbox

Issue:
Auto-create remote groups
### NetBox version

v3.0.8

### Feature type

Change to existing functionality

### Proposed functionality

Allow NetBox to auto-create groups referenced in the `REMOTE_AUTH_GROUP_HEADER`.

### Use case

As a data center administrator using Keycloak to manage auth/authz, I would like NetBox to automatically instantiate groups provided by Keycloak when using `REMOTE_AUTH_ENABLED`.

With `REMOTE_AUTH_ENABLED` and `REMOTE_AUTH_GROUP_SYNC_ENABLED`, it's still necessary to manually create groups before the group sync is effective.  I would like to see a `REMOTE_AUTH_AUTO_CREATE_GROUPS` setting, analagous to `REMOTE_AUTH_AUTO_CREATE_USER` but for groups, that would cause NetBox to automatically create any groups in the `REMOTE_AUTH_GROUP_HEADER`. This would allow authorization to be completely delegated to the identity management platform.

This is the same feature requested by [this comment](https://github.com/netbox-community/netbox/issues/5775#issuecomment-883651758) in #5775.


### Database changes

_No response_

### External dependencies

_No response_


Discussion:

Resolving patch:
diff --git a/netbox/netbox/authentication.py b/netbox/netbox/authentication.py
--- a/netbox/netbox/authentication.py
+++ b/netbox/netbox/authentication.py
@@ -156,8 +156,11 @@ def configure_groups(self, user, remote_groups):
             try:
                 group_list.append(Group.objects.get(name=name))
             except Group.DoesNotExist:
-                logging.error(
-                    f"Could not assign group {name} to remotely-authenticated user {user}: Group not found")
+                if settings.REMOTE_AUTH_AUTO_CREATE_GROUPS:
+                    group_list.append(Group.objects.create(name=name))
+                else:
+                    logging.error(
+                        f"Could not assign group {name} to remotely-authenticated user {user}: Group not found")
         if group_list:
             user.groups.set(group_list)
             logger.debug(
diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py
--- a/netbox/netbox/settings.py
+++ b/netbox/netbox/settings.py
@@ -122,6 +122,7 @@
 QUEUE_MAPPINGS = getattr(configuration, 'QUEUE_MAPPINGS', {})
 RELEASE_CHECK_URL = getattr(configuration, 'RELEASE_CHECK_URL', None)
 REMOTE_AUTH_AUTO_CREATE_USER = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_USER', False)
+REMOTE_AUTH_AUTO_CREATE_GROUPS = getattr(configuration, 'REMOTE_AUTH_AUTO_CREATE_GROUPS', False)
 REMOTE_AUTH_BACKEND = getattr(configuration, 'REMOTE_AUTH_BACKEND', 'netbox.authentication.RemoteUserBackend')
 REMOTE_AUTH_DEFAULT_GROUPS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_GROUPS', [])
 REMOTE_AUTH_DEFAULT_PERMISSIONS = getattr(configuration, 'REMOTE_AUTH_DEFAULT_PERMISSIONS', {})


Test patch:
diff --git a/netbox/netbox/tests/test_authentication.py b/netbox/netbox/tests/test_authentication.py
--- a/netbox/netbox/tests/test_authentication.py
+++ b/netbox/netbox/tests/test_authentication.py
@@ -310,6 +310,50 @@ def test_remote_auth_remote_groups_default(self):
             list(new_user.groups.all())
         )
 
+    @override_settings(
+        REMOTE_AUTH_ENABLED=True,
+        REMOTE_AUTH_AUTO_CREATE_USER=True,
+        REMOTE_AUTH_GROUP_SYNC_ENABLED=True,
+        REMOTE_AUTH_AUTO_CREATE_GROUPS=True,
+        LOGIN_REQUIRED=True,
+    )
+    def test_remote_auth_remote_groups_autocreate(self):
+        """
+        Test enabling remote authentication with group sync and autocreate
+        enabled with the default configuration.
+        """
+        headers = {
+            "HTTP_REMOTE_USER": "remoteuser2",
+            "HTTP_REMOTE_USER_GROUP": "Group 1|Group 2",
+        }
+
+        self.assertTrue(settings.REMOTE_AUTH_ENABLED)
+        self.assertTrue(settings.REMOTE_AUTH_AUTO_CREATE_USER)
+        self.assertTrue(settings.REMOTE_AUTH_AUTO_CREATE_GROUPS)
+        self.assertTrue(settings.REMOTE_AUTH_GROUP_SYNC_ENABLED)
+        self.assertEqual(settings.REMOTE_AUTH_HEADER, "HTTP_REMOTE_USER")
+        self.assertEqual(settings.REMOTE_AUTH_GROUP_HEADER, "HTTP_REMOTE_USER_GROUP")
+        self.assertEqual(settings.REMOTE_AUTH_GROUP_SEPARATOR, "|")
+
+        groups = (
+            Group(name="Group 1"),
+            Group(name="Group 2"),
+        )
+
+        response = self.client.get(reverse("home"), follow=True, **headers)
+        self.assertEqual(response.status_code, 200)
+
+        new_user = User.objects.get(username="remoteuser2")
+        self.assertEqual(
+            int(self.client.session.get("_auth_user_id")),
+            new_user.pk,
+            msg="Authentication failed",
+        )
+        self.assertListEqual(
+            [group.name for group in groups],
+            [group.name for group in list(new_user.groups.all())],
+        )
+
     @override_settings(
         REMOTE_AUTH_ENABLED=True,
         REMOTE_AUTH_AUTO_CREATE_USER=True,