From 0654f6dabf2c78c07b919f9674421c6cff5d5cd3 Mon Sep 17 00:00:00 2001
From: Thibault Dockx <thibault.dockx@fusiondirectory.org>
Date: Thu, 10 Apr 2025 14:48:06 +0100
Subject: [PATCH 1/3] :art: feat(audit) - implement AutomaticGroups endpoint
 for processing group assignments based on task criteria

---
 plugins/AutomaticGroups.php | 278 ++++++++++++++++++++++++++++++++++++
 1 file changed, 278 insertions(+)
 create mode 100644 plugins/AutomaticGroups.php

diff --git a/plugins/AutomaticGroups.php b/plugins/AutomaticGroups.php
new file mode 100644
index 0000000..4fed112
--- /dev/null
+++ b/plugins/AutomaticGroups.php
@@ -0,0 +1,278 @@
+<?php
+
+class AutomaticGroups implements EndpointInterface
+{
+  private TaskGateway $gateway;
+
+  public function __construct(TaskGateway $gateway)
+  {
+    $this->gateway = $gateway;
+  }
+
+  /**
+   * @return array
+   * Part of the interface of orchestrator plugin to treat GET method
+   */
+  public function processEndPointGet(): array
+  {
+    return $this->gateway->getObjectTypeTask('Automatic Groups');
+  }
+
+  /**
+   * @param array|null $data
+   * @return array
+   */
+  public function processEndPointPost(array $data = NULL): array
+  {
+    return [];
+  }
+
+  /**
+   * @param array|null $data
+   * @return array
+   * @throws Exception
+   */
+  public function processEndPointPatch(array $data = NULL): array
+  {
+    return $this->processAutomaticGroups($this->gateway->getObjectTypeTask('Automatic Groups'));
+  }
+
+  /**
+   * @param array|null $data
+   * @return array
+   */
+  public function processEndPointDelete(array $data = NULL): array
+  {
+    return [];
+  }
+
+  /**
+   * Process automatic group assignment tasks
+   * 
+   * @param array $automaticGroupsTasks
+   * @return array
+   * @throws Exception
+   */
+  public function processAutomaticGroups(array $automaticGroupsTasks): array
+  {
+    $result = [];
+    
+    if (empty($automaticGroupsTasks)) {
+      return ['No automatic groups tasks require processing.'];
+    }
+
+    foreach ($automaticGroupsTasks as $task) {
+      try {
+        // Check if task should be processed (status and schedule)
+        if (!$this->gateway->statusAndScheduleCheck($task)) {
+          continue;
+        }
+
+        // Get the DN of the user/group to process
+        $userDn = $task['fdtasksgranulardn'][0] ?? null;
+        if (empty($userDn)) {
+          throw new Exception("Missing user DN in task");
+        }
+
+        // Get main task configuration
+        $mainTaskConfig = $this->getAutomaticGroupsMainTask($task['fdtasksgranularmaster'][0]);
+        
+        // Get target group and resource/state criteria
+        $targetGroup = $mainTaskConfig[0]['fdtasksautomaticgroupsofname'][0] ?? null;
+        $resource = $mainTaskConfig[0]['fdtasksautomaticgroupsresource'][0] ?? null;
+        $state = $mainTaskConfig[0]['fdtasksautomaticgroupsstate'][0] ?? null;
+        $subState = $mainTaskConfig[0]['fdtasksautomaticgroupssubstate'][0] ?? null;
+
+        if (empty($targetGroup)) {
+          throw new Exception("Missing target group in task configuration");
+        }
+
+        // Check if user meets the criteria (if resource/state specified)
+        $shouldAddToGroup = true;
+        if ($resource !== 'NONE' && !empty($resource) && !empty($state)) {
+          $userSupannState = $this->getUserSupannState($userDn);
+          $shouldAddToGroup = $this->checkUserSupannState($userSupannState, $resource, $state, $subState);
+        }
+
+        // Add/remove user from group based on criteria
+        if ($shouldAddToGroup) {
+          $this->addUserToGroup($userDn, $targetGroup);
+          $result[$task['dn']]['result'] = "User $userDn successfully added to group $targetGroup";
+        } else {
+          $this->removeUserFromGroup($userDn, $targetGroup);
+          $result[$task['dn']]['result'] = "User $userDn doesn't meet criteria - removed from group $targetGroup";
+        }
+
+        // Update task status
+        $this->gateway->updateTaskStatus($task['dn'], $task['cn'][0], '2');
+      } catch (Exception $e) {
+        $result[$task['dn']]['result'] = "Error processing task: " . $e->getMessage();
+        $this->gateway->updateTaskStatus($task['dn'], $task['cn'][0], $e->getMessage());
+      }
+    }
+
+    return $result;
+  }
+
+  /**
+   * Get main task configuration
+   * 
+   * @param string $mainTaskDn
+   * @return array
+   */
+  private function getAutomaticGroupsMainTask(string $mainTaskDn): array
+  {
+    return $this->gateway->getLdapTasks(
+      '(objectClass=fdTasksAutomaticGroups)',
+      [
+        'fdTasksAutomaticGroupsOfName',
+        'fdTasksAutomaticGroupsResource',
+        'fdTasksAutomaticGroupsState',
+        'fdTasksAutomaticGroupsSubState'
+      ],
+      '',
+      $mainTaskDn
+    );
+  }
+
+  /**
+   * Get user's Supann state
+   * 
+   * @param string $userDn
+   * @return array
+   */
+  private function getUserSupannState(string $userDn): array
+  {
+    $result = $this->gateway->getLdapTasks(
+      '(objectClass=*)',
+      ['supannRessourceEtat'],
+      '',
+      $userDn
+    );
+
+    $this->gateway->unsetCountKeys($result);
+    return $result;
+  }
+
+  /**
+   * Check if user matches the required Supann state
+   * 
+   * @param array $userSupannState
+   * @param string $resource
+   * @param string $state
+   * @param string|null $subState
+   * @return bool
+   */
+  private function checkUserSupannState(array $userSupannState, string $resource, string $state, ?string $subState): bool
+  {
+    if (empty($userSupannState[0]['supannressourceetat'])) {
+      return false;
+    }
+
+    foreach ($userSupannState[0]['supannressourceetat'] as $value) {
+      // Create the expected format for comparison
+      $expectedState = '';
+      if (!empty($subState)) {
+        $expectedState = '{' . $resource . '}' . $state . ':' . $subState;
+      } else {
+        $expectedState = '{' . $resource . '}' . $state;
+      }
+      
+      if ($value === $expectedState) {
+        return true;
+      }
+    }
+    
+    return false;
+  }
+
+  /**
+   * Add user to LDAP group
+   * 
+   * @param string $userDn
+   * @param string $groupDn
+   * @return bool
+   * @throws Exception
+   */
+  private function addUserToGroup(string $userDn, string $groupDn): bool
+  {
+    // Get current group members
+    $groupInfo = $this->gateway->getLdapTasks(
+      '(objectClass=groupOfNames)',
+      ['member'],
+      '',
+      $groupDn
+    );
+
+    $this->gateway->unsetCountKeys($groupInfo);
+    $members = $groupInfo[0]['member'] ?? [];
+    
+    // If member is already in the group, nothing to do
+    if (in_array($userDn, $members)) {
+      return true;
+    }
+    
+    // Add member to the group
+    $members[] = $userDn;
+    $entry = ['member' => $members];
+    
+    // Update the group in LDAP
+    try {
+      $result = ldap_modify($this->gateway->ds, $groupDn, $entry);
+      if (!$result) {
+        throw new Exception("Failed to add $userDn to group $groupDn: " . ldap_error($this->gateway->ds));
+      }
+      return true;
+    } catch (Exception $e) {
+      throw new Exception("Error adding member to group: " . $e->getMessage());
+    }
+  }
+
+  /**
+   * Remove user from LDAP group
+   * 
+   * @param string $userDn
+   * @param string $groupDn
+   * @return bool
+   * @throws Exception
+   */
+  private function removeUserFromGroup(string $userDn, string $groupDn): bool
+  {
+    // Get current group members
+    $groupInfo = $this->gateway->getLdapTasks(
+      '(objectClass=groupOfNames)',
+      ['member'],
+      '',
+      $groupDn
+    );
+
+    $this->gateway->unsetCountKeys($groupInfo);
+    $members = $groupInfo[0]['member'] ?? [];
+    
+    // If member is not in the group, nothing to do
+    if (!in_array($userDn, $members)) {
+      return true;
+    }
+    
+    // Remove member from the group
+    $members = array_diff($members, [$userDn]);
+    
+    // Groups must have at least one member, so check if this would empty the group
+    if (empty($members)) {
+      return true; // Do nothing if it would empty the group
+    }
+    
+    $entry = ['member' => $members];
+    
+    // Update the group in LDAP
+    try {
+      $result = ldap_modify($this->gateway->ds, $groupDn, $entry);
+      if (!$result) {
+        throw new Exception("Failed to remove $userDn from group $groupDn: " . ldap_error($this->gateway->ds));
+      }
+      return true;
+    } catch (Exception $e) {
+      throw new Exception("Error removing member from group: " . $e->getMessage());
+    }
+  }
+}
\ No newline at end of file
-- 
GitLab


