From 5676b1a4680dbe706686f38902f5607ec33330ff Mon Sep 17 00:00:00 2001
From: June <june@jsts.xyz>
Date: Tue, 14 Jan 2025 20:49:14 +0100
Subject: [PATCH] netbox: configure and patch NetBox for OIDC group and role
 mapping

The custom pipeline code is licensed under the Creative Commons: CC
BY-SA 4.0 license.

See:
https://github.com/goauthentik/authentik/blob/main/LICENSE
https://github.com/goauthentik/authentik/blob/main/website/integrations/services/netbox/index.md
https://docs.goauthentik.io/integrations/services/netbox/
---
 README.md                                     |  3 +-
 config/hosts/netbox/netbox.nix                | 21 ++++++-
 flake.nix                                     |  8 +++
 ...oup_and_role_mapping_custom_pipeline.patch | 61 +++++++++++++++++++
 4 files changed, 91 insertions(+), 2 deletions(-)
 create mode 100644 patches/0001_oidc_group_and_role_mapping_custom_pipeline.patch

diff --git a/README.md b/README.md
index 186f14a..def4e60 100644
--- a/README.md
+++ b/README.md
@@ -76,4 +76,5 @@ nix build .#proxmox-chaosknoten-nixos-template
 
 ## License
 
-This CCCHH nix-infra repository is licensed under the [MIT License](./LICENSE).
+This CCCHH nix-infra repository is licensed under the [MIT License](./LICENSE).  
+[`0001_oidc_group_and_role_mapping_custom_pipeline.patch`](patches/0001_oidc_group_and_role_mapping_custom_pipeline.patch) is licensed under the Creative Commons: CC BY-SA 4.0 license.
diff --git a/config/hosts/netbox/netbox.nix b/config/hosts/netbox/netbox.nix
index e0f2df9..f816016 100644
--- a/config/hosts/netbox/netbox.nix
+++ b/config/hosts/netbox/netbox.nix
@@ -9,7 +9,8 @@
 {
   services.netbox = {
     enable = true;
-    package = pkgs.netbox;
+    # Explicitly use the patched NetBox package.
+    package = pkgs.netbox_4_1;
     secretKeyFile = "/run/secrets/netbox_secret_key";
     keycloakClientSecret = "/run/secrets/netbox_keycloak_secret";
     settings = {
@@ -24,6 +25,24 @@
       SOCIAL_AUTH_KEYCLOAK_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi/Shi+b2OyYNGVFPsa6qf9SesEpRl5U5rpwgmt8H7NawMvwpPUYVW9o46QW0ulYcDmysT3BzpP3tagO/SFNoOjZdYe0D9nJ7vEp8KHbzR09KCfkyQIi0wLssKnDotVHL5JeUY+iKk+gjiwF9FSFSHPBqsST7hXVAut9LkOvs2aDod9AzbTH/uYbt4wfUm5l/1Ii8D+K7YcsFGUIqxv4XS/ylKqObqN4M2dac69iIwapoh6reaBQEm66vrOzJ+3yi4DZuPrkShJqi2hddtoyZihyCkF+eJJKEI5LrBf1KZB3Ec2YUrqk93ZGUGs/XY6R87QSfR3hJ82B1wnF+c2pw+QIDAQAB";
       SOCIAL_AUTH_KEYCLOAK_AUTHORIZATION_URL = "https://id.hamburg.ccc.de/realms/ccchh/protocol/openid-connect/auth";
       SOCIAL_AUTH_KEYCLOAK_ACCESS_TOKEN_URL = "https://id.hamburg.ccc.de/realms/ccchh/protocol/openid-connect/token";
+      SOCIAL_AUTH_PIPELINE = [
+        # The default pipeline as can be found in:
+        # /nix/store/q2jsn56bgkj0nkz0j4w48x3klyn2x4gp-netbox-4.1.7/opt/netbox/netbox/netbox/settings.py
+        "social_core.pipeline.social_auth.social_details"
+        "social_core.pipeline.social_auth.social_uid"
+        "social_core.pipeline.social_auth.social_user"
+        "social_core.pipeline.user.get_username"
+        "social_core.pipeline.user.create_user"
+        "social_core.pipeline.social_auth.associate_user"
+        "netbox.authentication.user_default_groups_handler"
+        "social_core.pipeline.social_auth.load_extra_data"
+        "social_core.pipeline.user.user_details"
+        # Use custom pipeline functions patched in via netbox41OIDCMappingOverlay.
+        # See: https://docs.goauthentik.io/integrations/services/netbox/
+        "netbox.custom_pipeline.add_groups"
+        "netbox.custom_pipeline.remove_groups"
+        "netbox.custom_pipeline.set_roles"
+      ];
     };
   };
 
diff --git a/flake.nix b/flake.nix
index dd85023..fb4ed26 100644
--- a/flake.nix
+++ b/flake.nix
@@ -40,6 +40,13 @@
         proxmox-vm = ./config/proxmox-vm;
         prometheus-exporter = ./config/extra/prometheus-exporter.nix;
       };
