From 210363335d0bf1faf9c2383200f8e7e08a52f13b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=B4me=20Chilliet?= <come.chilliet@fusiondirectory.org>
Date: Thu, 6 Feb 2020 10:17:15 +0100
Subject: [PATCH] :tractor: fix(acl) Refactor and fix of ACL checking system

Should fix support for base ACLs, and improve contsistence and
 speed of ACL checks.

issue #5949
---
 include/class_ACLPermissions.inc |  84 +++++++++
 include/class_acl.inc            |   6 +-
 include/class_userinfo.inc       | 308 +++++++++----------------------
 3 files changed, 171 insertions(+), 227 deletions(-)
 create mode 100644 include/class_ACLPermissions.inc

diff --git a/include/class_ACLPermissions.inc b/include/class_ACLPermissions.inc
new file mode 100644
index 000000000..253c50312
--- /dev/null
+++ b/include/class_ACLPermissions.inc
@@ -0,0 +1,84 @@
+<?php
+/*
+  This code is part of FusionDirectory (http://www.fusiondirectory.org/)
+  Copyright (C) 2019-2020  FusionDirectory
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program; if not, write to the Free Software
+  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+class ACLPermissions
+{
+  static protected $letters = [
+    'r' => 'read',
+    'w' => 'write',
+    'c' => 'create',
+    'd' => 'delete',
+    'm' => 'move',
+  ];
+
+  protected $read;
+  protected $write;
+  protected $create;
+  protected $delete;
+  protected $move;
+
+  /* Rights on self only */
+  protected $self;
+
+  public function __construct (string $rights = '')
+  {
+    foreach (static::$letters as $letter => $var) {
+      $this->$var = (strpos($rights, $letter) !== FALSE);
+    }
+    $this->self = (strpos($rights, 's') !== FALSE);
+  }
+
+  public function toString (bool $readOnly = FALSE): string
+  {
+    $string = ($this->self ? 's' : '');
+    if ($readOnly) {
+      return $string.($this->read ? 'r' : '');
+    } else {
+      foreach (static::$letters as $letter => $var) {
+        if ($this->$var) {
+          $string .= $letter;
+        }
+      }
+      return $string;
+    }
+  }
+
+  public function __toString ()
+  {
+    return $this->toString(FALSE);
+  }
+
+  public function merge (ACLPermissions $merge)
+  {
+    foreach (static::$letters as $var) {
+      $this->$var = ($this->$var || $merge->$var);
+    }
+  }
+
+  public function isSelf (): bool
+  {
+    return $this->self;
+  }
+
+  public function isFull (): bool
+  {
+    return ($this->read && $this->write && $this->create && $this->delete && $this->move);
+  }
+}
diff --git a/include/class_acl.inc b/include/class_acl.inc
index 2935a02f1..386720718 100644
--- a/include/class_acl.inc
+++ b/include/class_acl.inc
@@ -2,7 +2,7 @@
 /*
   This code is part of FusionDirectory (http://www.fusiondirectory.org/)
   Copyright (C) 2003-2010  Cajus Pollmeier
-  Copyright (C) 2011-2016  FusionDirectory
+  Copyright (C) 2011-2020  FusionDirectory
 
   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
@@ -192,12 +192,12 @@ class acl
 
           /* Append ACL if set */
           if ($gacl != "") {
-            $a[$gobject] = [$gacl];
+            $a[$gobject] = [new ACLPermissions($gacl)];
           }
         } else {
           /* All other entries get appended... */
           list($field, $facl)   = explode(';', $ssacl);
-          $a[$gobject][$field]  = $facl;
+          $a[$gobject][$field]  = new ACLPermissions($facl);
         }
       }
     }