From dcc3580a6664d663b38b000250e288f7869d7a9e Mon Sep 17 00:00:00 2001
From: Thibault Dockx <thibault.dockx@fusiondirectory.org>
Date: Thu, 10 Apr 2025 14:48:44 +0100
Subject: [PATCH 2/3] :art: feat(audit) - add AutomaticGroups endpoint for
 processing automatic group assignments based on task criteria

---
 plugins/{ => tasks}/AutomaticGroups.php | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename plugins/{ => tasks}/AutomaticGroups.php (100%)

diff --git a/plugins/AutomaticGroups.php b/plugins/tasks/AutomaticGroups.php
similarity index 100%
rename from plugins/AutomaticGroups.php
rename to plugins/tasks/AutomaticGroups.php
-- 
GitLab


From 4c58fbbff80b99aabe289893377b07aed36d7b3b Mon Sep 17 00:00:00 2001
From: Thibault Dockx <thibault.dockx@fusiondirectory.org>
Date: Thu, 10 Apr 2025 16:24:25 +0100
Subject: [PATCH 3/3] :art: refactor(audit) - standardize spacing and
 formatting in AutomaticGroups class for improved readability

---
 plugins/tasks/AutomaticGroups.php | 86 +++++++++++++++----------------
 1 file changed, 43 insertions(+), 43 deletions(-)