+      overlays = {
+        netbox41OIDCMappingOverlay = final: prev: {
+          netbox_4_1 = prev.netbox_4_1.overrideAttrs (finalAttr: previousAttr: {
+            patches = previousAttr.patches ++ [ ./patches/0001_oidc_group_and_role_mapping_custom_pipeline.patch ];
+          });
+        };
+      };
       nixosConfigurations = {
         audio-hauptraum-kueche = nixpkgs.lib.nixosSystem {
           inherit system specialArgs;
@@ -85,6 +92,7 @@
             sops-nix.nixosModules.sops
             self.nixosModules.prometheus-exporter
             ./config/hosts/netbox
+            { nixpkgs.overlays = [ self.overlays.netbox41OIDCMappingOverlay ]; }
           ];
         };
 
diff --git a/patches/0001_oidc_group_and_role_mapping_custom_pipeline.patch b/patches/0001_oidc_group_and_role_mapping_custom_pipeline.patch
new file mode 100644
index 0000000..89f805a
--- /dev/null
+++ b/patches/0001_oidc_group_and_role_mapping_custom_pipeline.patch
@@ -0,0 +1,61 @@
+diff --git a/netbox/netbox/custom_pipeline.py b/netbox/netbox/custom_pipeline.py
+new file mode 100644
+index 000000000..470f388dc
+--- /dev/null
++++ b/netbox/netbox/custom_pipeline.py
+@@ -0,0 +1,55 @@
++# Licensed under Creative Commons: CC BY-SA 4.0 license.
++# https://github.com/goauthentik/authentik/blob/main/LICENSE
++# https://github.com/goauthentik/authentik/blob/main/website/integrations/services/netbox/index.md
++# https://docs.goauthentik.io/integrations/services/netbox/
++from netbox.authentication import Group
++
++class AuthFailed(Exception):
++    pass
++
++def add_groups(response, user, backend, *args, **kwargs):
++    try:
++        groups = response['groups']
++    except KeyError:
++        pass
++
++    # Add all groups from oAuth token
++    for group in groups:
++        group, created = Group.objects.get_or_create(name=group)
++        user.groups.add(group)
++
++def remove_groups(response, user, backend, *args, **kwargs):
++    try:
++        groups = response['groups']
++    except KeyError:
++        # Remove all groups if no groups in oAuth token
++        user.groups.clear()
++        pass
++
++    # Get all groups of user
++    user_groups = [item.name for item in user.groups.all()]
++    # Get groups of user which are not part of oAuth token
++    delete_groups = list(set(user_groups) - set(groups))
++
++    # Delete non oAuth token groups
++    for delete_group in delete_groups:
++        group = Group.objects.get(name=delete_group)
++        user.groups.remove(group)
++
++
++def set_roles(response, user, backend, *args, **kwargs):
++    # Remove Roles temporary
++    user.is_superuser = False
++    user.is_staff = False
++    try:
++        groups = response['groups']
++    except KeyError:
++        # When no groups are set
++        # save the user without Roles
++        user.save()
++        pass
++
++    # Set roles is role (superuser or staff) is in groups
++    user.is_superuser = True if 'superusers' in groups else False
++    user.is_staff = True if 'staff' in groups else False
++    user.save()