From 84b350c4d57c3f3f1baf615633684eb9f1970648 Mon Sep 17 00:00:00 2001
From: BaranekD <0Baranek.dominik0@gmail.com>
Date: Wed, 17 Feb 2021 17:11:12 +0100
Subject: [PATCH] Added extended entitlements

---
 CHANGELOG.md                                  |   3 +
 .../processFilterConfigurations-example.md    |  15 ++
 lib/AdapterLdap.php                           |  12 +-
 lib/AdapterRpc.php                            |   4 +
 lib/Auth/Process/PerunEntitlement.php         | 123 ++------------
 lib/Auth/Process/PerunEntitlementExtended.php | 140 ++++++++++++++++
 lib/EntitlementUtils.php                      | 154 ++++++++++++++++++
 lib/model/Group.php                           |  12 +-
 8 files changed, 352 insertions(+), 111 deletions(-)
 create mode 100644 lib/Auth/Process/PerunEntitlementExtended.php
 create mode 100644 lib/EntitlementUtils.php

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 498be28b..3c336126 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,9 @@ All notable changes to this project will be documented in this file.
  
 ## [Unreleased]
 
+#### Added
+- Added extended PerunEntitlements
+
 ## [v4.1.1]
 #### Fixed
 - Fixed bad log message in PerunIdentity in mode USERONLY