diff --git a/include/class_userinfo.inc b/include/class_userinfo.inc
index e7438f084..865103162 100644
--- a/include/class_userinfo.inc
+++ b/include/class_userinfo.inc
@@ -3,7 +3,7 @@
   This code is part of FusionDirectory (http://www.fusiondirectory.org/)
 
   Copyright (C) 2003-2010  Cajus Pollmeier
-  Copyright (C) 2011-2019  FusionDirectory
+  Copyright (C) 2011-2020  FusionDirectory
 
   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
@@ -297,37 +297,41 @@ class userinfo
       For objects located in 'ou=dep1,ou=base' we have to apply both ACLs,
        for objects in 'ou=base' we only have to apply one ACL.
      */
-    $without_self_acl = $all_acl = [];
+    $all_acl = [];
     foreach ($this->ACL as $dn => $acl) {
+      $all_acl[$dn][$dn] = $acl;
       $sdn = $dn;
-      do {
+      while (strpos($dn, ',') !== FALSE) {
+        $dn = preg_replace('/^[^,]*+,/', '', $dn);
         if (isset($this->ACL[$dn])) {
-          $all_acl[$sdn][$dn]           = $this->ACL[$dn];
-          $without_self_acl[$sdn][$dn]  = $this->ACL[$dn];
-          foreach ($without_self_acl[$sdn][$dn] as $acl_id => $acl_set) {
-            /* Remove all acl entries which are especially for the current user (self acl) */
-            foreach ($acl_set['acl'] as $object => $object_acls) {
-              if (isset($object_acls[0]) && (strpos($object_acls[0], 's') !== FALSE)) {
-                unset($without_self_acl[$sdn][$dn][$acl_id]['acl'][$object]);
-                if (empty($without_self_acl[$sdn][$dn][$acl_id]['acl'])) {
-                  unset($without_self_acl[$sdn][$dn][$acl_id]);
-                }
-              }
+          $all_acl[$sdn][$dn] = array_filter(
+            $this->ACL[$dn],
+            function ($ACLInfos)
+            {
+              return ($ACLInfos['type'] === 'subtree');
             }
-          }
+          );
         }
-        $dn = preg_replace("/^[^,]*+,/", "", $dn);
-      } while (strpos($dn, ',') !== FALSE);
+      }
     }
-    $this->ACLperPath = $without_self_acl;
+    $this->ACLperPath = $all_acl;
 
     /* Append Self entry */
     $dn = $this->dn;
-    while (strpos($dn, ",") && !isset($all_acl[$dn])) {
-      $dn = preg_replace("/^[^,]*+,/", "", $dn);
+    while (strpos($dn, ',') && !isset($all_acl[$dn])) {
+      $dn = preg_replace('/^[^,]*+,/', '', $dn);
     }
     if (isset($all_acl[$dn])) {
       $this->ACLperPath[$this->dn] = $all_acl[$dn];
+      if ($dn !== $this->dn) {
+        $this->ACLperPath[$this->dn][$dn] = array_filter(
+          $this->ACLperPath[$this->dn][$dn],
+          function ($ACLInfos)
+          {
+            return ($ACLInfos['type'] === 'subtree');
+          }
+        );
+      }
     }
 
     /* Reset plist menu and ACL cache if needed */
@@ -357,7 +361,7 @@ class userinfo
    */
   function get_category_permissions ($dn, $category)
   {
-    return @$this->get_permissions($dn, $category.'/0', '');
+    return $this->get_permissions($dn, $category.'/0', '');
   }
 
 
@@ -496,7 +500,7 @@ class userinfo
    * \param bool $skip_write   Remove the write acl for this dn
    *
    */
-  function get_permissions ($dn, $object, $attribute = "", $skip_write = FALSE)
+  function get_permissions ($dn, $object, $attribute = '', $skip_write = FALSE)
   {
     global $config;
     /* If we are forced to skip ACLs checks for the current user
@@ -521,71 +525,65 @@ class userinfo
       return $ret;
     }
 
-    /* Detect the set of ACLs we have to check for this object
-     */
-    $adn = $dn;
-    while (!isset($this->ACLperPath[$adn]) && (strpos($adn, ',') !== FALSE)) {
-      $adn = preg_replace("/^[^,]*+,/", "", $adn);
+    /* Detect the set of ACLs we have to check for this object */
+    $parentACLdn = $dn;
+    while (!isset($this->ACLperPath[$parentACLdn]) && (strpos($parentACLdn, ',') !== FALSE)) {
+      $parentACLdn = preg_replace('/^[^,]*+,/', '', $parentACLdn);
     }
-    if (isset($this->ACLperPath[$adn])) {
-      $ACLs = $this->ACLperPath[$adn];
-    } else {
+    if (!isset($this->ACLperPath[$parentACLdn])) {
       $ACL_CACHE["$dn+$object+$attribute"] = '';
       return '';
     }
 
-    /* If we do not need to respect any user-filter settings
-        we can skip the per object ACL checks.
-     */
-    $orig_dn  = $dn;
-    $dn       = $adn;
-    if (isset($ACL_CACHE["$dn+$object+$attribute"])) {
-      $ret = $ACL_CACHE["$dn+$object+$attribute"];
-      if (!isset($ACL_CACHE["$orig_dn+$object+$attribute"])) {
-        $ACL_CACHE["$orig_dn+$object+$attribute"] = $ret;
-      }
-      if ($skip_write) {
-        $ret = str_replace(['w','c','d','m'], '', $ret);
-      }
-      return $ret;
-    }
-
-    $acl = ['r' => '', 'w' => '', 'c' => '', 'd' => '', 'm' => '', 'a' => ''];
-
-    /* Build dn array */
-    $path = explode(',', $dn);
-    $path = array_reverse($path);
-
-    $departmentInfo = $config->getDepartmentInfo();
-
-    /* Walk along the path to evaluate the acl */
-    $cpath = '';
-    foreach ($path as $element) {
-
-      /* Clean potential ACLs for each level */
-      if (isset($departmentInfo[$cpath])) {
-        $acl = $this->cleanACL($acl);
-      }
-
-      if ($cpath == "") {
-        $cpath = $element;
-      } else {
-        $cpath = $element.','.$cpath;
-      }
-
-      if (isset($ACLs[$cpath])) {
+    if (($parentACLdn !== $dn) && isset($ACL_CACHE["sub:$parentACLdn+$object+$attribute"])) {
+      /* Load parent subtree ACLs from cache */
+      $permissions = $ACL_CACHE["sub:$parentACLdn+$object+$attribute"];
+    } else {
+      $permissions = new ACLPermissions();
+
+      /* Merge relevent permissions from parent ACLs */
+      foreach ($this->ACLperPath[$parentACLdn] as $parentdn => $ACLs) {
+        /* Inspect this ACL, place the result into permissions */
+        foreach ($ACLs as $subacl) {
+          if ($permissions->isFull()) {
+            /* Stop merging if we have all rights already */
+            break 2;
+          }
 
-        /* Inspect this ACL, place the result into ACL */
-        foreach ($ACLs[$cpath] as $subacl) {
+          if (($dn != $this->dn) && isset($subacl['acl'][$object][0]) && ($subacl['acl'][$object][0]->isSelf())) {
+            /* Self ACL */
+            continue;
+          }
 
-          /* Reset? Just clean the ACL and turn over to the next one... */
-          if ($subacl['type'] == 'reset') {
-            $acl = $this->cleanACL($acl, TRUE);
+          if (($subacl['type'] === 'base') && ($parentdn !== $dn)) {
+            /* Base assignment on another dn */
             continue;
           }
 
-          /* Self ACLs? */
-          if (($dn != $this->dn) && isset($subacl['acl'][$object][0]) && (strpos($subacl['acl'][$object][0], "s") !== FALSE)) {
+          /* Special global ACL */
+          if (isset($subacl['acl']['all'][0])) {
+            $permissions->merge($subacl['acl']['all'][0]);
+          }
+
+          /* Category ACLs (e.g. $object = "user/0") */
+          if (strstr($object, '/0')) {
+            $ocs = preg_replace("/\/0$/", '', $object);
+            if (isset($config->data['CATEGORIES'][$ocs]) && ($attribute == '')) {
+              foreach ($config->data['CATEGORIES'][$ocs]['classes'] as $oc) {
+                if (isset($subacl['acl'][$ocs.'/'.$oc])) {
+                  if (($dn != $this->dn) &&
+                      isset($subacl['acl'][$ocs.'/'.$oc][0]) &&
+                      ($subacl['acl'][$ocs.'/'.$oc][0]->isSelf())) {
+                    /* Skip self ACL */
+                    continue;
+                  }
+
+                  foreach ($subacl['acl'][$ocs.'/'.$oc] as $anyPermissions) {
+                    $permissions->merge($anyPermissions);
+                  }
+                }
+              }
+            }
             continue;
           }
 
@@ -593,93 +591,32 @@ class userinfo
               Merge global class ACLs [0] with attributes specific ACLs [attribute].
            */
           if (($attribute == '') && isset($subacl['acl'][$object])) {
-            foreach ($subacl['acl'][$object] as $attr => $dummy) {
-              $acl = $this->mergeACL($acl, $subacl['type'], $subacl['acl'][$object][$attr]);
+            foreach ($subacl['acl'][$object] as $anyPermissions) {
+              $permissions->merge($anyPermissions);
             }
             continue;
           }
 
           /* Per attribute ACL? */
           if (isset($subacl['acl'][$object][$attribute])) {
-            $acl = $this->mergeACL($acl, $subacl['type'], $subacl['acl'][$object][$attribute]);
-            continue;
+            $permissions->merge($subacl['acl'][$object][$attribute]);
           }
 
           /* Per object ACL? */
           if (isset($subacl['acl'][$object][0])) {
-            $acl = $this->mergeACL($acl, $subacl['type'], $subacl['acl'][$object][0]);
-            continue;
-          }
-
-          /* Global ACL? */
-          if (isset($subacl['acl']['all'][0])) {
-            $acl = $this->mergeACL($acl, $subacl['type'], $subacl['acl']['all'][0]);
-            continue;
-          }
-
-          /* Category ACLs    (e.g. $object = "user/0")
-           */
-          if (strstr($object, '/0')) {
-            $ocs = preg_replace("/\/0$/", "", $object);
-            if (isset($config->data['CATEGORIES'][$ocs])) {
-
-              /* if $attribute is "", then check every single attribute for this object.
-                 if it is 0, then just check the object category ACL.
-               */
-              if ($attribute == "") {
-                foreach ($config->data['CATEGORIES'][$ocs]['classes'] as $oc) {
-                  if (isset($subacl['acl'][$ocs.'/'.$oc])) {
-                    // Skip ACLs which are defined for ourselfs only - if not checking against ($ui->dn)
-                    if (isset($subacl['acl'][$ocs.'/'.$oc][0]) &&
-                        ($dn != $this->dn) &&
-                        (strpos($subacl['acl'][$ocs.'/'.$oc][0], "s") !== FALSE)) {
-                      continue;
-                    }
-
-                    foreach ($subacl['acl'][$ocs.'/'.$oc] as $attr => $dummy) {
-                      $acl = $this->mergeACL($acl, $subacl['type'], $subacl['acl'][$ocs.'/'.$oc][$attr]);
-                    }
-                    continue;
-                  }
-                }
-              } else {
-                if (isset($subacl['acl'][$ocs.'/'.$oc][0])) {
-                  if (($dn != $this->dn) && (strpos($subacl['acl'][$ocs.'/'.$oc][0], "s") !== FALSE)) {
-                    continue;
-                  }
-                  $acl = $this->mergeACL($acl, $subacl['type'], $subacl['acl'][$ocs.'/'.$oc][0]);
-                }
-              }
-            }
-            continue;
+            $permissions->merge($subacl['acl'][$object][0]);
           }
         }
       }
     }
 
-    /* If the requested ACL is for a container object, then alter
-        ACLs by applying cleanACL a last time.
-     */
-    if (isset($departmentInfo[$dn])) {
-      $acl = $this->cleanACL($acl);
+    if ($parentACLdn !== $dn) {
+      $ACL_CACHE["sub:$parentACLdn+$object+$attribute"] = $permissions;
     }
-
-    /* Assemble string */
-    $ret = "";
-    foreach ($acl as $key => $value) {
-      if ($value !== "") {
-        $ret .= $key;
-      }
-    }
-
-    $ACL_CACHE["$dn+$object+$attribute"]      = $ret;
-    $ACL_CACHE["$orig_dn+$object+$attribute"] = $ret;
+    $ACL_CACHE["$dn+$object+$attribute"] = $permissions;
 
     /* Remove write if needed */
-    if ($skip_write) {
-      $ret = str_replace(['w','c','d','m'], '', $ret);
-    }
-    return $ret;
+    return $permissions->toString($skip_write);
   }
 
   /*!
@@ -728,7 +665,7 @@ class userinfo
           $found = FALSE;
           foreach ($info['acl'] as $cat => $data) {
             /* Skip self acls? */
-            if ($skip_self_acls && isset($data['0']) && (strpos($data['0'], "s") !== FALSE)) {
+            if ($skip_self_acls && isset($data[0]) && $data[0]->isSelf()) {
               continue;
             }
             if (preg_match('/^'.preg_quote($mod, '/').'/', $cat) || ($cat === 'all')) {
@@ -773,83 +710,6 @@ class userinfo
     return array_values($res);
   }
 
-  /*!
-   * \brief Merge acls
-   *
-   * \param $acl The ACL
-   *
-   * \param $type The type
-   *
-   * \param $newACL The new ACL
-   */
-  function mergeACL (array $acl, $type, $newACL)
-  {
-    $at = ["subtree" => "s", "one" => "1"];
-
-    if ((strpos($newACL, 'w') !== FALSE) && (strpos($newACL, 'r') === FALSE)) {
-      $newACL .= "r";
-    }
-
-    /* Ignore invalid characters */
-    $newACL = preg_replace('/[^rwcdm]/', '', $newACL);
-
-    foreach (str_split($newACL) as $char) {
-      /* Skip "self" ACLs without combination of rwcdm, they have no effect.
-         -self flag without read/write/create/...
-       */
-      if (empty($char)) {
-        continue;
-      }
-
-      /* Skip subtree entries */
-      if ($acl[$char] == 's') {
-        continue;
-      }
-
-      if ($type == "base" && $acl[$char] != 1) {
-        $acl[$char] = 0;
-      } else {
-        $acl[$char] = $at[$type];
-      }
-    }
-
-    return $acl;
-  }
-
-  /*!
-   * \brief Clean acls
-   *
-   * \param $acl ACL to be cleaned
-   *
-   * \param boolean $reset FALSE
-   */
-  function cleanACL ($acl, $reset = FALSE)
-  {
-    foreach ($acl as $key => $value) {
-      /* Continue, if value is empty or subtree */
-      if (($value == "") || ($value == "s")) {
-        continue;
-      }
-
-      /* Reset removes everything but 'p' */
-      if ($reset && $value != 'p') {
-        $acl[$key] = "";
-        continue;
-      }
-
-      /* Decrease tree level */
-      if (is_int($value)) {
-        if ($value) {
-          $acl[$key]--;
-        } else {
-          $acl[$key] = "";
-        }
-      }
-    }
-
-    return $acl;
-  }
-
   /*!
    * \brief Return combined acls for a given category
    *
-- 
GitLab