diff --git a/plugins/tasks/AutomaticGroups.php b/plugins/tasks/AutomaticGroups.php
index 4fed112..b883e99 100644
--- a/plugins/tasks/AutomaticGroups.php
+++ b/plugins/tasks/AutomaticGroups.php
@@ -4,7 +4,7 @@ class AutomaticGroups implements EndpointInterface
 {
   private TaskGateway $gateway;
 
-  public function __construct(TaskGateway $gateway)
+  public function __construct (TaskGateway $gateway)
   {
     $this->gateway = $gateway;
   }
@@ -13,7 +13,7 @@ class AutomaticGroups implements EndpointInterface
    * @return array
    * Part of the interface of orchestrator plugin to treat GET method
    */
-  public function processEndPointGet(): array
+  public function processEndPointGet (): array
   {
     return $this->gateway->getObjectTypeTask('Automatic Groups');
   }
@@ -22,7 +22,7 @@ class AutomaticGroups implements EndpointInterface
    * @param array|null $data
    * @return array
    */
-  public function processEndPointPost(array $data = NULL): array
+  public function processEndPointPost (array $data = NULL): array
   {
     return [];
   }
@@ -32,7 +32,7 @@ class AutomaticGroups implements EndpointInterface
    * @return array
    * @throws Exception
    */
-  public function processEndPointPatch(array $data = NULL): array
+  public function processEndPointPatch (array $data = NULL): array
   {
     return $this->processAutomaticGroups($this->gateway->getObjectTypeTask('Automatic Groups'));
   }
@@ -41,22 +41,22 @@ class AutomaticGroups implements EndpointInterface
    * @param array|null $data
    * @return array
    */
-  public function processEndPointDelete(array $data = NULL): array
+  public function processEndPointDelete (array $data = NULL): array
   {
     return [];
   }
 
   /**
    * Process automatic group assignment tasks
-   * 
+   *
    * @param array $automaticGroupsTasks
    * @return array
    * @throws Exception
    */
-  public function processAutomaticGroups(array $automaticGroupsTasks): array
+  public function processAutomaticGroups (array $automaticGroupsTasks): array
   {
     $result = [];
-    
+
     if (empty($automaticGroupsTasks)) {
       return ['No automatic groups tasks require processing.'];
     }
@@ -69,26 +69,26 @@ class AutomaticGroups implements EndpointInterface
         }
 
         // Get the DN of the user/group to process
-        $userDn = $task['fdtasksgranulardn'][0] ?? null;
+        $userDn = $task['fdtasksgranulardn'][0] ?? NULL;
         if (empty($userDn)) {
           throw new Exception("Missing user DN in task");
         }
 
         // Get main task configuration
         $mainTaskConfig = $this->getAutomaticGroupsMainTask($task['fdtasksgranularmaster'][0]);
-        
+
         // Get target group and resource/state criteria
-        $targetGroup = $mainTaskConfig[0]['fdtasksautomaticgroupsofname'][0] ?? null;
-        $resource = $mainTaskConfig[0]['fdtasksautomaticgroupsresource'][0] ?? null;
-        $state = $mainTaskConfig[0]['fdtasksautomaticgroupsstate'][0] ?? null;
-        $subState = $mainTaskConfig[0]['fdtasksautomaticgroupssubstate'][0] ?? null;
+        $targetGroup  = $mainTaskConfig[0]['fdtasksautomaticgroupsofname'][0] ?? NULL;
+        $resource     = $mainTaskConfig[0]['fdtasksautomaticgroupsresource'][0] ?? NULL;
+        $state        = $mainTaskConfig[0]['fdtasksautomaticgroupsstate'][0] ?? NULL;
+        $subState     = $mainTaskConfig[0]['fdtasksautomaticgroupssubstate'][0] ?? NULL;
 
         if (empty($targetGroup)) {
           throw new Exception("Missing target group in task configuration");
         }
 
         // Check if user meets the criteria (if resource/state specified)
-        $shouldAddToGroup = true;
+        $shouldAddToGroup = TRUE;
         if ($resource !== 'NONE' && !empty($resource) && !empty($state)) {
           $userSupannState = $this->getUserSupannState($userDn);
           $shouldAddToGroup = $this->checkUserSupannState($userSupannState, $resource, $state, $subState);
@@ -116,11 +116,11 @@ class AutomaticGroups implements EndpointInterface
 
   /**
    * Get main task configuration
-   * 
+   *
    * @param string $mainTaskDn
    * @return array
    */
-  private function getAutomaticGroupsMainTask(string $mainTaskDn): array
+  private function getAutomaticGroupsMainTask (string $mainTaskDn): array
   {
     return $this->gateway->getLdapTasks(
       '(objectClass=fdTasksAutomaticGroups)',
@@ -137,11 +137,11 @@ class AutomaticGroups implements EndpointInterface
 
   /**
    * Get user's Supann state
-   * 
+   *
    * @param string $userDn
    * @return array
    */
-  private function getUserSupannState(string $userDn): array
+  private function getUserSupannState (string $userDn): array
   {
     $result = $this->gateway->getLdapTasks(
       '(objectClass=*)',
@@ -156,17 +156,17 @@ class AutomaticGroups implements EndpointInterface
 
   /**
    * Check if user matches the required Supann state
-   * 
+   *
    * @param array $userSupannState
    * @param string $resource
    * @param string $state
    * @param string|null $subState
    * @return bool
    */
-  private function checkUserSupannState(array $userSupannState, string $resource, string $state, ?string $subState): bool
+  private function checkUserSupannState (array $userSupannState, string $resource, string $state, ?string $subState): bool
   {
     if (empty($userSupannState[0]['supannressourceetat'])) {
-      return false;
+      return FALSE;
     }
 
     foreach ($userSupannState[0]['supannressourceetat'] as $value) {
@@ -177,24 +177,24 @@ class AutomaticGroups implements EndpointInterface
       } else {
         $expectedState = '{' . $resource . '}' . $state;
       }
-      
+
       if ($value === $expectedState) {
-        return true;
+        return TRUE;
       }
     }
-    
-    return false;
+
+    return FALSE;
   }
 
   /**
    * Add user to LDAP group
-   * 
+   *
    * @param string $userDn
    * @param string $groupDn
    * @return bool
    * @throws Exception
    */
-  private function addUserToGroup(string $userDn, string $groupDn): bool
+  private function addUserToGroup (string $userDn, string $groupDn): bool
   {
     // Get current group members
     $groupInfo = $this->gateway->getLdapTasks(
@@ -206,23 +206,23 @@ class AutomaticGroups implements EndpointInterface
 
     $this->gateway->unsetCountKeys($groupInfo);
     $members = $groupInfo[0]['member'] ?? [];
-    
+
     // If member is already in the group, nothing to do
     if (in_array($userDn, $members)) {
-      return true;
+      return TRUE;
     }
-    
+
     // Add member to the group
     $members[] = $userDn;
     $entry = ['member' => $members];
-    
+
     // Update the group in LDAP
     try {
       $result = ldap_modify($this->gateway->ds, $groupDn, $entry);
       if (!$result) {
         throw new Exception("Failed to add $userDn to group $groupDn: " . ldap_error($this->gateway->ds));
       }
-      return true;
+      return TRUE;
     } catch (Exception $e) {
       throw new Exception("Error adding member to group: " . $e->getMessage());
     }
@@ -230,13 +230,13 @@ class AutomaticGroups implements EndpointInterface
 
   /**
    * Remove user from LDAP group
-   * 
+   *
    * @param string $userDn
    * @param string $groupDn
    * @return bool
    * @throws Exception
    */
-  private function removeUserFromGroup(string $userDn, string $groupDn): bool
+  private function removeUserFromGroup (string $userDn, string $groupDn): bool
   {
     // Get current group members
     $groupInfo = $this->gateway->getLdapTasks(
@@ -248,29 +248,29 @@ class AutomaticGroups implements EndpointInterface
 
     $this->gateway->unsetCountKeys($groupInfo);
     $members = $groupInfo[0]['member'] ?? [];
-    
+
     // If member is not in the group, nothing to do
     if (!in_array($userDn, $members)) {
-      return true;
+      return TRUE;
     }
-    
+
     // Remove member from the group
     $members = array_diff($members, [$userDn]);
-    
+
     // Groups must have at least one member, so check if this would empty the group
     if (empty($members)) {
-      return true; // Do nothing if it would empty the group
+      return TRUE; // Do nothing if it would empty the group
     }
-    
+
     $entry = ['member' => $members];
-    
+
     // Update the group in LDAP
     try {
       $result = ldap_modify($this->gateway->ds, $groupDn, $entry);
       if (!$result) {
         throw new Exception("Failed to remove $userDn from group $groupDn: " . ldap_error($this->gateway->ds));
       }
-      return true;
+      return TRUE;
     } catch (Exception $e) {
       throw new Exception("Error removing member from group: " . $e->getMessage());
     }
-- 
GitLab