diff --git a/config-templates/processFilterConfigurations-example.md b/config-templates/processFilterConfigurations-example.md
index 6d494939..223e4821 100644
--- a/config-templates/processFilterConfigurations-example.md
+++ b/config-templates/processFilterConfigurations-example.md
@@ -57,6 +57,21 @@ Example how to enable/configure filter PerunEntitlement:
 ),
 ```
 
+## PerunEntitlementExtended
+
+Example how to enable/configure filter PerunEntitlement:
+
+```php
+33 => array(
+    'class' => 'perun:PerunEntitlementExtended',
+    'interface' => 'ldap',
+    'outputAttrName' => 'eduPersonEntitlementExtended',
+    # forwarded entitlement are released by default
+    #'releaseForwardedEntitlement' => false, OPTIONAL
+    'forwardedEduPersonEntitlement' => 'eduPersonEntitlement',
+),
+```
+
 ## ForceAup
 
 1.Create these attributes in Perun:
diff --git a/lib/AdapterLdap.php b/lib/AdapterLdap.php
index 48f4e97f..3e55cf49 100644
--- a/lib/AdapterLdap.php
+++ b/lib/AdapterLdap.php
@@ -114,13 +114,14 @@ class AdapterLdap extends Adapter
             $group = $this->connector->searchForEntity(
                 $groupDn,
                 '(objectClass=perunGroup)',
-                ['perunGroupId', 'cn', 'perunUniqueGroupName', 'perunVoId', 'description']
+                ['perunGroupId', 'cn', 'perunUniqueGroupName', 'perunVoId', 'uuid', 'description']
             );
             array_push(
                 $groups,
                 new Group(
                     $group['perunGroupId'][0],
                     $group['perunVoId'][0],
+                    $group['uuid'][0],
                     $group['cn'][0],
                     $group['perunUniqueGroupName'][0],
                     $group['description'][0] ?? ''
@@ -154,13 +155,14 @@ class AdapterLdap extends Adapter
                     $group = $this->connector->searchForEntity(
                         'perunGroupId=' . $groupId . ',perunVoId=' . $resource['perunVoId'][0] . ',' . $this->ldapBase,
                         '(objectClass=perunGroup)',
-                        ['perunGroupId', 'cn', 'perunUniqueGroupName', 'perunVoId', 'description']
+                        ['perunGroupId', 'cn', 'perunUniqueGroupName', 'perunVoId', 'uuid', 'description']
                     );
                     array_push(
                         $groups,
                         new Group(
                             $group['perunGroupId'][0],
                             $group['perunVoId'][0],
+                            $group['uuid'][0],
                             $group['cn'],
                             $group['perunUniqueGroupName'][0],
                             $group['description'][0] ?? ''
@@ -180,7 +182,7 @@ class AdapterLdap extends Adapter
         $group = $this->connector->searchForEntity(
             'perunVoId=' . $voId . ',' . $this->ldapBase,
             '(&(objectClass=perunGroup)(perunUniqueGroupName=' . $name . '))',
-            ['perunGroupId', 'cn', 'perunUniqueGroupName', 'perunVoId', 'description']
+            ['perunGroupId', 'cn', 'perunUniqueGroupName', 'perunVoId', 'uuid', 'description']
         );
         if ($group === null) {
             throw new Exception(
@@ -190,6 +192,7 @@ class AdapterLdap extends Adapter
         return new Group(
             $group['perunGroupId'][0],
             $group['perunVoId'][0],
+            $group['uuId'][0],
             $group['cn'][0],
             $group['perunUniqueGroupName'][0],
             $group['description'][0] ?? ''
@@ -404,7 +407,7 @@ class AdapterLdap extends Adapter
         $groups = $this->connector->searchForEntities(
             $this->ldapBase,
             '(&(uniqueMember=perunUserId=' . $userId . ', ou=People,' . $this->ldapBase . ')' . $resourcesString . ')',
-            ['perunGroupId', 'cn', 'perunUniqueGroupName', 'perunVoId', 'description']
+            ['perunGroupId', 'cn', 'perunUniqueGroupName', 'perunVoId', 'uuid', 'description']
         );
 
         foreach ($groups as $group) {
@@ -413,6 +416,7 @@ class AdapterLdap extends Adapter
                 new Group(
                     $group['perunGroupId'][0],
                     $group['perunVoId'][0],
+                    $group['uuid'][0],
                     $group['cn'][0],
                     $group['perunUniqueGroupName'][0],
                     $group['description'][0] ?? ''
diff --git a/lib/AdapterRpc.php b/lib/AdapterRpc.php
index 93629333..1b786511 100644
--- a/lib/AdapterRpc.php
+++ b/lib/AdapterRpc.php
@@ -139,6 +139,7 @@ class AdapterRpc extends Adapter
                     new Group(
                         $group['id'],
                         $group['voId'],
+                        $group['uuid'],
                         $group['name'],
                         $uniqueName,
                         $group['description']
@@ -194,6 +195,7 @@ class AdapterRpc extends Adapter
                     new Group(
                         $group['id'],
                         $group['voId'],
+                        $group['uuid'],
                         $group['name'],
                         $uniqueName,
                         $group['description']
@@ -219,6 +221,7 @@ class AdapterRpc extends Adapter
         return new Group(
             $group['id'],
             $group['voId'],
+            $group['uuid'],
             $group['name'],
             $uniqueName,
             $group['description']
@@ -360,6 +363,7 @@ class AdapterRpc extends Adapter
                 array_push($groups, new Group(
                     $usersGroupOnFacility['id'],
                     $usersGroupOnFacility['voId'],
+                    $usersGroupOnFacility['uuid'],
                     $usersGroupOnFacility['name'],
                     $uniqueName,
                     $usersGroupOnFacility['description']
diff --git a/lib/Auth/Process/PerunEntitlement.php b/lib/Auth/Process/PerunEntitlement.php
index 2f05148f..93b5124e 100644
--- a/lib/Auth/Process/PerunEntitlement.php
+++ b/lib/Auth/Process/PerunEntitlement.php
@@ -7,6 +7,7 @@ use SimpleSAML\Configuration;
 use \SimpleSAML\Auth\ProcessingFilter;
 use SimpleSAML\Module\perun\Adapter;
 use SimpleSAML\Logger;
+use SimpleSAML\Module\perun\EntitlementUtils;
 
 /**
  * Class PerunEntitlement
@@ -77,16 +78,25 @@ class PerunEntitlement extends ProcessingFilter
 
         if (isset($request['perun']['groups'])) {
             $eduPersonEntitlement = $this->getEduPersonEntitlement($request);
-            $capabilities = $this->getCapabilities($request);
+            $capabilities = EntitlementUtils::getCapabilities(
+                $request,
+                $this->adapter,
+                $this->entitlementPrefix,
+                $this->entitlementAuthority
+            );
         } else {
             Logger::debug(
-                'perun:PerunEntitlement: There are no user groups assign to facility.' .
-                '=> Skipping getEduPersonEntitlement and getResourceCapabilities'
+                'perun:PerunEntitlement: There are no user groups assigned to facility.' .
+                '=> Skipping getEduPersonEntitlement and getCapabilities'
             );
         }
 
         if ($this->releaseForwardedEntitlement) {
-            $forwardedEduPersonEntitlement = $this->getForwardedEduPersonEntitlement($request);
+            $forwardedEduPersonEntitlement = EntitlementUtils::getForwardedEduPersonEntitlement(
+                $request,
+                $this->adapter,
+                $this->forwardedEduPersonEntitlement
+            );
         }
 
         $request['Attributes'][$this->eduPersonEntitlement] = array_unique(array_merge(
@@ -126,76 +136,11 @@ class PerunEntitlement extends ProcessingFilter
         return $eduPersonEntitlement;
     }
 
-    private function getForwardedEduPersonEntitlement(&$request)
-    {
-        $forwardedEduPersonEntitlement = [];
-
-        if (!isset($request['perun']['user'])) {
-            Logger::debug(
-                'perun:PerunEntitlement: Object Perun User is not specified.' .
-                '=> Skipping getting forwardedEntitlement.'
-            );
-            return $forwardedEduPersonEntitlement;
-        }
-
-        $user = $request['perun']['user'];
-
-        try {
-            $forwardedEduPersonEntitlementMap = $this->adapter->getUserAttributesValues(
-                $user,
-                [$this->forwardedEduPersonEntitlement]
-            );
-        } catch (Exception $exception) {
-            Logger::error(
-                'perun:PerunEntitlement: Exception ' . $exception->getMessage() .
-                ' was thrown in method \'getForwardedEduPersonEntitlement\'.'
-            );
-        }
-
-        if (!empty($forwardedEduPersonEntitlementMap)) {
-            $forwardedEduPersonEntitlement = array_values($forwardedEduPersonEntitlementMap)[0];
-        }
-
-        return $forwardedEduPersonEntitlement;
-    }
-
-    private function getCapabilities(&$request)
-    {
-        $resourceCapabilities = [];
-        $facilityCapabilities = [];
-        $capabilitiesResult = [];
-
-        $spEntityId = $this->getSpEntityId($request);
-        try {
-            $resourceCapabilities = $this->adapter->getResourceCapabilities($spEntityId, $request['perun']['groups']);
-            $facilityCapabilities = $this->adapter->getFacilityCapabilities($spEntityId);
-        } catch (Exception $exception) {
-            Logger::error(
-                'perun:PerunEntitlement: Exception ' . $exception->getMessage() .
-                ' was thrown in method \'getCapabilities\'.'
-            );
-        }
-
-        $capabilities = array_unique(array_merge($resourceCapabilities, $facilityCapabilities));
-
-        foreach ($capabilities as $capability) {
-            $wrappedCapability = $this->capabilitiesWrapper($capability);
-            array_push($capabilitiesResult, $wrappedCapability);
-        }
-
-        return $capabilitiesResult;
-    }
-
     private function groupNameWrapper($groupName)
     {
-        return $this->entitlementPrefix . 'group:' . implode(':', $this->encodeName($groupName)) .
-               '#' . $this->entitlementAuthority;
-    }
-
-    private function capabilitiesWrapper($capabilities)
-    {
-        return $this->entitlementPrefix . implode(':', $this->encodeName($capabilities)) .
-               '#' . $this->entitlementAuthority;
+        return $this->entitlementPrefix . 'group:' .
+            implode(':', EntitlementUtils::encodeEntitlement($groupName)) .
+            '#' . $this->entitlementAuthority;
     }
 
     /**
@@ -228,38 +173,4 @@ class PerunEntitlement extends ProcessingFilter
             return $this->entitlementPrefix . 'group:' . $groupName;
         }
     }
-
-    private function encodeName($name)
-    {
-        $charsToSkip = [
-            '!' => '%21',
-            '$' => '%24',
-            '\'' => '%27',
-            '(' => '%28',
-            ')' => '%29',
-            '*' => '%2A',
-            ',' => '%2C',
-            ';' => '%3B',
-            '&' => '%26',
-            '=' => '%3D',
-            '@' => '%40',
-            ':' => '%3A',
-            '+' => '%2B'
-        ];
-
-        $name = array_map('rawurlencode', explode(':', $name));
-        $name = str_replace(array_values($charsToSkip), array_keys($charsToSkip), $name);
-
-        return $name;
-    }
-
-    private function getSpEntityId(&$request)
-    {
-        if (isset($request['SPMetadata']['entityid'])) {
-            return $request['SPMetadata']['entityid'];
-        } else {
-            throw new Exception('perun:PerunEntitlement: Cannot find entityID of remote SP. ' .
-                'hint: Do you have this filter in IdP context?');
-        }
-    }
 }
diff --git a/lib/Auth/Process/PerunEntitlementExtended.php b/lib/Auth/Process/PerunEntitlementExtended.php
new file mode 100644
index 00000000..99876790
--- /dev/null
+++ b/lib/Auth/Process/PerunEntitlementExtended.php
@@ -0,0 +1,140 @@
+<?php
+
+
+namespace SimpleSAML\Module\perun\Auth\Process;
+
+use SimpleSAML\Error\Exception;
+use SimpleSAML\Configuration;
+use \SimpleSAML\Auth\ProcessingFilter;
+use SimpleSAML\Module\perun\Adapter;
+use SimpleSAML\Logger;
+use SimpleSAML\Module\perun\EntitlementUtils;
+
+/**
+ * Class PerunEntitlementExtended
+ *
+ * This filter joins extended version of eduPersonEntitlement, forwardedEduPersonEntitlement, resource capabilities
+ * and facility capabilities
+ *
+ * @author Dominik Baránek <baranek@ics.muni.cz>
+ */
+class PerunEntitlementExtended extends ProcessingFilter
+{
+    const CONFIG_FILE_NAME = 'module_perun.php';
+    const OUTPUT_ATTR_NAME = 'outputAttrName';
+    const RELEASE_FORWARDED_ENTITLEMENT = 'releaseForwardedEntitlement';
+    const FORWARDED_EDU_PERSON_ENTITLEMENT = 'forwardedEduPersonEntitlement';
+    const ENTITLEMENTPREFIX_ATTR = 'entitlementPrefix';
+    const ENTITLEMENTAUTHORITY_ATTR = 'entitlementAuthority';
+    const GROUPNAMEAARC_ATTR = 'groupNameAARC';
+    const INTERFACE_PROPNAME = 'interface';
+
+    private $outputAttrName;
+    private $releaseForwardedEntitlement;
+    private $forwardedEduPersonEntitlement;
+    private $entitlementPrefix;
+    private $entitlementAuthority;
+    private $groupNameAARC;
+    private $adapter;
+
+    public function __construct($config, $reserved)
+    {
+        parent::__construct($config, $reserved);
+        $modulePerunConfiguration = Configuration::getConfig(self::CONFIG_FILE_NAME);
+        assert('is_array($config)');
+
+        $configuration = Configuration::loadFromArray($config);
+
+        $this->outputAttrName = $configuration->getString(self::OUTPUT_ATTR_NAME, 'eduPersonEntitlementExtended');
+        $this->releaseForwardedEntitlement = $configuration->getBoolean(self::RELEASE_FORWARDED_ENTITLEMENT, true);
+        $this->forwardedEduPersonEntitlement = $configuration->getString(
+            self::FORWARDED_EDU_PERSON_ENTITLEMENT,
+            $this->releaseForwardedEntitlement ? Configuration::REQUIRED_OPTION : ''
+        );
+
+        $this->groupNameAARC = $modulePerunConfiguration->getBoolean(self::GROUPNAMEAARC_ATTR, false);
+        $this->entitlementPrefix = $modulePerunConfiguration->getString(
+            self::ENTITLEMENTPREFIX_ATTR,
+            $this->groupNameAARC ? Configuration::REQUIRED_OPTION : ''
+        );
+        $this->entitlementAuthority = $modulePerunConfiguration->getString(
+            self::ENTITLEMENTAUTHORITY_ATTR,
+            $this->groupNameAARC ? Configuration::REQUIRED_OPTION : ''
+        );
+
+        $interface = $configuration->getValueValidate(
+            self::INTERFACE_PROPNAME,
+            [Adapter::RPC, Adapter::LDAP],
+            Adapter::RPC
+        );
+        $this->adapter = Adapter::getInstance($interface);
+    }
+
+    public function process(&$request)
+    {
+        $eduPersonEntitlementExtended = [];
+        $capabilities = [];
+        $forwardedEduPersonEntitlement = [];
+
+        if (isset($request['perun']['groups'])) {
+            $eduPersonEntitlementExtended = $this->getEduPersonEntitlementExtended($request);
+
+            $capabilities = EntitlementUtils::getCapabilities(
+                $request,
+                $this->adapter,
+                $this->entitlementPrefix,
+                $this->entitlementAuthority
+            );
+        } else {
+            Logger::debug(
+                'perun:PerunEntitlementExtended: There are no user groups assigned to facility.' .
+                '=> Skipping getEduPersonEntitlementExtended and getCapabilities'
+            );
+        }
+
+        if ($this->releaseForwardedEntitlement) {
+            $forwardedEduPersonEntitlement = EntitlementUtils::getForwardedEduPersonEntitlement(
+                $request,
+                $this->adapter,
+                $this->forwardedEduPersonEntitlement
+            );
+        }
+
+        $request['Attributes'][$this->outputAttrName] = array_unique(array_merge(
+            $eduPersonEntitlementExtended,
+            $forwardedEduPersonEntitlement,
+            $capabilities
+        ));
+    }
+
+    private function getEduPersonEntitlementExtended(&$request)
+    {
+        $eduPersonEntitlementExtended = [];
+
+        $groups = $request['perun']['groups'];
+        foreach ($groups as $group) {
+            $entitlement = EntitlementUtils::groupEntitlementWrapper(
+                $group->getUuid(),
+                $this->entitlementPrefix,
+                $this->entitlementAuthority
+            );
+
+            array_push($eduPersonEntitlementExtended, $entitlement);
+
+            $groupName = $group->getUniqueName();
+            $groupName = preg_replace('/^(\w*)\:members$/', '$1', $groupName);
+
+            $entitlementWithAttributes = EntitlementUtils::groupEntitlementWithAttributesWrapper(
+                $group->getUuid(),
+                $groupName,
+                $this->entitlementPrefix,
+                $this->entitlementAuthority
+            );
+
+            array_push($eduPersonEntitlementExtended, $entitlementWithAttributes);
+        }
+
+        natsort($eduPersonEntitlementExtended);
+        return $eduPersonEntitlementExtended;
+    }
+}
diff --git a/lib/EntitlementUtils.php b/lib/EntitlementUtils.php
new file mode 100644
index 00000000..181683f3
--- /dev/null
+++ b/lib/EntitlementUtils.php
@@ -0,0 +1,154 @@
+<?php
+
+
+namespace SimpleSAML\Module\perun;
+
+use SimpleSAML\Error\Exception;
+use SimpleSAML\Logger;
+
+/**
+ * Class EntitlementUtils
+ *
+ * This class contains common functions of PerunEntitlement and PerunEntitlementExtended.
+ *
+ * @author Dominik Baránek <baranek@ics.muni.cz>
+ */
+class EntitlementUtils
+{
+    const GROUP = 'group';
+    const GROUP_ATTRIBUTES = 'groupAttributes';
+    const DISPLAY_NAME = 'displayName';
+
+
+    public static function getForwardedEduPersonEntitlement(&$request, $adapter, $forwardedEduPersonEntitlement)
+    {
+        $result = [];
+
+        if (!isset($request['perun']['user'])) {
+            Logger::debug(
+                'perun:EntitlementUtils: Object Perun User is not specified.' .
+                '=> Skipping getting forwardedEntitlement.'
+            );
+            return $result;
+        }
+
+        $user = $request['perun']['user'];
+
+        try {
+            $forwardedEduPersonEntitlementMap = $adapter->getUserAttributesValues(
+                $user,
+                [$forwardedEduPersonEntitlement]
+            );
+        } catch (Exception $exception) {
+            Logger::error(
+                'perun:EntitlementUtils: Exception ' . $exception->getMessage() .
+                ' was thrown in method \'getForwardedEduPersonEntitlement\'.'
+            );
+        }
+
+        if (!empty($forwardedEduPersonEntitlementMap)) {
+            $result = array_values($forwardedEduPersonEntitlementMap)[0];
+        }
+
+        return $result;
+    }
+
+    public static function getCapabilities(&$request, $adapter, $prefix, $authority)
+    {
+        $resourceCapabilities = [];
+        $facilityCapabilities = [];
+        $capabilitiesResult = [];
+
+        $spEntityId = self::getSpEntityId($request);
+        try {
+            $resourceCapabilities = $adapter->getResourceCapabilities($spEntityId, $request['perun']['groups']);
+            $facilityCapabilities = $adapter->getFacilityCapabilities($spEntityId);
+        } catch (Exception $exception) {
+            Logger::error(
+                'perun:EntitlementUtils: Exception ' . $exception->getMessage() .
+                ' was thrown in method \'getCapabilities\'.'
+            );
+        }
+
+        $capabilities = array_unique(array_merge($resourceCapabilities, $facilityCapabilities));
+
+        foreach ($capabilities as $capability) {
+            $wrappedCapability = self::capabilitiesWrapper($capability, $prefix, $authority);
+            array_push($capabilitiesResult, $wrappedCapability);
+        }
+
+        return $capabilitiesResult;
+    }
+
+    public static function encodeName($name)
+    {
+        $charsToSkip = [
+            '-' => '%2D',
+            '_' => '%5F',
+            '.' => '%2E',
+            '~' => '%7E',
+            '!' => '%21',
+            '\'' => '%27',
+            '(' => '%28',
+            ')' => '%29',
+            '*' => '%2A',
+        ];
+
+        $name = rawurlencode($name);
+        $name = str_replace(array_values($charsToSkip), array_keys($charsToSkip), $name);
+
+        return $name;
+    }
+
+    public static function encodeEntitlement($name)
+    {
+        $charsToSkip = [
+            '!' => '%21',
+            '$' => '%24',
+            '\'' => '%27',
+            '(' => '%28',
+            ')' => '%29',
+            '*' => '%2A',
+            ',' => '%2C',
+            ';' => '%3B',
+            '&' => '%26',
+            '=' => '%3D',
+            '@' => '%40',
+            ':' => '%3A',
+            '+' => '%2B'
+        ];
+
+        $name = array_map('rawurlencode', explode(':', $name));
+        $name = str_replace(array_values($charsToSkip), array_keys($charsToSkip), $name);
+
+        return $name;
+    }
+
+    public static function capabilitiesWrapper($capabilities, $prefix, $authority)
+    {
+        return $prefix . implode(':', self::encodeEntitlement($capabilities)) .
+            '#' . $authority;
+    }
+
+    public static function groupEntitlementWrapper($uuid, $prefix, $authority)
+    {
+        return $prefix . self::GROUP . ':' .
+            EntitlementUtils::encodeName($uuid) . '#' . $authority;
+    }
+
+    public static function groupEntitlementWithAttributesWrapper($uuid, $groupName, $prefix, $authority)
+    {
+        return $prefix . self::GROUP_ATTRIBUTES . ':' . $uuid . '?' . self::DISPLAY_NAME . '=' .
+            EntitlementUtils::encodeName($groupName) . '#' . $authority;
+    }
+
+    private static function getSpEntityId(&$request)
+    {
+        if (isset($request['SPMetadata']['entityid'])) {
+            return $request['SPMetadata']['entityid'];
+        } else {
+            throw new Exception('perun:EntitlementUtils: Cannot find entityID of remote SP. ' .
+                'hint: Do you have this filter in IdP context?');
+        }
+    }
+}
diff --git a/lib/model/Group.php b/lib/model/Group.php
index 43024118..96869fd6 100644
--- a/lib/model/Group.php
+++ b/lib/model/Group.php
@@ -10,6 +10,7 @@ class Group implements HasId
 {
     private $id;
     private $voId;
+    private $uuid;
     private $name;
     private $uniqueName;
     private $description;
@@ -22,10 +23,11 @@ class Group implements HasId
      * @param $uniqueName
      * @param $description
      */
-    public function __construct($id, $voId, $name, $uniqueName, $description)
+    public function __construct($id, $voId, $uuid, $name, $uniqueName, $description)
     {
         $this->id = $id;
         $this->voId = $voId;
+        $this->uuid = $uuid;
         $this->name = $name;
         $this->uniqueName = $uniqueName;
         $this->description = $description;
@@ -44,6 +46,14 @@ class Group implements HasId
         return $this->voId;
     }
 
+    /**
+     * @return string
+     */
+    public function getUuid()
+    {
+        return $this->uuid;
+    }
+
     /**
      * @return string
      */
-- 
GitLab