You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@guacamole.apache.org by vn...@apache.org on 2018/08/14 23:37:33 UTC

[01/12] guacamole-client git commit: GUACAMOLE-220: Hide identifier set editor if there are no identifiers to edit.

Repository: guacamole-client
Updated Branches:
  refs/heads/staging/1.0.0 c36d33321 -> 402ddb577


GUACAMOLE-220: Hide identifier set editor if there are no identifiers to edit.

Project: http://git-wip-us.apache.org/repos/asf/guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/guacamole-client/commit/00591217
Tree: http://git-wip-us.apache.org/repos/asf/guacamole-client/tree/00591217
Diff: http://git-wip-us.apache.org/repos/asf/guacamole-client/diff/00591217

Branch: refs/heads/staging/1.0.0
Commit: 0059121716c869e34a8089ada3069ad67ffb6ee7
Parents: 229b0de
Author: Michael Jumper <mj...@apache.org>
Authored: Tue Aug 7 12:25:55 2018 -0700
Committer: Michael Jumper <mj...@apache.org>
Committed: Wed Aug 8 09:00:06 2018 -0700

----------------------------------------------------------------------
 .../app/manage/directives/identifierSetEditor.js      | 14 ++++++++++++++
 .../app/manage/templates/identifierSetEditor.html     |  2 +-
 2 files changed, 15 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/00591217/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js b/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js
index 82f1109..4240901 100644
--- a/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js
+++ b/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js
@@ -260,6 +260,20 @@ angular.module('manage').directive('identifierSetEditor', ['$injector',
             $scope.expanded = false;
         };
 
+        /**
+         * Returns whether there are absolutely no identifiers that can be
+         * managed using this editor. If true, the editor is effectively
+         * useless, as there is nothing whatsoever to display.
+         *
+         * @returns {Boolean}
+         *     true if there are no identifiers that can be managed using this
+         *     editor, false otherwise.
+         */
+        $scope.isEmpty = function isEmpty() {
+            return _.isEmpty($scope.identifiers)
+                && _.isEmpty($scope.identifiersAvailable);
+        };
+
     }];
 
     return directive;

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/00591217/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html b/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html
index 838decf..7f66088 100644
--- a/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html
+++ b/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html
@@ -1,4 +1,4 @@
-<div class="related-objects">
+<div class="related-objects" ng-hide="isEmpty()">
     <div class="header">
         <h2>{{ header | translate }}</h2>
         <div class="filter">


[07/12] guacamole-client git commit: GUACAMOLE-220: Do not display "X" for removing an identifier if the identifier cannot actually be edited.

Posted by vn...@apache.org.
GUACAMOLE-220: Do not display "X" for removing an identifier if the identifier cannot actually be edited.


Project: http://git-wip-us.apache.org/repos/asf/guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/guacamole-client/commit/ca1db783
Tree: http://git-wip-us.apache.org/repos/asf/guacamole-client/tree/ca1db783
Diff: http://git-wip-us.apache.org/repos/asf/guacamole-client/diff/ca1db783

Branch: refs/heads/staging/1.0.0
Commit: ca1db7831bfc7b365f4bb84e2d0f9b082354162f
Parents: 0059121
Author: Michael Jumper <mj...@apache.org>
Authored: Tue Aug 7 13:05:09 2018 -0700
Committer: Michael Jumper <mj...@apache.org>
Committed: Wed Aug 8 21:50:26 2018 -0700

----------------------------------------------------------------------
 .../app/manage/directives/identifierSetEditor.js | 19 +++++++++++++++++++
 .../manage/templates/identifierSetEditor.html    |  3 ++-
 2 files changed, 21 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/ca1db783/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js b/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js
index 4240901..d2936e7 100644
--- a/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js
+++ b/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js
@@ -118,6 +118,15 @@ angular.module('manage').directive('identifierSetEditor', ['$injector',
         $scope.identifierFlags = {};
 
         /**
+         * Map of identifiers to boolean flags indicating whether that
+         * identifier is editable. If an identifier is not editable, it will be
+         * absent from this map.
+         *
+         * @type Object.<String, Boolean>
+         */
+        $scope.isEditable = {};
+
+        /**
          * Adds the given identifier to the given sorted array of identifiers,
          * preserving the sorted order of the array. If the identifier is
          * already present, no change is made to the array. The given array
@@ -194,6 +203,16 @@ angular.module('manage').directive('identifierSetEditor', ['$injector',
 
         });
 
+        // An identifier is editable iff it is available to be added or removed
+        // from the identifier set being edited (iff it is within the
+        // identifiersAvailable array)
+        $scope.$watch('identifiersAvailable', function availableIdentifiersChanged(identifiers) {
+            $scope.isEditable = {};
+            angular.forEach(identifiers, function storeEditableIdentifier(identifier) {
+                $scope.isEditable[identifier] = true;
+            });
+        });
+
         /**
          * Notifies the controller that a change has been made to the flag
          * denoting presence/absence of a particular identifier within the

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/ca1db783/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html b/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html
index 7f66088..72c235c 100644
--- a/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html
+++ b/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html
@@ -18,7 +18,8 @@
             <ul>
                 <li ng-repeat="identifier in identifiers | filter: filterString">
                     <label><img src="images/x-red.png" alt="Remove" class="remove"
-                                ng-click="removeIdentifier(identifier)"/><span class="identifier">{{ identifier }}</span>
+                                ng-click="removeIdentifier(identifier)"
+                                ng-show="isEditable[identifier]"/><span class="identifier">{{ identifier }}</span>
                     </label>
                 </li>
             </ul>


[04/12] guacamole-client git commit: GUACAMOLE-220: Add missing getUserGroupAttributes() to JavaScript schemaService.

Posted by vn...@apache.org.
GUACAMOLE-220: Add missing getUserGroupAttributes() to JavaScript schemaService.


Project: http://git-wip-us.apache.org/repos/asf/guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/guacamole-client/commit/615f5c6b
Tree: http://git-wip-us.apache.org/repos/asf/guacamole-client/tree/615f5c6b
Diff: http://git-wip-us.apache.org/repos/asf/guacamole-client/diff/615f5c6b

Branch: refs/heads/staging/1.0.0
Commit: 615f5c6bab28f78884cf42fb29c76ad44822a33d
Parents: 55bcf25
Author: Michael Jumper <mj...@apache.org>
Authored: Thu Apr 19 23:21:17 2018 -0700
Committer: Michael Jumper <mj...@apache.org>
Committed: Wed Aug 8 09:00:06 2018 -0700

----------------------------------------------------------------------
 .../webapp/app/rest/services/schemaService.js   | 34 ++++++++++++++++++++
 1 file changed, 34 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/615f5c6b/guacamole/src/main/webapp/app/rest/services/schemaService.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/rest/services/schemaService.js b/guacamole/src/main/webapp/app/rest/services/schemaService.js
index cc871d8..61c8639 100644
--- a/guacamole/src/main/webapp/app/rest/services/schemaService.js
+++ b/guacamole/src/main/webapp/app/rest/services/schemaService.js
@@ -66,6 +66,40 @@ angular.module('rest').factory('schemaService', ['$injector',
 
     /**
      * Makes a request to the REST API to get the list of available attributes
+     * for user group objects, returning a promise that provides an array of
+     * @link{Form} objects if successful. Each element of the array describes
+     * a logical grouping of possible attributes.
+     *
+     * @param {String} dataSource
+     *     The unique identifier of the data source containing the user groups
+     *     whose available attributes are to be retrieved. This identifier
+     *     corresponds to an AuthenticationProvider within the Guacamole web
+     *     application.
+     *
+     * @returns {Promise.<Form[]>}
+     *     A promise which will resolve with an array of @link{Form}
+     *     objects, where each @link{Form} describes a logical grouping of
+     *     possible attributes.
+     */
+    service.getUserGroupAttributes = function getUserGroupAttributes(dataSource) {
+
+        // Build HTTP parameters set
+        var httpParameters = {
+            token : authenticationService.getCurrentToken()
+        };
+
+        // Retrieve available user group attributes
+        return requestService({
+            cache   : cacheService.schema,
+            method  : 'GET',
+            url     : 'api/session/data/' + encodeURIComponent(dataSource) + '/schema/userGroupAttributes',
+            params  : httpParameters
+        });
+
+    };
+
+    /**
+     * Makes a request to the REST API to get the list of available attributes
      * for connection objects, returning a promise that provides an array of
      * @link{Form} objects if successful. Each element of the array describes
      * a logical grouping of possible attributes.


[06/12] guacamole-client git commit: GUACAMOLE-220: Upgrade to latest versions of jQuery and Lodash.

Posted by vn...@apache.org.
GUACAMOLE-220: Upgrade to latest versions of jQuery and Lodash.

The version of Lodash previously included with Guacamole lacks the
sortedIndexOf() function, which is needed by the identity set editor
directive for manipulating sorted arrays of identifiers.

If upgrading Lodash, may as well upgrade jQuery while we're at it. The
version previously included within Guacamole is no longer maintained.


Project: http://git-wip-us.apache.org/repos/asf/guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/guacamole-client/commit/1cf16d1d
Tree: http://git-wip-us.apache.org/repos/asf/guacamole-client/tree/1cf16d1d
Diff: http://git-wip-us.apache.org/repos/asf/guacamole-client/diff/1cf16d1d

Branch: refs/heads/staging/1.0.0
Commit: 1cf16d1dc6f02e608eb860d627c423dc6be958da
Parents: 615f5c6
Author: Michael Jumper <mj...@apache.org>
Authored: Sat Jul 21 19:41:47 2018 -0700
Committer: Michael Jumper <mj...@apache.org>
Committed: Wed Aug 8 09:00:06 2018 -0700

----------------------------------------------------------------------
 guacamole/pom.xml                               |  4 +-
 guacamole/src/licenses/LICENSE                  | 60 ++++++++++++++++----
 .../bundled/jquery-2.1.3/MIT-LICENSE.txt        | 21 -------
 .../licenses/bundled/jquery-3.3.1/LICENSE.txt   | 36 ++++++++++++
 .../licenses/bundled/lodash-2.4.1/LICENSE.txt   | 22 -------
 .../src/licenses/bundled/lodash-4.17.10/LICENSE | 47 +++++++++++++++
 guacamole/src/main/webapp/index.html            |  4 +-
 7 files changed, 137 insertions(+), 57 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/1cf16d1d/guacamole/pom.xml
----------------------------------------------------------------------
diff --git a/guacamole/pom.xml b/guacamole/pom.xml
index dc4f082..0899e78 100644
--- a/guacamole/pom.xml
+++ b/guacamole/pom.xml
@@ -308,7 +308,7 @@
         <dependency>
             <groupId>org.webjars.bower</groupId>
             <artifactId>lodash</artifactId>
-            <version>2.4.1</version>
+            <version>4.17.10</version>
             <scope>runtime</scope>
         </dependency>
 
@@ -316,7 +316,7 @@
         <dependency>
             <groupId>org.webjars.bower</groupId>
             <artifactId>jquery</artifactId>
-            <version>2.1.3</version>
+            <version>3.3.1</version>
             <scope>runtime</scope>
         </dependency>
 

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/1cf16d1d/guacamole/src/licenses/LICENSE
----------------------------------------------------------------------
diff --git a/guacamole/src/licenses/LICENSE b/guacamole/src/licenses/LICENSE
index 4474707..51f5b21 100644
--- a/guacamole/src/licenses/LICENSE
+++ b/guacamole/src/licenses/LICENSE
@@ -486,13 +486,21 @@ Jettison (https://github.com/jettison-json/jettison)
 jQuery (http://jquery.com/)
 ---------------------------
 
-    Version: 2.1.3
-    From: 'jQuery Foundation' (http://jquery.com/)
+    Version: 3.3.1
+    From: 'JS Foundation' (https://js.foundation/)
     License(s):
-        MIT (bundled/jquery-2.1.3/MIT-LICENSE.txt)
+        MIT (bundled/jquery-3.3.1/LICENSE.txt)
 
-Copyright 2014 jQuery Foundation and other contributors
-http://jquery.com/
+Copyright JS Foundation and other contributors, https://js.foundation/
+
+This software consists of voluntary contributions made by many
+individuals. For exact contribution history, see the revision history
+available at https://github.com/jquery/jquery
+
+The following license applies to all parts of this software except as
+documented below:
+
+====
 
 Permission is hereby granted, free of charge, to any person obtaining
 a copy of this software and associated documentation files (the
@@ -513,6 +521,13 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
+====
+
+All files located in the node_modules and external directories are
+externally maintained libraries used by this software which have their
+own licenses; we recommend you read them, as their terms may differ from
+the terms above.
+
 
 JSR-250 Reference Implementation
 (https://jcp.org/aboutJava/communityprocess/final/jsr250/index.html)
@@ -536,15 +551,25 @@ JSR-330 / Dependency Injection for Java (http://code.google.com/p/atinject/)
 Lodash (https://lodash.com/)
 ----------------------------
 
-    Version: 2.4.1
-    From: 'The Dojo Foundation' (http://dojofoundation.org/)
+    Version: 4.17.10
+    From: 'JS Foundation' (https://js.foundation/)
     License(s):
-        MIT (bundled/lodash-2.4.1/LICENSE.txt)
+        MIT (bundled/lodash-4.17.10/LICENSE)
+
+Copyright JS Foundation and other contributors <https://js.foundation/>
 
-Copyright 2012-2013 The Dojo Foundation <http://dojofoundation.org/>
-Based on Underscore.js 1.5.2, copyright 2009-2013 Jeremy Ashkenas,
+Based on Underscore.js, copyright Jeremy Ashkenas,
 DocumentCloud and Investigative Reporters & Editors <http://underscorejs.org/>
 
+This software consists of voluntary contributions made by many
+individuals. For exact contribution history, see the revision history
+available at https://github.com/lodash/lodash
+
+The following license applies to all parts of this software except as
+documented below:
+
+====
+
 Permission is hereby granted, free of charge, to any person obtaining
 a copy of this software and associated documentation files (the
 "Software"), to deal in the Software without restriction, including
@@ -564,6 +589,21 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
+====
+
+Copyright and related rights for sample code are waived via CC0. Sample
+code is defined as all source code displayed within the prose of the
+documentation.
+
+CC0: http://creativecommons.org/publicdomain/zero/1.0/
+
+====
+
+Files located in the node_modules and vendor directories are externally
+maintained libraries used by this software which have their own
+licenses; we recommend you read them, as their terms may differ from the
+terms above.
+
 
 Logback (http://logback.qos.ch/)
 --------------------------------

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/1cf16d1d/guacamole/src/licenses/bundled/jquery-2.1.3/MIT-LICENSE.txt
----------------------------------------------------------------------
diff --git a/guacamole/src/licenses/bundled/jquery-2.1.3/MIT-LICENSE.txt b/guacamole/src/licenses/bundled/jquery-2.1.3/MIT-LICENSE.txt
deleted file mode 100644
index cdd31b5..0000000
--- a/guacamole/src/licenses/bundled/jquery-2.1.3/MIT-LICENSE.txt
+++ /dev/null
@@ -1,21 +0,0 @@
-Copyright 2014 jQuery Foundation and other contributors
-http://jquery.com/
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/1cf16d1d/guacamole/src/licenses/bundled/jquery-3.3.1/LICENSE.txt
----------------------------------------------------------------------
diff --git a/guacamole/src/licenses/bundled/jquery-3.3.1/LICENSE.txt b/guacamole/src/licenses/bundled/jquery-3.3.1/LICENSE.txt
new file mode 100644
index 0000000..e4e5e00
--- /dev/null
+++ b/guacamole/src/licenses/bundled/jquery-3.3.1/LICENSE.txt
@@ -0,0 +1,36 @@
+Copyright JS Foundation and other contributors, https://js.foundation/
+
+This software consists of voluntary contributions made by many
+individuals. For exact contribution history, see the revision history
+available at https://github.com/jquery/jquery
+
+The following license applies to all parts of this software except as
+documented below:
+
+====
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+====
+
+All files located in the node_modules and external directories are
+externally maintained libraries used by this software which have their
+own licenses; we recommend you read them, as their terms may differ from
+the terms above.

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/1cf16d1d/guacamole/src/licenses/bundled/lodash-2.4.1/LICENSE.txt
----------------------------------------------------------------------
diff --git a/guacamole/src/licenses/bundled/lodash-2.4.1/LICENSE.txt b/guacamole/src/licenses/bundled/lodash-2.4.1/LICENSE.txt
deleted file mode 100644
index 49869bb..0000000
--- a/guacamole/src/licenses/bundled/lodash-2.4.1/LICENSE.txt
+++ /dev/null
@@ -1,22 +0,0 @@
-Copyright 2012-2013 The Dojo Foundation <http://dojofoundation.org/>
-Based on Underscore.js 1.5.2, copyright 2009-2013 Jeremy Ashkenas,
-DocumentCloud and Investigative Reporters & Editors <http://underscorejs.org/>
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/1cf16d1d/guacamole/src/licenses/bundled/lodash-4.17.10/LICENSE
----------------------------------------------------------------------
diff --git a/guacamole/src/licenses/bundled/lodash-4.17.10/LICENSE b/guacamole/src/licenses/bundled/lodash-4.17.10/LICENSE
new file mode 100644
index 0000000..c6f2f61
--- /dev/null
+++ b/guacamole/src/licenses/bundled/lodash-4.17.10/LICENSE
@@ -0,0 +1,47 @@
+Copyright JS Foundation and other contributors <https://js.foundation/>
+
+Based on Underscore.js, copyright Jeremy Ashkenas,
+DocumentCloud and Investigative Reporters & Editors <http://underscorejs.org/>
+
+This software consists of voluntary contributions made by many
+individuals. For exact contribution history, see the revision history
+available at https://github.com/lodash/lodash
+
+The following license applies to all parts of this software except as
+documented below:
+
+====
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+====
+
+Copyright and related rights for sample code are waived via CC0. Sample
+code is defined as all source code displayed within the prose of the
+documentation.
+
+CC0: http://creativecommons.org/publicdomain/zero/1.0/
+
+====
+
+Files located in the node_modules and vendor directories are externally
+maintained libraries used by this software which have their own
+licenses; we recommend you read them, as their terms may differ from the
+terms above.

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/1cf16d1d/guacamole/src/main/webapp/index.html
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/index.html b/guacamole/src/main/webapp/index.html
index 5a53d8a..a4a93da 100644
--- a/guacamole/src/main/webapp/index.html
+++ b/guacamole/src/main/webapp/index.html
@@ -57,8 +57,8 @@
         <script type="text/javascript" src="relocateParameters.js"></script>
 
         <!-- Utility libraries -->
-        <script type="text/javascript" src="webjars/jquery/2.1.3/dist/jquery.min.js"></script>
-        <script type="text/javascript" src="webjars/lodash/2.4.1/dist/lodash.min.js"></script>
+        <script type="text/javascript" src="webjars/jquery/3.3.1/dist/jquery.min.js"></script>
+        <script type="text/javascript" src="webjars/lodash/4.17.10/dist/lodash.min.js"></script>
 
         <!-- AngularJS -->
         <script type="text/javascript" src="webjars/angular/1.6.9/angular.min.js"></script>


[10/12] guacamole-client git commit: GUACAMOLE-220: Add management tab and editor for user groups.

Posted by vn...@apache.org.
GUACAMOLE-220: Add management tab and editor for user groups.


Project: http://git-wip-us.apache.org/repos/asf/guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/guacamole-client/commit/8ad3f253
Tree: http://git-wip-us.apache.org/repos/asf/guacamole-client/tree/8ad3f253
Diff: http://git-wip-us.apache.org/repos/asf/guacamole-client/diff/8ad3f253

Branch: refs/heads/staging/1.0.0
Commit: 8ad3f2537119d61becad38558dc1365742ba7444
Parents: de80957
Author: Michael Jumper <mj...@apache.org>
Authored: Thu Apr 19 23:51:25 2018 -0700
Committer: Michael Jumper <mj...@apache.org>
Committed: Thu Aug 9 10:46:06 2018 -0700

----------------------------------------------------------------------
 .../webapp/app/index/config/indexRouteConfig.js |   9 +
 .../src/main/webapp/app/index/styles/lists.css  |   4 +
 .../src/main/webapp/app/index/styles/ui.css     |   8 +
 .../controllers/manageUserGroupController.js    | 538 +++++++++++++++++++
 .../manage/directives/systemPermissionEditor.js |   4 +
 .../app/manage/styles/manage-user-group.css     |  71 +++
 .../app/manage/templates/manageUserGroup.html   | 101 ++++
 .../app/manage/types/ManageableUserGroup.js     |  53 ++
 .../app/navigation/services/userPageService.js  |  27 +
 .../settings/controllers/settingsController.js  |   4 +-
 .../directives/guacSettingsUserGroups.js        | 270 ++++++++++
 .../main/webapp/app/settings/styles/buttons.css |   6 +
 .../app/settings/styles/user-group-list.css     |  36 ++
 .../webapp/app/settings/templates/settings.html |   1 +
 .../settings/templates/settingsUserGroups.html  |  48 ++
 .../images/action-icons/guac-user-group-add.png | Bin 0 -> 1222 bytes
 .../images/user-icons/guac-user-group.png       | Bin 0 -> 1428 bytes
 guacamole/src/main/webapp/translations/en.json  |  67 ++-
 18 files changed, 1244 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8ad3f253/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js
index 47bc48e..5a8c3fb 100644
--- a/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js
+++ b/guacamole/src/main/webapp/app/index/config/indexRouteConfig.js
@@ -171,6 +171,15 @@ angular.module('index').config(['$routeProvider', '$locationProvider',
             resolve       : { updateCurrentToken: updateCurrentToken }
         })
 
+        // User group editor
+        .when('/manage/:dataSource/userGroups/:id?', {
+            title         : 'APP.NAME',
+            bodyClassName : 'manage',
+            templateUrl   : 'app/manage/templates/manageUserGroup.html',
+            controller    : 'manageUserGroupController',
+            resolve       : { updateCurrentToken: updateCurrentToken }
+        })
+
         // Client view
         .when('/client/:id/:params?', {
             bodyClassName : 'client',

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8ad3f253/guacamole/src/main/webapp/app/index/styles/lists.css
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/index/styles/lists.css b/guacamole/src/main/webapp/app/index/styles/lists.css
index 0c761ae..80df491 100644
--- a/guacamole/src/main/webapp/app/index/styles/lists.css
+++ b/guacamole/src/main/webapp/app/index/styles/lists.css
@@ -18,12 +18,14 @@
  */
 
 .user,
+.user-group,
 .connection-group,
 .connection {
     cursor: pointer;
 }
 
 .user a,
+.user-group a,
 .connection a,
 .connection-group a {
     text-decoration:none;
@@ -31,6 +33,7 @@
 }
 
 .user a:hover,
+.user-group a:hover,
 .connection a:hover,
 .connection-group a:hover {
     text-decoration:none;
@@ -38,6 +41,7 @@
 }
 
 .user a:visited,
+.user-group a:visited,
 .connection a:visited,
 .connection-group a:visited {
     text-decoration:none;

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8ad3f253/guacamole/src/main/webapp/app/index/styles/ui.css
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/index/styles/ui.css b/guacamole/src/main/webapp/app/index/styles/ui.css
index 434f443..58406eb 100644
--- a/guacamole/src/main/webapp/app/index/styles/ui.css
+++ b/guacamole/src/main/webapp/app/index/styles/ui.css
@@ -156,6 +156,14 @@ div.section {
     background-image: url('images/action-icons/guac-user-add.png');
 }
 
+.icon.user-group {
+    background-image: url('images/user-icons/guac-user-group.png');
+}
+
+.icon.user-group.add {
+    background-image: url('images/action-icons/guac-user-group-add.png');
+}
+
 .icon.connection {
     background-image: url('images/protocol-icons/guac-plug.png');
 }

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8ad3f253/guacamole/src/main/webapp/app/manage/controllers/manageUserGroupController.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserGroupController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserGroupController.js
new file mode 100644
index 0000000..229b3b8
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserGroupController.js
@@ -0,0 +1,538 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * The controller for editing user groups.
+ */
+angular.module('manage').controller('manageUserGroupController', ['$scope', '$injector',
+        function manageUserGroupController($scope, $injector) {
+            
+    // Required types
+    var ManagementPermissions = $injector.get('ManagementPermissions');
+    var PermissionFlagSet     = $injector.get('PermissionFlagSet');
+    var PermissionSet         = $injector.get('PermissionSet');
+    var UserGroup             = $injector.get('UserGroup');
+
+    // Required services
+    var $location             = $injector.get('$location');
+    var $routeParams          = $injector.get('$routeParams');
+    var $q                    = $injector.get('$q');
+    var authenticationService = $injector.get('authenticationService');
+    var dataSourceService     = $injector.get('dataSourceService');
+    var membershipService     = $injector.get('membershipService');
+    var permissionService     = $injector.get('permissionService');
+    var requestService        = $injector.get('requestService');
+    var schemaService         = $injector.get('schemaService');
+    var userGroupService      = $injector.get('userGroupService');
+    var userService           = $injector.get('userService');
+
+    /**
+     * The identifiers of all data sources currently available to the
+     * authenticated user.
+     *
+     * @type String[]
+     */
+    var dataSources = authenticationService.getAvailableDataSources();
+
+    /**
+     * The username of the current, authenticated user.
+     *
+     * @type String
+     */
+    var currentUsername = authenticationService.getCurrentUsername();
+
+    /**
+     * The identifier of the original user group from which this user group is
+     * being cloned. Only valid if this is a new user group.
+     *
+     * @type String
+     */
+    var cloneSourceIdentifier = $location.search().clone;
+
+    /**
+     * The identifier of the user group being edited. If a new user group is
+     * being created, this will not be defined.
+     *
+     * @type String
+     */
+    var identifier = $routeParams.id;
+
+    /**
+     * The unique identifier of the data source containing the user group being
+     * edited.
+     *
+     * @type String
+     */
+    $scope.dataSource = $routeParams.dataSource;
+
+    /**
+     * All user groups associated with the same identifier as the group being
+     * created or edited, as a map of data source identifier to the UserGroup
+     * object within that data source.
+     *
+     * @type Object.<String, UserGroup>
+     */
+    $scope.userGroups = null;
+
+    /**
+     * The user group being modified.
+     *
+     * @type UserGroup
+     */
+    $scope.userGroup = null;
+
+    /**
+     * All permissions associated with the user group being modified.
+     * 
+     * @type PermissionFlagSet
+     */
+    $scope.permissionFlags = null;
+
+    /**
+     * The set of permissions that will be added to the user group when the
+     * user group is saved. Permissions will only be present in this set if they
+     * are manually added, and not later manually removed before saving.
+     *
+     * @type PermissionSet
+     */
+    $scope.permissionsAdded = new PermissionSet();
+
+    /**
+     * The set of permissions that will be removed from the user group when the
+     * user group is saved. Permissions will only be present in this set if they
+     * are manually removed, and not later manually added before saving.
+     *
+     * @type PermissionSet
+     */
+    $scope.permissionsRemoved = new PermissionSet();
+
+    /**
+     * The identifiers of all user groups which can be manipulated (all groups
+     * for which the user accessing this interface has UPDATE permission),
+     * whether that means changing the members of those groups or changing the
+     * groups of which those groups are members. If this information has not
+     * yet been retrieved, this will be null.
+     *
+     * @type String[]
+     */
+    $scope.availableGroups = null;
+
+    /**
+     * The identifiers of all users which can be manipulated (all users for
+     * which the user accessing this interface has UPDATE permission), either
+     * through adding those users as a member of the current group or removing
+     * those users from the current group. If this information has not yet been
+     * retrieved, this will be null.
+     *
+     * @type String[]
+     */
+    $scope.availableUsers = null;
+
+    /**
+     * The identifiers of all user groups of which this group is a member,
+     * taking into account any user groups which will be added/removed when
+     * saved. If this information has not yet been retrieved, this will be
+     * null.
+     *
+     * @type String[]
+     */
+    $scope.parentGroups = null;
+
+    /**
+     * The set of identifiers of all parent user groups to which this group
+     * will be added when saved. Parent groups will only be present in this set
+     * if they are manually added, and not later manually removed before
+     * saving.
+     *
+     * @type String[]
+     */
+    $scope.parentGroupsAdded = [];
+
+    /**
+     * The set of identifiers of all parent user groups from which this group
+     * will be removed when saved. Parent groups will only be present in this
+     * set if they are manually removed, and not later manually added before
+     * saving.
+     *
+     * @type String[]
+     */
+    $scope.parentGroupsRemoved = [];
+
+    /**
+     * The identifiers of all user groups which are members of this group,
+     * taking into account any user groups which will be added/removed when
+     * saved. If this information has not yet been retrieved, this will be
+     * null.
+     *
+     * @type String[]
+     */
+    $scope.memberGroups = null;
+
+    /**
+     * The set of identifiers of all member user groups which will be added to
+     * this group when saved. Member groups will only be present in this set if
+     * they are manually added, and not later manually removed before saving.
+     *
+     * @type String[]
+     */
+    $scope.memberGroupsAdded = [];
+
+    /**
+     * The set of identifiers of all member user groups which will be removed
+     * from this group when saved. Member groups will only be present in this
+     * set if they are manually removed, and not later manually added before
+     * saving.
+     *
+     * @type String[]
+     */
+    $scope.memberGroupsRemoved = [];
+
+    /**
+     * The identifiers of all users which are members of this group, taking
+     * into account any users which will be added/removed when saved. If this
+     * information has not yet been retrieved, this will be null.
+     *
+     * @type String[]
+     */
+    $scope.memberUsers = null;
+
+    /**
+     * The set of identifiers of all member users which will be added to this
+     * group when saved. Member users will only be present in this set if they
+     * are manually added, and not later manually removed before saving.
+     *
+     * @type String[]
+     */
+    $scope.memberUsersAdded = [];
+
+    /**
+     * The set of identifiers of all member users which will be removed from
+     * this group when saved. Member users will only be present in this set if
+     * they are manually removed, and not later manually added before saving.
+     *
+     * @type String[]
+     */
+    $scope.memberUsersRemoved = [];
+
+    /**
+     * For each applicable data source, the management-related actions that the
+     * current user may perform on the user group currently being created
+     * or modified, as a map of data source identifier to the
+     * {@link ManagementPermissions} object describing the actions available
+     * within that data source, or null if the current user's permissions have
+     * not yet been loaded.
+     *
+     * @type Object.<String, ManagementPermissions>
+     */
+    $scope.managementPermissions = null;
+
+    /**
+     * All available user group attributes. This is only the set of attribute
+     * definitions, organized as logical groupings of attributes, not attribute
+     * values.
+     *
+     * @type Form[]
+     */
+    $scope.attributes = null;
+
+    /**
+     * Returns whether critical data has completed being loaded.
+     *
+     * @returns {Boolean}
+     *     true if enough data has been loaded for the user group interface to
+     *     be useful, false otherwise.
+     */
+    $scope.isLoaded = function isLoaded() {
+
+        return $scope.userGroups            !== null
+            && $scope.permissionFlags       !== null
+            && $scope.managementPermissions !== null
+            && $scope.availableGroups       !== null
+            && $scope.availableUsers        !== null
+            && $scope.parentGroups          !== null
+            && $scope.memberGroups          !== null
+            && $scope.memberUsers           !== null
+            && $scope.attributes            !== null;
+
+    };
+
+    /**
+     * Returns whether the current user can edit the identifier of the user
+     * group being edited.
+     *
+     * @returns {Boolean}
+     *     true if the current user can edit the identifier of the user group
+     *     being edited, false otherwise.
+     */
+    $scope.canEditIdentifier = function canEditIdentifier() {
+        return !identifier;
+    };
+
+    /**
+     * Loads the data associated with the user group having the given
+     * identifier, preparing the interface for making modifications to that
+     * existing user group.
+     *
+     * @param {String} dataSource
+     *     The unique identifier of the data source containing the user group
+     *     to load.
+     *
+     * @param {String} identifier
+     *     The unique identifier of the user group to load.
+     *
+     * @returns {Promise}
+     *     A promise which is resolved when the interface has been prepared for
+     *     editing the given user group.
+     */
+    var loadExistingUserGroup = function loadExistingGroup(dataSource, identifier) {
+        return $q.all({
+            userGroups   : dataSourceService.apply(userGroupService.getUserGroup, dataSources, identifier),
+            permissions  : permissionService.getPermissions(dataSource, identifier, true),
+            parentGroups : membershipService.getUserGroups(dataSource, identifier, true),
+            memberGroups : membershipService.getMemberUserGroups(dataSource, identifier),
+            memberUsers  : membershipService.getMemberUsers(dataSource, identifier)
+        })
+        .then(function userGroupDataRetrieved(values) {
+
+            $scope.userGroups = values.userGroups;
+            $scope.userGroup  = values.userGroups[dataSource];
+            $scope.parentGroups = values.parentGroups;
+            $scope.memberGroups = values.memberGroups;
+            $scope.memberUsers = values.memberUsers;
+
+            // Create skeleton user group if user group does not exist
+            if (!$scope.userGroup)
+                $scope.userGroup = new UserGroup({
+                    'identifier' : identifier
+                });
+
+            $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(values.permissions);
+
+        });
+    };
+
+    /**
+     * Loads the data associated with the user group having the given
+     * identifier, preparing the interface for cloning that existing user
+     * group.
+     *
+     * @param {String} dataSource
+     *     The unique identifier of the data source containing the user group to
+     *     be cloned.
+     *
+     * @param {String} identifier
+     *     The unique identifier of the user group being cloned.
+     *
+     * @returns {Promise}
+     *     A promise which is resolved when the interface has been prepared for
+     *     cloning the given user group.
+     */
+    var loadClonedUserGroup = function loadClonedUserGroup(dataSource, identifier) {
+        return $q.all({
+            userGroups   : dataSourceService.apply(userGroupService.getUserGroup, [dataSource], identifier),
+            permissions  : permissionService.getPermissions(dataSource, identifier, true),
+            parentGroups : membershipService.getUserGroups(dataSource, identifier, true),
+            memberGroups : membershipService.getMemberUserGroups(dataSource, identifier),
+            memberUsers  : membershipService.getMemberUsers(dataSource, identifier)
+        })
+        .then(function userGroupDataRetrieved(values) {
+
+            $scope.userGroups = {};
+            $scope.userGroup  = values.userGroups[dataSource];
+            $scope.parentGroups = values.parentGroups;
+            $scope.parentGroupsAdded = values.parentGroups;
+            $scope.memberGroups = values.memberGroups;
+            $scope.memberGroupsAdded = values.memberGroups;
+            $scope.memberUsers = values.memberUsers;
+            $scope.memberUsersAdded = values.memberUsers;
+
+            $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(values.permissions);
+            $scope.permissionsAdded = values.permissions;
+
+        });
+    };
+
+    /**
+     * Loads skeleton user group data, preparing the interface for creating a
+     * new user group.
+     *
+     * @returns {Promise}
+     *     A promise which is resolved when the interface has been prepared for
+     *     creating a new user group.
+     */
+    var loadSkeletonUserGroup = function loadSkeletonUserGroup() {
+
+        // No user groups exist regardless of data source if the user group is
+        // being created
+        $scope.userGroups = {};
+
+        // Use skeleton user group object with no associated permissions
+        $scope.userGroup = new UserGroup();
+        $scope.parentGroups = [];
+        $scope.memberGroups = [];
+        $scope.memberUsers = [];
+        $scope.permissionFlags = new PermissionFlagSet();
+
+        return $q.resolve();
+
+    };
+
+    /**
+     * Loads the data required for performing the management task requested
+     * through the route parameters given at load time, automatically preparing
+     * the interface for editing an existing user group, cloning an existing
+     * user group, or creating an entirely new user group.
+     *
+     * @returns {Promise}
+     *     A promise which is resolved when the interface has been prepared
+     *     for performing the requested management task.
+     */
+    var loadRequestedUserGroup = function loadRequestedUserGroup() {
+
+        // Pull user group data and permissions if we are editing an existing
+        // user group
+        if (identifier)
+            return loadExistingUserGroup($scope.dataSource, identifier);
+
+        // If we are cloning an existing user group, pull its data instead
+        if (cloneSourceIdentifier)
+            return loadClonedUserGroup($scope.dataSource, cloneSourceIdentifier);
+
+        // If we are creating a new user group, populate skeleton user group data
+        return loadSkeletonUserGroup();
+
+    };
+
+    // Populate interface with requested data
+    $q.all({
+        userGroupData : loadRequestedUserGroup(),
+        permissions   : dataSourceService.apply(permissionService.getEffectivePermissions, dataSources, currentUsername),
+        userGroups    : userGroupService.getUserGroups($scope.dataSource, [ PermissionSet.ObjectPermissionType.UPDATE ]),
+        users         : userService.getUsers($scope.dataSource, [ PermissionSet.ObjectPermissionType.UPDATE ]),
+        attributes    : schemaService.getUserGroupAttributes($scope.dataSource)
+    })
+    .then(function dataReceived(values) {
+
+        $scope.attributes = values.attributes;
+
+        $scope.managementPermissions = {};
+        angular.forEach(dataSources, function deriveManagementPermissions(dataSource) {
+
+            // Determine whether data source contains this user group
+            var exists = (dataSource in $scope.userGroups);
+
+            // Add the identifiers of all modifiable user groups
+            $scope.availableGroups = [];
+            angular.forEach(values.userGroups, function addUserGroupIdentifier(userGroup) {
+                $scope.availableGroups.push(userGroup.identifier);
+            });
+
+            // Add the identifiers of all modifiable users
+            $scope.availableUsers = [];
+            angular.forEach(values.users, function addUserIdentifier(user) {
+                $scope.availableUsers.push(user.username);
+            });
+
+            // Calculate management actions available for this specific group
+            $scope.managementPermissions[dataSource] = ManagementPermissions.fromPermissionSet(
+                    values.permissions[dataSource],
+                    PermissionSet.SystemPermissionType.CREATE_USER_GROUP,
+                    PermissionSet.hasUserGroupPermission,
+                    exists ? identifier : null);
+
+        });
+
+    }, requestService.WARN);
+
+    /**
+     * Returns the URL for the page which manages the user group currently
+     * being edited under the given data source. The given data source need not
+     * be the same as the data source currently selected.
+     *
+     * @param {String} dataSource
+     *     The unique identifier of the data source that the URL is being
+     *     generated for.
+     *
+     * @returns {String}
+     *     The URL for the page which manages the user group currently being
+     *     edited under the given data source.
+     */
+    $scope.getUserGroupURL = function getUserGroupURL(dataSource) {
+        return '/manage/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier || '');
+    };
+
+    /**
+     * Cancels all pending edits, returning to the main list of user groups.
+     */
+    $scope.returnToUserGroupList = function returnToUserGroupList() {
+        $location.url('/settings/userGroups');
+    };
+
+    /**
+     * Cancels all pending edits, opening an edit page for a new user group
+     * which is prepopulated with the data from the user currently being edited.
+     */
+    $scope.cloneUserGroup = function cloneUserGroup() {
+        $location.path('/manage/' + encodeURIComponent($scope.dataSource) + '/userGroups').search('clone', identifier);
+    };
+
+    /**
+     * Saves the current user group, creating a new user group or updating the
+     * existing user group depending on context, returning a promise which is
+     * resolved if the save operation succeeds and rejected if the save
+     * operation fails.
+     *
+     * @returns {Promise}
+     *     A promise which is resolved if the save operation succeeds and is
+     *     rejected with an {@link Error} if the save operation fails.
+     */
+    $scope.saveUserGroup = function saveUserGroup() {
+
+        // Save or create the user group, depending on whether the user group exists
+        var saveUserGroupPromise;
+        if ($scope.dataSource in $scope.userGroups)
+            saveUserGroupPromise = userGroupService.saveUserGroup($scope.dataSource, $scope.userGroup);
+        else
+            saveUserGroupPromise = userGroupService.createUserGroup($scope.dataSource, $scope.userGroup);
+
+        return saveUserGroupPromise.then(function savedUserGroup() {
+            return $q.all([
+                permissionService.patchPermissions($scope.dataSource, $scope.userGroup.identifier, $scope.permissionsAdded, $scope.permissionsRemoved, true),
+                membershipService.patchUserGroups($scope.dataSource, $scope.userGroup.identifier, $scope.parentGroupsAdded, $scope.parentGroupsRemoved, true),
+                membershipService.patchMemberUserGroups($scope.dataSource, $scope.userGroup.identifier, $scope.memberGroupsAdded, $scope.memberGroupsRemoved),
+                membershipService.patchMemberUsers($scope.dataSource, $scope.userGroup.identifier, $scope.memberUsersAdded, $scope.memberUsersRemoved)
+            ]);
+        });
+
+    };
+
+    /**
+     * Deletes the current user group, returning a promise which is resolved if
+     * the delete operation succeeds and rejected if the delete operation
+     * fails.
+     *
+     * @returns {Promise}
+     *     A promise which is resolved if the delete operation succeeds and is
+     *     rejected with an {@link Error} if the delete operation fails.
+     */
+    $scope.deleteUserGroup = function deleteUserGroup() {
+        return userGroupService.deleteUserGroup($scope.dataSource, $scope.userGroup);
+    };
+
+}]);

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8ad3f253/guacamole/src/main/webapp/app/manage/directives/systemPermissionEditor.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/manage/directives/systemPermissionEditor.js b/guacamole/src/main/webapp/app/manage/directives/systemPermissionEditor.js
index ec41872..67fd3f4 100644
--- a/guacamole/src/main/webapp/app/manage/directives/systemPermissionEditor.js
+++ b/guacamole/src/main/webapp/app/manage/directives/systemPermissionEditor.js
@@ -126,6 +126,10 @@ angular.module('manage').directive('systemPermissionEditor', ['$injector',
                 value: PermissionSet.SystemPermissionType.CREATE_USER
             },
             {
+                label: "MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS",
+                value: PermissionSet.SystemPermissionType.CREATE_USER_GROUP
+            },
+            {
                 label: "MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS",
                 value: PermissionSet.SystemPermissionType.CREATE_CONNECTION
             },

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8ad3f253/guacamole/src/main/webapp/app/manage/styles/manage-user-group.css
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/manage/styles/manage-user-group.css b/guacamole/src/main/webapp/app/manage/styles/manage-user-group.css
new file mode 100644
index 0000000..df9e80d
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/styles/manage-user-group.css
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.manage-user-group .page-tabs .page-list li.read-only a[href],
+.manage-user-group .page-tabs .page-list li.unlinked  a[href],
+.manage-user-group .page-tabs .page-list li.linked    a[href] {
+    padding-right: 2.5em;
+    position: relative;
+}
+
+.manage-user-group .page-tabs .page-list li.read-only a[href]:before,
+.manage-user-group .page-tabs .page-list li.unlinked  a[href]:before,
+.manage-user-group .page-tabs .page-list li.linked    a[href]:before {
+    content: ' ';
+    position: absolute;
+    right: 0;
+    bottom: 0;
+    top: 0;
+    width: 2.5em;
+    background-size: 1.25em;
+    background-repeat: no-repeat;
+    background-position: center;
+}
+
+.manage-user-group .page-tabs .page-list li.read-only a[href]:before {
+    background-image: url('images/lock.png');
+}
+
+.manage-user-group .page-tabs .page-list li.unlinked a[href]:before {
+    background-image: url('images/plus.png');
+}
+
+.manage-user-group .page-tabs .page-list li.unlinked a[href] {
+    opacity: 0.5;
+}
+
+.manage-user-group .page-tabs .page-list li.unlinked a[href]:hover,
+.manage-user-group .page-tabs .page-list li.unlinked a[href].current {
+    opacity: 1;
+}
+
+.manage-user-group .page-tabs .page-list li.linked a[href]:before {
+    background-image: url('images/checkmark.png');
+}
+
+.manage-user-group .notice.read-only {
+
+    background: #FDA;
+    border: 1px solid rgba(0, 0, 0, 0.125);
+    border-radius: 0.25em;
+
+    text-align: center;
+    padding: 1em;
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8ad3f253/guacamole/src/main/webapp/app/manage/templates/manageUserGroup.html
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUserGroup.html b/guacamole/src/main/webapp/app/manage/templates/manageUserGroup.html
new file mode 100644
index 0000000..c659915
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/templates/manageUserGroup.html
@@ -0,0 +1,101 @@
+<div class="manage-user-group view" ng-class="{loading: !isLoaded()}">
+
+    <!-- User group header and data source tabs -->
+    <div class="header tabbed">
+        <h2>{{'MANAGE_USER_GROUP.SECTION_HEADER_EDIT_USER_GROUP' | translate}}</h2>
+        <guac-user-menu></guac-user-menu>
+    </div>
+    <data-data-source-tabs ng-hide="cloneSourceIdentifier"
+        permissions="managementPermissions"
+        url="getUserGroupURL(dataSource)">
+    </data-data-source-tabs>
+
+    <!-- Warn if user group is read-only -->
+    <div class="section" ng-hide="managementPermissions[dataSource].canSaveObject">
+        <p class="notice read-only">{{'MANAGE_USER_GROUP.INFO_READ_ONLY' | translate}}</p>
+    </div>
+
+    <!-- Sections applicable to non-read-only user groups -->
+    <div ng-show="managementPermissions[dataSource].canSaveObject">
+
+        <!-- User group name -->
+        <div class="section">
+            <table class="properties">
+                <tr>
+                    <th>{{'MANAGE_USER_GROUP.FIELD_HEADER_USER_GROUP_NAME' | translate}}</th>
+                    <td>
+                        <input ng-show="canEditIdentifier()" ng-model="userGroup.identifier" type="text"/>
+                        <span  ng-hide="canEditIdentifier()">{{userGroup.identifier}}</span>
+                    </td>
+                </tr>
+            </table>
+        </div>
+
+        <!-- User group attributes section -->
+        <div class="attributes" ng-show="managementPermissions[dataSource].canChangeAttributes">
+            <guac-form namespace="'USER_GROUP_ATTRIBUTES'" content="attributes"
+                       model="userGroup.attributes"
+                       model-only="!managementPermissions[dataSource].canChangeAllAttributes"></guac-form>
+        </div>
+
+        <!-- System permissions section -->
+        <system-permission-editor ng-show="managementPermissions[dataSource].canChangePermissions"
+              data-data-source="dataSource"
+              permission-flags="permissionFlags"
+              permissions-added="permissionsAdded"
+              permissions-removed="permissionsRemoved">
+        </system-permission-editor>
+
+        <!-- Parent group section -->
+        <identifier-set-editor
+            header="MANAGE_USER_GROUP.SECTION_HEADER_USER_GROUPS"
+            empty-placeholder="MANAGE_USER_GROUP.HELP_NO_USER_GROUPS"
+            unavailable-placeholder="MANAGE_USER_GROUP.INFO_NO_USER_GROUPS_AVAILABLE"
+            identifiers-available="availableGroups"
+            identifiers="parentGroups"
+            identifiers-added="parentGroupsAdded"
+            identifiers-removed="parentGroupsRemoved">
+        </identifier-set-editor>
+
+        <!-- Member group section -->
+        <identifier-set-editor
+            header="MANAGE_USER_GROUP.SECTION_HEADER_MEMBER_USER_GROUPS"
+            empty-placeholder="MANAGE_USER_GROUP.HELP_NO_MEMBER_USER_GROUPS"
+            unavailable-placeholder="MANAGE_USER_GROUP.INFO_NO_USER_GROUPS_AVAILABLE"
+            identifiers-available="availableGroups"
+            identifiers="memberGroups"
+            identifiers-added="memberGroupsAdded"
+            identifiers-removed="memberGroupsRemoved">
+        </identifier-set-editor>
+
+        <!-- Member user section -->
+        <identifier-set-editor
+            header="MANAGE_USER_GROUP.SECTION_HEADER_MEMBER_USERS"
+            empty-placeholder="MANAGE_USER_GROUP.HELP_NO_MEMBER_USERS"
+            unavailable-placeholder="MANAGE_USER_GROUP.INFO_NO_USERS_AVAILABLE"
+            identifiers-available="availableUsers"
+            identifiers="memberUsers"
+            identifiers-added="memberUsersAdded"
+            identifiers-removed="memberUsersRemoved">
+        </identifier-set-editor>
+
+        <!-- Connection permissions section -->
+        <connection-permission-editor ng-show="managementPermissions[dataSource].canChangePermissions"
+              data-data-source="dataSource"
+              permission-flags="permissionFlags"
+              permissions-added="permissionsAdded"
+              permissions-removed="permissionsRemoved">
+        </connection-permission-editor>
+
+        <!-- Form action buttons -->
+        <management-buttons namespace="MANAGE_USER_GROUP"
+              permissions="managementPermissions[dataSource]"
+              save="saveUserGroup()"
+              delete="deleteUserGroup()"
+              clone="cloneUserGroup()"
+              return="returnToUserGroupList()">
+        </management-buttons>
+
+    </div>
+
+</div>

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8ad3f253/guacamole/src/main/webapp/app/manage/types/ManageableUserGroup.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/manage/types/ManageableUserGroup.js b/guacamole/src/main/webapp/app/manage/types/ManageableUserGroup.js
new file mode 100644
index 0000000..6853fa0
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/types/ManageableUserGroup.js
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * A service for defining the ManageableUserGroup class.
+ */
+angular.module('manage').factory('ManageableUserGroup', [function defineManageableUserGroup() {
+
+    /**
+     * A pairing of an @link{UserGroup} with the identifier of its corresponding
+     * data source.
+     *
+     * @constructor
+     * @param {Object|ManageableUserGroup} template
+     */
+    var ManageableUserGroup = function ManageableUserGroup(template) {
+
+        /**
+         * The unique identifier of the data source containing this user.
+         *
+         * @type String
+         */
+        this.dataSource = template.dataSource;
+
+        /**
+         * The @link{UserGroup} object represented by this ManageableUserGroup
+         * and contained within the associated data source.
+         *
+         * @type UserGroup
+         */
+        this.userGroup = template.userGroup;
+
+    };
+
+    return ManageableUserGroup;
+
+}]);

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8ad3f253/guacamole/src/main/webapp/app/navigation/services/userPageService.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/navigation/services/userPageService.js b/guacamole/src/main/webapp/app/navigation/services/userPageService.js
index 4d1e612..f5bc308 100644
--- a/guacamole/src/main/webapp/app/navigation/services/userPageService.js
+++ b/guacamole/src/main/webapp/app/navigation/services/userPageService.js
@@ -192,6 +192,7 @@ angular.module('navigation').factory('userPageService', ['$injector',
         var pages = [];
         
         var canManageUsers = [];
+        var canManageUserGroups = [];
         var canManageConnections = [];
         var canViewConnectionRecords = [];
         var canManageSessions = [];
@@ -235,6 +236,24 @@ angular.module('navigation').factory('userPageService', ['$injector',
                 canManageUsers.push(dataSource);
             }
 
+            // Determine whether the current user needs access to the group management UI
+            if (
+                    // System permissions
+                       PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER)
+                    || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_USER_GROUP)
+
+                    // Permission to update user groups
+                    || PermissionSet.hasUserGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE)
+
+                    // Permission to delete user groups
+                    || PermissionSet.hasUserGroupPermission(permissions, PermissionSet.ObjectPermissionType.DELETE)
+
+                    // Permission to administer user groups
+                    || PermissionSet.hasUserGroupPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER)
+            ) {
+                canManageUserGroups.push(dataSource);
+            }
+
             // Determine whether the current user needs access to the connection management UI
             if (
                     // System permissions
@@ -295,6 +314,14 @@ angular.module('navigation').factory('userPageService', ['$injector',
             }));
         }
 
+        // If user can manage user groups, add link to group management page
+        if (canManageUserGroups.length) {
+            pages.push(new PageDefinition({
+                name : 'USER_MENU.ACTION_MANAGE_USER_GROUPS',
+                url  : '/settings/userGroups'
+            }));
+        }
+
         // If user can manage connections, add links for connection management pages
         angular.forEach(canManageConnections, function addConnectionManagementLink(dataSource) {
             pages.push(new PageDefinition({

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8ad3f253/guacamole/src/main/webapp/app/settings/controllers/settingsController.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/settings/controllers/settingsController.js b/guacamole/src/main/webapp/app/settings/controllers/settingsController.js
index 91ef633..a462d87 100644
--- a/guacamole/src/main/webapp/app/settings/controllers/settingsController.js
+++ b/guacamole/src/main/webapp/app/settings/controllers/settingsController.js
@@ -36,8 +36,8 @@ angular.module('manage').controller('settingsController', ['$scope', '$injector'
     $scope.settingsPages = null;
 
     /**
-     * The currently-selected settings tab. This may be 'users', 'connections',
-     * or 'sessions'.
+     * The currently-selected settings tab. This may be 'users', 'userGroups',
+     * 'connections', 'history', 'preferences', or 'sessions'.
      *
      * @type String
      */

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8ad3f253/guacamole/src/main/webapp/app/settings/directives/guacSettingsUserGroups.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsUserGroups.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUserGroups.js
new file mode 100644
index 0000000..5d45bc1
--- /dev/null
+++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUserGroups.js
@@ -0,0 +1,270 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * A directive for managing all user groups in the system.
+ */
+angular.module('settings').directive('guacSettingsUserGroups', ['$injector',
+    function guacSettingsUserGroups($injector) {
+
+    // Required types
+    var ManageableUserGroup = $injector.get('ManageableUserGroup');
+    var PermissionSet       = $injector.get('PermissionSet');
+    var SortOrder           = $injector.get('SortOrder');
+
+    // Required services
+    var $location              = $injector.get('$location');
+    var authenticationService  = $injector.get('authenticationService');
+    var dataSourceService      = $injector.get('dataSourceService');
+    var permissionService      = $injector.get('permissionService');
+    var requestService         = $injector.get('requestService');
+    var userGroupService       = $injector.get('userGroupService');
+
+    var directive = {
+        restrict    : 'E',
+        replace     : true,
+        templateUrl : 'app/settings/templates/settingsUserGroups.html',
+        scope       : {}
+    };
+
+    directive.controller = ['$scope', function settingsUserGroupsController($scope) {
+
+        // Identifier of the current user
+        var currentUsername = authenticationService.getCurrentUsername();
+
+        /**
+         * The identifiers of all data sources accessible by the current
+         * user.
+         *
+         * @type String[]
+         */
+        var dataSources = authenticationService.getAvailableDataSources();
+
+        /**
+         * Map of data source identifiers to all permissions associated
+         * with the current user within that data source, or null if the
+         * user's permissions have not yet been loaded.
+         *
+         * @type Object.<String, PermissionSet>
+         */
+        var permissions = null;
+
+        /**
+         * All visible user groups, along with their corresponding data
+         * sources.
+         *
+         * @type ManageableUserGroup[]
+         */
+        $scope.manageableUserGroups = null;
+
+        /**
+         * Array of all user group properties that are filterable.
+         *
+         * @type String[]
+         */
+        $scope.filteredUserGroupProperties = [
+            'userGroup.identifier'
+        ];
+
+        /**
+         * SortOrder instance which stores the sort order of the listed
+         * user groups.
+         *
+         * @type SortOrder
+         */
+        $scope.order = new SortOrder([
+            'userGroup.identifier'
+        ]);
+
+        /**
+         * Returns whether critical data has completed being loaded.
+         *
+         * @returns {Boolean}
+         *     true if enough data has been loaded for the user group
+         *     interface to be useful, false otherwise.
+         */
+        $scope.isLoaded = function isLoaded() {
+            return $scope.manageableUserGroups !== null;
+        };
+
+        /**
+         * Returns the identifier of the data source that should be used by
+         * default when creating a new user group.
+         *
+         * @return {String}
+         *     The identifier of the data source that should be used by
+         *     default when creating a new user group, or null if user group
+         *     creation is not allowed.
+         */
+        $scope.getDefaultDataSource = function getDefaultDataSource() {
+
+            // Abort if permissions have not yet loaded
+            if (!permissions)
+                return null;
+
+            // For each data source
+            for (var dataSource in permissions) {
+
+                // Retrieve corresponding permission set
+                var permissionSet = permissions[dataSource];
+
+                // Can create user groups if adminstrator or have explicit permission
+                if (PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.ADMINISTER)
+                 || PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.CREATE_USER_GROUP))
+                    return dataSource;
+
+            }
+
+            // No data sources allow user group creation
+            return null;
+
+        };
+
+        /**
+         * Returns whether the current user can create new user groups
+         * within at least one data source.
+         *
+         * @return {Boolean}
+         *     true if the current user can create new user groups within at
+         *     least one data source, false otherwise.
+         */
+        $scope.canCreateUserGroups = function canCreateUserGroups() {
+            return $scope.getDefaultDataSource() !== null;
+        };
+
+        /**
+         * Returns whether the current user can create new user groups or
+         * make changes to existing user groups within at least one data
+         * source. The user group management interface as a whole is useless
+         * if this function returns false.
+         *
+         * @return {Boolean}
+         *     true if the current user can create new user groups or make
+         *     changes to existing user groups within at least one data
+         *     source, false otherwise.
+         */
+        var canManageUserGroups = function canManageUserGroups() {
+
+            // Abort if permissions have not yet loaded
+            if (!permissions)
+                return false;
+
+            // Creating user groups counts as management
+            if ($scope.canCreateUserGroups())
+                return true;
+
+            // For each data source
+            for (var dataSource in permissions) {
+
+                // Retrieve corresponding permission set
+                var permissionSet = permissions[dataSource];
+
+                // Can manage user groups if granted explicit update or delete
+                if (PermissionSet.hasUserGroupPermission(permissionSet, PermissionSet.ObjectPermissionType.UPDATE)
+                 || PermissionSet.hasUserGroupPermission(permissionSet, PermissionSet.ObjectPermissionType.DELETE))
+                    return true;
+
+            }
+
+            // No data sources allow management of user groups
+            return false;
+
+        };
+
+        /**
+         * Sets the displayed list of user groups. If any user groups are
+         * already shown within the interface, those user groups are replaced
+         * with the given user groups.
+         *
+         * @param {Object.<String, PermissionSet>} permissions
+         *     A map of data source identifiers to all permissions associated
+         *     with the current user within that data source.
+         *
+         * @param {Object.<String, Object.<String, UserGroup>>} userGroups
+         *     A map of all user groups which should be displayed, where each
+         *     key is the data source identifier from which the user groups
+         *     were retrieved and each value is a map of user group identifiers
+         *     to their corresponding @link{UserGroup} objects.
+         */
+        var setDisplayedUserGroups = function setDisplayedUserGroups(permissions, userGroups) {
+
+            var addedUserGroups = {};
+            $scope.manageableUserGroups = [];
+
+            // For each user group in each data source
+            angular.forEach(dataSources, function addUserGroupList(dataSource) {
+                angular.forEach(userGroups[dataSource], function addUserGroup(userGroup) {
+
+                    // Do not add the same user group twice
+                    if (addedUserGroups[userGroup.identifier])
+                        return;
+
+                    // Link to default creation data source if we cannot manage this user
+                    if (!PermissionSet.hasSystemPermission(permissions[dataSource], PermissionSet.ObjectPermissionType.ADMINISTER)
+                     && !PermissionSet.hasUserGroupPermission(permissions[dataSource], PermissionSet.ObjectPermissionType.UPDATE, userGroup.identifier)
+                     && !PermissionSet.hasUserGroupPermission(permissions[dataSource], PermissionSet.ObjectPermissionType.DELETE, userGroup.identifier))
+                        dataSource = $scope.getDefaultDataSource();
+
+                    // Add user group to overall list
+                    addedUserGroups[userGroup.identifier] = userGroup;
+                    $scope.manageableUserGroups.push(new ManageableUserGroup ({
+                        'dataSource' : dataSource,
+                        'userGroup'  : userGroup
+                    }));
+
+                });
+            });
+
+        };
+
+        // Retrieve current permissions
+        dataSourceService.apply(
+            permissionService.getEffectivePermissions,
+            dataSources,
+            currentUsername
+        )
+        .then(function permissionsRetrieved(retrievedPermissions) {
+
+            // Store retrieved permissions
+            permissions = retrievedPermissions;
+
+            // Return to home if there's nothing to do here
+            if (!canManageUserGroups())
+                $location.path('/');
+
+            // If user groups can be created, list all readable user groups
+            if ($scope.canCreateUserGroups())
+                return dataSourceService.apply(userGroupService.getUserGroups, dataSources);
+
+            // Otherwise, list only updateable/deletable users
+            return dataSourceService.apply(userGroupService.getUserGroups, dataSources, [
+                PermissionSet.ObjectPermissionType.UPDATE,
+                PermissionSet.ObjectPermissionType.DELETE
+            ]);
+
+        })
+        .then(function userGroupsReceived(userGroups) {
+            setDisplayedUserGroups(permissions, userGroups);
+        }, requestService.WARN);
+
+    }];
+
+    return directive;
+    
+}]);

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8ad3f253/guacamole/src/main/webapp/app/settings/styles/buttons.css
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/settings/styles/buttons.css b/guacamole/src/main/webapp/app/settings/styles/buttons.css
index 17401c3..e530510 100644
--- a/guacamole/src/main/webapp/app/settings/styles/buttons.css
+++ b/guacamole/src/main/webapp/app/settings/styles/buttons.css
@@ -18,6 +18,7 @@
  */
 
 a.button.add-user,
+a.button.add-user-group,
 a.button.add-connection,
 a.button.add-connection-group {
     font-size: 0.8em;
@@ -26,6 +27,7 @@ a.button.add-connection-group {
 }
 
 a.button.add-user::before,
+a.button.add-user-group::before,
 a.button.add-connection::before,
 a.button.add-connection-group::before {
 
@@ -46,6 +48,10 @@ a.button.add-user::before {
     background-image: url('images/action-icons/guac-user-add.png');
 }
 
+a.button.add-user-group::before {
+    background-image: url('images/action-icons/guac-user-group-add.png');
+}
+
 a.button.add-connection::before {
     background-image: url('images/action-icons/guac-monitor-add.png');
 }

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8ad3f253/guacamole/src/main/webapp/app/settings/styles/user-group-list.css
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/settings/styles/user-group-list.css b/guacamole/src/main/webapp/app/settings/styles/user-group-list.css
new file mode 100644
index 0000000..2040eb4
--- /dev/null
+++ b/guacamole/src/main/webapp/app/settings/styles/user-group-list.css
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.settings.user-groups table.user-group-list {
+    width: 100%;
+}
+
+.settings.user-groups table.user-group-list th.user-group-name,
+.settings.user-groups table.user-group-list td.user-group-name {
+    width: 100%;
+}
+
+.settings.user-groups table.user-group-list tr.user td.user-group-name a[href] {
+    display: block;
+    padding: .5em 1em;
+}
+
+.settings.user-groups table.user-group-list tr.user td.user-group-name {
+    padding: 0;
+}

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8ad3f253/guacamole/src/main/webapp/app/settings/templates/settings.html
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/settings/templates/settings.html b/guacamole/src/main/webapp/app/settings/templates/settings.html
index b29d809..2bae3ae 100644
--- a/guacamole/src/main/webapp/app/settings/templates/settings.html
+++ b/guacamole/src/main/webapp/app/settings/templates/settings.html
@@ -13,6 +13,7 @@
 
     <!-- Selected tab -->
     <guac-settings-users                ng-if="activeTab === 'users'"></guac-settings-users>
+    <guac-settings-user-groups          ng-if="activeTab === 'userGroups'"></guac-settings-user-groups>
     <guac-settings-connections          ng-if="activeTab === 'connections'"></guac-settings-connections>
     <guac-settings-connection-history   ng-if="activeTab === 'history'"></guac-settings-connection-history>
     <guac-settings-sessions             ng-if="activeTab === 'sessions'"></guac-settings-sessions>

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8ad3f253/guacamole/src/main/webapp/app/settings/templates/settingsUserGroups.html
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/settings/templates/settingsUserGroups.html b/guacamole/src/main/webapp/app/settings/templates/settingsUserGroups.html
new file mode 100644
index 0000000..1943773
--- /dev/null
+++ b/guacamole/src/main/webapp/app/settings/templates/settingsUserGroups.html
@@ -0,0 +1,48 @@
+<div class="settings section user-groups" ng-class="{loading: !isLoaded()}">
+
+    <!-- User group management -->
+    <p>{{'SETTINGS_USER_GROUPS.HELP_USER_GROUPS' | translate}}</p>
+
+
+    <!-- User management toolbar -->
+    <div class="toolbar">
+
+        <!-- Form action buttons -->
+        <div class="action-buttons">
+            <a class="add-user-group button" ng-show="canCreateUserGroups()"
+               href="#/manage/{{getDefaultDataSource()}}/userGroups/">{{'SETTINGS_USER_GROUPS.ACTION_NEW_USER_GROUP' | translate}}</a>
+        </div>
+
+        <!-- User group filter -->
+        <guac-filter filtered-items="filteredManageableUserGroups" items="manageableUserGroups"
+                     placeholder="'SETTINGS_USER_GROUPS.FIELD_PLACEHOLDER_FILTER' | translate"
+                     properties="filteredUserGroupProperties"></guac-filter>
+
+    </div>
+
+    <!-- List of user groups this user has access to -->
+    <table class="sorted user-group-list">
+        <thead>
+            <tr>
+                <th guac-sort-order="order" guac-sort-property="'userGroup.identifier'" class="user-group-name">
+                    {{'SETTINGS_USER_GROUPS.TABLE_HEADER_USER_GROUP_NAME' | translate}}
+                </th>
+            </tr>
+        </thead>
+        <tbody ng-class="{loading: !isLoaded()}">
+            <tr ng-repeat="manageableUserGroup in manageableUserGroupPage" class="user-group">
+                <td class="user-group-name">
+                    <a ng-href="#/manage/{{manageableUserGroup.dataSource}}/userGroups/{{manageableUserGroup.userGroup.identifier}}">
+                        <div class="icon user-group"></div>
+                        <span class="name">{{manageableUserGroup.userGroup.identifier}}</span>
+                    </a>
+                </td>
+            </tr>
+        </tbody>
+    </table>
+
+    <!-- Pager controls for user group list -->
+    <guac-pager page="manageableUserGroupPage" page-size="25"
+                items="filteredManageableUserGroups | orderBy : order.predicate"></guac-pager>
+
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8ad3f253/guacamole/src/main/webapp/images/action-icons/guac-user-group-add.png
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/images/action-icons/guac-user-group-add.png b/guacamole/src/main/webapp/images/action-icons/guac-user-group-add.png
new file mode 100644
index 0000000..a833433
Binary files /dev/null and b/guacamole/src/main/webapp/images/action-icons/guac-user-group-add.png differ

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8ad3f253/guacamole/src/main/webapp/images/user-icons/guac-user-group.png
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/images/user-icons/guac-user-group.png b/guacamole/src/main/webapp/images/user-icons/guac-user-group.png
new file mode 100644
index 0000000..4eb0aa4
Binary files /dev/null and b/guacamole/src/main/webapp/images/user-icons/guac-user-group.png differ

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/8ad3f253/guacamole/src/main/webapp/translations/en.json
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/translations/en.json b/guacamole/src/main/webapp/translations/en.json
index d0eaa9a..24ab0d7 100644
--- a/guacamole/src/main/webapp/translations/en.json
+++ b/guacamole/src/main/webapp/translations/en.json
@@ -21,6 +21,7 @@
         "ACTION_MANAGE_SETTINGS"    : "Settings",
         "ACTION_MANAGE_SESSIONS"    : "Active Sessions",
         "ACTION_MANAGE_USERS"       : "Users",
+        "ACTION_MANAGE_USER_GROUPS" : "Groups",
         "ACTION_NAVIGATE_BACK"      : "Back",
         "ACTION_NAVIGATE_HOME"      : "Home",
         "ACTION_SAVE"               : "Save",
@@ -292,6 +293,7 @@
         "FIELD_HEADER_ADMINISTER_SYSTEM"             : "Administer system:",
         "FIELD_HEADER_CHANGE_OWN_PASSWORD"           : "Change own password:",
         "FIELD_HEADER_CREATE_NEW_USERS"              : "Create new users:",
+        "FIELD_HEADER_CREATE_NEW_USER_GROUPS"        : "Create new user groups:",
         "FIELD_HEADER_CREATE_NEW_CONNECTIONS"        : "Create new connections:",
         "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS"  : "Create new connection groups:",
         "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES"   : "Create new sharing profiles:",
@@ -316,6 +318,49 @@
         "TEXT_CONFIRM_DELETE" : "Users cannot be restored after they have been deleted. Are you sure you want to delete this user?"
 
     },
+
+    "MANAGE_USER_GROUP" : {
+
+        "ACTION_ACKNOWLEDGE"   : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"        : "@:APP.ACTION_CANCEL",
+        "ACTION_CLONE"         : "@:APP.ACTION_CLONE",
+        "ACTION_DELETE"        : "@:APP.ACTION_DELETE",
+        "ACTION_SAVE"          : "@:APP.ACTION_SAVE",
+
+        "DIALOG_HEADER_CONFIRM_DELETE" : "Delete Group",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_HEADER_ADMINISTER_SYSTEM"             : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM",
+        "FIELD_HEADER_CHANGE_OWN_PASSWORD"           : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD",
+        "FIELD_HEADER_CREATE_NEW_USERS"              : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS",
+        "FIELD_HEADER_CREATE_NEW_USER_GROUPS"        : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS",
+        "FIELD_HEADER_CREATE_NEW_CONNECTIONS"        : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS",
+        "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS"  : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS",
+        "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES"   : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES",
+        "FIELD_HEADER_USER_GROUP_NAME"               : "Group name:",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "HELP_NO_USER_GROUPS"        : "This group does not currently belong to any groups. Expand this section to add groups.",
+        "HELP_NO_MEMBER_USER_GROUPS" : "This group does not currently contain any groups. Expand this section to add groups.",
+        "HELP_NO_MEMBER_USERS"       : "This group does not currently contain any users. Expand this section to add users.",
+
+        "INFO_READ_ONLY"                : "Sorry, but this group cannot be edited.",
+        "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE",
+        "INFO_NO_USERS_AVAILABLE"       : "No users available.",
+
+        "SECTION_HEADER_ALL_CONNECTIONS"     : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS",
+        "SECTION_HEADER_CONNECTIONS"         : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS",
+        "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS",
+        "SECTION_HEADER_EDIT_USER_GROUP"     : "Edit Group",
+        "SECTION_HEADER_MEMBER_USERS"        : "Member Users",
+        "SECTION_HEADER_MEMBER_USER_GROUPS"  : "Member Groups",
+        "SECTION_HEADER_PERMISSIONS"         : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS",
+        "SECTION_HEADER_USER_GROUPS"         : "Parent Groups",
+
+        "TEXT_CONFIRM_DELETE" : "Groups cannot be restored after they have been deleted. Are you sure you want to delete this group?"
+
+    },
     
     "PROTOCOL_RDP" : {
 
@@ -747,7 +792,26 @@
         "TABLE_HEADER_USERNAME"    : "Username"
 
     },
-    
+
+    "SETTINGS_USER_GROUPS" : {
+
+        "ACTION_ACKNOWLEDGE"    : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_NEW_USER_GROUP" : "New Group",
+
+        "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
+
+        "HELP_USER_GROUPS" : "Click or tap on a group below to manage that group. Depending on your access level, groups can be added and deleted, and their member users and groups can be changed.",
+
+        "SECTION_HEADER_USER_GROUPS" : "Groups",
+
+        "TABLE_HEADER_USER_GROUP_NAME" : "Group Name"
+
+    },
+
     "SETTINGS_SESSIONS" : {
         
         "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
@@ -793,6 +857,7 @@
         "ACTION_MANAGE_SESSIONS"    : "@:APP.ACTION_MANAGE_SESSIONS",
         "ACTION_MANAGE_SETTINGS"    : "@:APP.ACTION_MANAGE_SETTINGS",
         "ACTION_MANAGE_USERS"       : "@:APP.ACTION_MANAGE_USERS",
+        "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS",
         "ACTION_NAVIGATE_HOME"      : "@:APP.ACTION_NAVIGATE_HOME",
         "ACTION_VIEW_HISTORY"       : "@:APP.ACTION_VIEW_HISTORY"
 


[11/12] guacamole-client git commit: GUACAMOLE-220: Allow attributes to be specified via the template supplied to REST object constructors, for consistency's sake.

Posted by vn...@apache.org.
GUACAMOLE-220: Allow attributes to be specified via the template supplied to REST object constructors, for consistency's sake.

Project: http://git-wip-us.apache.org/repos/asf/guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/guacamole-client/commit/7917f46b
Tree: http://git-wip-us.apache.org/repos/asf/guacamole-client/tree/7917f46b
Diff: http://git-wip-us.apache.org/repos/asf/guacamole-client/diff/7917f46b

Branch: refs/heads/staging/1.0.0
Commit: 7917f46b36fb04565c0e1d4119c0136c8f987fcd
Parents: 6aaef76
Author: Michael Jumper <mj...@apache.org>
Authored: Thu Aug 9 10:43:46 2018 -0700
Committer: Michael Jumper <mj...@apache.org>
Committed: Thu Aug 9 10:46:06 2018 -0700

----------------------------------------------------------------------
 guacamole/src/main/webapp/app/rest/types/Connection.js      | 2 +-
 guacamole/src/main/webapp/app/rest/types/ConnectionGroup.js | 2 +-
 guacamole/src/main/webapp/app/rest/types/SharingProfile.js  | 2 +-
 guacamole/src/main/webapp/app/rest/types/User.js            | 2 +-
 guacamole/src/main/webapp/app/rest/types/UserGroup.js       | 2 +-
 5 files changed, 5 insertions(+), 5 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/7917f46b/guacamole/src/main/webapp/app/rest/types/Connection.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/rest/types/Connection.js b/guacamole/src/main/webapp/app/rest/types/Connection.js
index 76ece9d..89da4e1 100644
--- a/guacamole/src/main/webapp/app/rest/types/Connection.js
+++ b/guacamole/src/main/webapp/app/rest/types/Connection.js
@@ -84,7 +84,7 @@ angular.module('rest').factory('Connection', [function defineConnection() {
          *
          * @type Object.<String, String>
          */
-        this.attributes = {};
+        this.attributes = template.attributes || {};
 
         /**
          * The count of currently active connections using this connection.

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/7917f46b/guacamole/src/main/webapp/app/rest/types/ConnectionGroup.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/rest/types/ConnectionGroup.js b/guacamole/src/main/webapp/app/rest/types/ConnectionGroup.js
index a40dba1..6da754c 100644
--- a/guacamole/src/main/webapp/app/rest/types/ConnectionGroup.js
+++ b/guacamole/src/main/webapp/app/rest/types/ConnectionGroup.js
@@ -95,7 +95,7 @@ angular.module('rest').factory('ConnectionGroup', [function defineConnectionGrou
          *
          * @type Object.<String, String>
          */
-        this.attributes = {};
+        this.attributes = template.attributes || {};
 
         /**
          * The count of currently active connections using this connection

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/7917f46b/guacamole/src/main/webapp/app/rest/types/SharingProfile.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/rest/types/SharingProfile.js b/guacamole/src/main/webapp/app/rest/types/SharingProfile.js
index ea8287d..50f1307 100644
--- a/guacamole/src/main/webapp/app/rest/types/SharingProfile.js
+++ b/guacamole/src/main/webapp/app/rest/types/SharingProfile.js
@@ -76,7 +76,7 @@ angular.module('rest').factory('SharingProfile', [function defineSharingProfile(
          *
          * @type Object.<String, String>
          */
-        this.attributes = {};
+        this.attributes = template.attributes || {};
 
     };
 

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/7917f46b/guacamole/src/main/webapp/app/rest/types/User.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/rest/types/User.js b/guacamole/src/main/webapp/app/rest/types/User.js
index f796147..3ca138d 100644
--- a/guacamole/src/main/webapp/app/rest/types/User.js
+++ b/guacamole/src/main/webapp/app/rest/types/User.js
@@ -69,7 +69,7 @@ angular.module('rest').factory('User', [function defineUser() {
          *
          * @type Object.<String, String>
          */
-        this.attributes = {};
+        this.attributes = template.attributes || {};
 
     };
 

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/7917f46b/guacamole/src/main/webapp/app/rest/types/UserGroup.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/rest/types/UserGroup.js b/guacamole/src/main/webapp/app/rest/types/UserGroup.js
index 03b73e2..f4bf26c 100644
--- a/guacamole/src/main/webapp/app/rest/types/UserGroup.js
+++ b/guacamole/src/main/webapp/app/rest/types/UserGroup.js
@@ -50,7 +50,7 @@ angular.module('rest').factory('UserGroup', [function defineUserGroup() {
          *
          * @type Object.<String, String>
          */
-        this.attributes = {};
+        this.attributes = template.attributes || {};
 
     };
 


[09/12] guacamole-client git commit: GUACAMOLE-220: Select from multiple datasources deterministically.

Posted by vn...@apache.org.
GUACAMOLE-220: Select from multiple datasources deterministically.

Project: http://git-wip-us.apache.org/repos/asf/guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/guacamole-client/commit/6aaef768
Tree: http://git-wip-us.apache.org/repos/asf/guacamole-client/tree/6aaef768
Diff: http://git-wip-us.apache.org/repos/asf/guacamole-client/diff/6aaef768

Branch: refs/heads/staging/1.0.0
Commit: 6aaef7685f8b455bc4b7854d8aef8617a3d7b4f1
Parents: 8ad3f25
Author: Michael Jumper <mj...@apache.org>
Authored: Wed Aug 8 22:56:07 2018 -0700
Committer: Michael Jumper <mj...@apache.org>
Committed: Thu Aug 9 10:46:06 2018 -0700

----------------------------------------------------------------------
 .../src/main/webapp/app/manage/directives/dataSourceTabs.js     | 5 ++++-
 .../webapp/app/settings/directives/guacSettingsUserGroups.js    | 4 +++-
 .../main/webapp/app/settings/directives/guacSettingsUsers.js    | 4 +++-
 3 files changed, 10 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/6aaef768/guacamole/src/main/webapp/app/manage/directives/dataSourceTabs.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/manage/directives/dataSourceTabs.js b/guacamole/src/main/webapp/app/manage/directives/dataSourceTabs.js
index cf7068f..627197b 100644
--- a/guacamole/src/main/webapp/app/manage/directives/dataSourceTabs.js
+++ b/guacamole/src/main/webapp/app/manage/directives/dataSourceTabs.js
@@ -76,9 +76,12 @@ angular.module('manage').directive('dataSourceTabs', ['$injector',
         $scope.$watch('permissions', function permissionsChanged(permissions) {
 
             $scope.pages = [];
-            angular.forEach(permissions, function addDataSourcePage(managementPermissions, dataSource) {
+
+            var dataSources = _.keys($scope.permissions).sort();
+            angular.forEach(dataSources, function addDataSourcePage(dataSource) {
 
                 // Determine whether data source contains this object
+                var managementPermissions = permissions[dataSource];
                 var exists = !!managementPermissions.identifier;
 
                 // Data source is not relevant if the associated object does not

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/6aaef768/guacamole/src/main/webapp/app/settings/directives/guacSettingsUserGroups.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsUserGroups.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUserGroups.js
index 5d45bc1..a8f2dd3 100644
--- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsUserGroups.js
+++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUserGroups.js
@@ -119,9 +119,11 @@ angular.module('settings').directive('guacSettingsUserGroups', ['$injector',
                 return null;
 
             // For each data source
-            for (var dataSource in permissions) {
+            var dataSources = _.keys(permissions).sort();
+            for (var i = 0; i < dataSources.length; i++) {
 
                 // Retrieve corresponding permission set
+                var dataSource = dataSources[i];
                 var permissionSet = permissions[dataSource];
 
                 // Can create user groups if adminstrator or have explicit permission

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/6aaef768/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js
index 870a862..4adf04e 100644
--- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js
+++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js
@@ -150,9 +150,11 @@ angular.module('settings').directive('guacSettingsUsers', [function guacSettings
                     return null;
 
                 // For each data source
-                for (var dataSource in $scope.permissions) {
+                var dataSources = _.keys($scope.permissions).sort();
+                for (var i = 0; i < dataSources.length; i++) {
 
                     // Retrieve corresponding permission set
+                    var dataSource = dataSources[i];
                     var permissionSet = $scope.permissions[dataSource];
 
                     // Can create users if adminstrator or have explicit permission


[08/12] guacamole-client git commit: GUACAMOLE-220: Add management interface for user parent groups.

Posted by vn...@apache.org.
GUACAMOLE-220: Add management interface for user parent groups.


Project: http://git-wip-us.apache.org/repos/asf/guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/guacamole-client/commit/de809574
Tree: http://git-wip-us.apache.org/repos/asf/guacamole-client/tree/de809574
Diff: http://git-wip-us.apache.org/repos/asf/guacamole-client/diff/de809574

Branch: refs/heads/staging/1.0.0
Commit: de8095740435de7fcfddf263304aa8be2eb553dd
Parents: ca1db78
Author: Michael Jumper <mj...@apache.org>
Authored: Tue Aug 7 12:15:46 2018 -0700
Committer: Michael Jumper <mj...@apache.org>
Committed: Wed Aug 8 21:50:26 2018 -0700

----------------------------------------------------------------------
 .../manage/controllers/manageUserController.js  | 69 ++++++++++++++++++--
 .../webapp/app/manage/templates/manageUser.html | 11 ++++
 guacamole/src/main/webapp/translations/en.json  |  6 +-
 3 files changed, 80 insertions(+), 6 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/de809574/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js
index e4a91db..7d8397f 100644
--- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js
+++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js
@@ -36,9 +36,11 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
     var $q                       = $injector.get('$q');
     var authenticationService    = $injector.get('authenticationService');
     var dataSourceService        = $injector.get('dataSourceService');
+    var membershipService        = $injector.get('membershipService');
     var permissionService        = $injector.get('permissionService');
     var requestService           = $injector.get('requestService');
     var schemaService            = $injector.get('schemaService');
+    var userGroupService         = $injector.get('userGroupService');
     var userService              = $injector.get('userService');
 
     /**
@@ -134,6 +136,46 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
     $scope.permissionsRemoved = new PermissionSet();
 
     /**
+     * The identifiers of all user groups which can be manipulated (all groups
+     * for which the user accessing this interface has UPDATE permission),
+     * either through adding the current user as a member or removing the
+     * current user from that group. If this information has not yet been
+     * retrieved, this will be null.
+     *
+     * @type String[]
+     */
+    $scope.availableGroups = null;
+
+    /**
+     * The identifiers of all user groups of which the user is a member,
+     * taking into account any user groups which will be added/removed when
+     * saved. If this information has not yet been retrieved, this will be
+     * null.
+     *
+     * @type String[]
+     */
+    $scope.parentGroups = null;
+
+    /**
+     * The set of identifiers of all parent user groups to which the user will
+     * be added when saved. Parent groups will only be present in this set if
+     * they are manually added, and not later manually removed before saving.
+     *
+     * @type String[]
+     */
+    $scope.parentGroupsAdded = [];
+
+    /**
+     * The set of identifiers of all parent user groups from which the user
+     * will be removed when saved. Parent groups will only be present in this
+     * set if they are manually removed, and not later manually added before
+     * saving.
+     *
+     * @type String[]
+     */
+    $scope.parentGroupsRemoved = [];
+
+    /**
      * For each applicable data source, the management-related actions that the
      * current user may perform on the user account currently being created
      * or modified, as a map of data source identifier to the
@@ -166,6 +208,8 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
         return $scope.users                 !== null
             && $scope.permissionFlags       !== null
             && $scope.managementPermissions !== null
+            && $scope.availableGroups       !== null
+            && $scope.parentGroups          !== null
             && $scope.attributes            !== null;
 
     };
@@ -204,12 +248,14 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
     var loadExistingUser = function loadExistingUser(dataSource, username) {
         return $q.all({
             users : dataSourceService.apply(userService.getUser, dataSources, username),
-            permissions : permissionService.getPermissions(dataSource, username)
+            permissions : permissionService.getPermissions(dataSource, username),
+            parentGroups : membershipService.getUserGroups(dataSource, username)
         })
         .then(function userDataRetrieved(values) {
 
             $scope.users = values.users;
             $scope.user  = values.users[dataSource];
+            $scope.parentGroups = values.parentGroups;
 
             // Create skeleton user if user does not exist
             if (!$scope.user)
@@ -243,12 +289,15 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
     var loadClonedUser = function loadClonedUser(dataSource, username) {
         return $q.all({
             users : dataSourceService.apply(userService.getUser, [dataSource], username),
-            permissions : permissionService.getPermissions(dataSource, username)
+            permissions : permissionService.getPermissions(dataSource, username),
+            parentGroups : membershipService.getUserGroups(dataSource, username)
         })
         .then(function userDataRetrieved(values) {
 
             $scope.users = {};
             $scope.user  = values.users[dataSource];
+            $scope.parentGroups = values.parentGroups;
+            $scope.parentGroupsAdded = values.parentGroups;
 
             // The current user will be associated with cloneSourceUsername in the
             // retrieved permission set
@@ -274,6 +323,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
 
         // Use skeleton user object with no associated permissions
         $scope.user = new User();
+        $scope.parentGroups = [];
         $scope.permissionFlags = new PermissionFlagSet();
 
         // As no permissions are yet associated with the user, it is safe to
@@ -314,6 +364,7 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
     $q.all({
         userData    : loadRequestedUser(),
         permissions : dataSourceService.apply(permissionService.getEffectivePermissions, dataSources, currentUsername),
+        userGroups  : userGroupService.getUserGroups($scope.dataSource, [ PermissionSet.ObjectPermissionType.UPDATE ]),
         attributes  : schemaService.getUserAttributes($scope.dataSource)
     })
     .then(function dataReceived(values) {
@@ -326,6 +377,12 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
             // Determine whether data source contains this user
             var exists = (dataSource in $scope.users);
 
+            // Add the identifiers of all modifiable user groups
+            $scope.availableGroups = [];
+            angular.forEach(values.userGroups, function addUserGroupIdentifier(userGroup) {
+                $scope.availableGroups.push(userGroup.identifier);
+            });
+
             // Calculate management actions available for this specific account
             $scope.managementPermissions[dataSource] = ManagementPermissions.fromPermissionSet(
                     values.permissions[dataSource],
@@ -415,9 +472,11 @@ angular.module('manage').controller('manageUserController', ['$scope', '$injecto
                 
             }
 
-            // Upon success, save any changed permissions
-            return permissionService.patchPermissions($scope.dataSource, $scope.user.username,
-                $scope.permissionsAdded, $scope.permissionsRemoved);
+            // Upon success, save any changed permissions/groups
+            return $q.all([
+                permissionService.patchPermissions($scope.dataSource, $scope.user.username, $scope.permissionsAdded, $scope.permissionsRemoved),
+                membershipService.patchUserGroups($scope.dataSource, $scope.user.username, $scope.parentGroupsAdded, $scope.parentGroupsRemoved)
+            ]);
 
         });
 

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/de809574/guacamole/src/main/webapp/app/manage/templates/manageUser.html
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/manage/templates/manageUser.html b/guacamole/src/main/webapp/app/manage/templates/manageUser.html
index bb5cfb3..571f5c8 100644
--- a/guacamole/src/main/webapp/app/manage/templates/manageUser.html
+++ b/guacamole/src/main/webapp/app/manage/templates/manageUser.html
@@ -56,6 +56,17 @@
               permissions-removed="permissionsRemoved">
         </system-permission-editor>
 
+        <!-- Parent group section -->
+        <identifier-set-editor
+            header="MANAGE_USER.SECTION_HEADER_USER_GROUPS"
+            empty-placeholder="MANAGE_USER.HELP_NO_USER_GROUPS"
+            unavailable-placeholder="MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE"
+            identifiers-available="availableGroups"
+            identifiers="parentGroups"
+            identifiers-added="parentGroupsAdded"
+            identifiers-removed="parentGroupsRemoved">
+        </identifier-set-editor>
+
         <!-- Connection permissions section -->
         <connection-permission-editor ng-show="managementPermissions[dataSource].canChangePermissions"
               data-data-source="dataSource"

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/de809574/guacamole/src/main/webapp/translations/en.json
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/translations/en.json b/guacamole/src/main/webapp/translations/en.json
index d55e262..d0eaa9a 100644
--- a/guacamole/src/main/webapp/translations/en.json
+++ b/guacamole/src/main/webapp/translations/en.json
@@ -301,13 +301,17 @@
 
         "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
 
-        "INFO_READ_ONLY" : "Sorry, but this user account cannot be edited.",
+        "HELP_NO_USER_GROUPS" : "This user does not currently belong to any groups. Expand this section to add groups.",
+
+        "INFO_READ_ONLY"                : "Sorry, but this user account cannot be edited.",
+        "INFO_NO_USER_GROUPS_AVAILABLE" : "No groups available.",
 
         "SECTION_HEADER_ALL_CONNECTIONS"     : "All Connections",
         "SECTION_HEADER_CONNECTIONS"         : "Connections",
         "SECTION_HEADER_CURRENT_CONNECTIONS" : "Current Connections",
         "SECTION_HEADER_EDIT_USER"           : "Edit User",
         "SECTION_HEADER_PERMISSIONS"         : "Permissions",
+        "SECTION_HEADER_USER_GROUPS"         : "Groups",
 
         "TEXT_CONFIRM_DELETE" : "Users cannot be restored after they have been deleted. Are you sure you want to delete this user?"
 


[02/12] guacamole-client git commit: GUACAMOLE-220: Add JavaScript service for retrieving/manipulating user groups.

Posted by vn...@apache.org.
GUACAMOLE-220: Add JavaScript service for retrieving/manipulating user groups.


Project: http://git-wip-us.apache.org/repos/asf/guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/guacamole-client/commit/9f01fcb1
Tree: http://git-wip-us.apache.org/repos/asf/guacamole-client/tree/9f01fcb1
Diff: http://git-wip-us.apache.org/repos/asf/guacamole-client/diff/9f01fcb1

Branch: refs/heads/staging/1.0.0
Commit: 9f01fcb1558b11b52c78bcddee2ea601ab4b102c
Parents: c36d333
Author: Michael Jumper <mj...@apache.org>
Authored: Thu Apr 19 14:38:24 2018 -0700
Committer: Michael Jumper <mj...@apache.org>
Committed: Wed Aug 8 09:00:06 2018 -0700

----------------------------------------------------------------------
 .../webapp/app/rest/services/cacheService.js    |   3 +-
 .../app/rest/services/membershipService.js      | 385 +++++++++++++++++++
 .../app/rest/services/userGroupService.js       | 223 +++++++++++
 .../webapp/app/rest/types/RelatedObjectPatch.js |  85 ++++
 .../src/main/webapp/app/rest/types/UserGroup.js |  59 +++
 5 files changed, 754 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/9f01fcb1/guacamole/src/main/webapp/app/rest/services/cacheService.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/rest/services/cacheService.js b/guacamole/src/main/webapp/app/rest/services/cacheService.js
index 55b7fc1..9a32004 100644
--- a/guacamole/src/main/webapp/app/rest/services/cacheService.js
+++ b/guacamole/src/main/webapp/app/rest/services/cacheService.js
@@ -60,7 +60,8 @@ angular.module('rest').factory('cacheService', ['$injector',
     service.schema = $cacheFactory('API-SCHEMA');
 
     /**
-     * Shared cache used by both userService and permissionService.
+     * Shared cache used by userService, userGroupService, permissionService,
+     * and membershipService.
      *
      * @type $cacheFactory.Cache
      */

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/9f01fcb1/guacamole/src/main/webapp/app/rest/services/membershipService.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/rest/services/membershipService.js b/guacamole/src/main/webapp/app/rest/services/membershipService.js
new file mode 100644
index 0000000..58181c8
--- /dev/null
+++ b/guacamole/src/main/webapp/app/rest/services/membershipService.js
@@ -0,0 +1,385 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Service for operating on user group memberships via the REST API.
+ */
+angular.module('rest').factory('membershipService', ['$injector',
+        function membershipService($injector) {
+
+    // Required services
+    var requestService        = $injector.get('requestService');
+    var authenticationService = $injector.get('authenticationService');
+    var cacheService          = $injector.get('cacheService');
+    
+    // Required types
+    var RelatedObjectPatch = $injector.get('RelatedObjectPatch');
+
+    var service = {};
+
+    /**
+     * Creates a new array of patches which represents the given changes to an
+     * arbitrary set of objects sharing some common relation.
+     *
+     * @param {String[]} [identifiersToAdd]
+     *     The identifiers of all objects which should be added to the
+     *     relation, if any.
+     *
+     * @param {String[]} [identifiersToRemove]
+     *     The identifiers of all objects which should be removed from the
+     *     relation, if any.
+     *
+     * @returns {RelatedObjectPatch[]}
+     *     A new array of patches which represents the given changes.
+     */
+    var getRelatedObjectPatch = function getRelatedObjectPatch(identifiersToAdd, identifiersToRemove) {
+
+        var patch = [];
+
+        angular.forEach(identifiersToAdd, function addIdentifier(identifier) {
+            patch.push(new RelatedObjectPatch({
+                op    : RelatedObjectPatch.Operation.ADD,
+                value : identifier
+            }));
+        });
+
+        angular.forEach(identifiersToRemove, function removeIdentifier(identifier) {
+            patch.push(new RelatedObjectPatch({
+                op    : RelatedObjectPatch.Operation.REMOVE,
+                value : identifier
+            }));
+        });
+
+        return patch;
+
+    };
+
+    /**
+     * Returns the URL for the REST resource most appropriate for accessing
+     * the parent user groups of the user or group having the given identifier.
+     *
+     * It is important to note that a particular data source can authenticate
+     * and provide user groups for a user, even if that user does not exist
+     * within that data source (and thus cannot be found beneath
+     * "api/session/data/{dataSource}/users")
+     *
+     * @param {String} dataSource
+     *     The unique identifier of the data source containing the user or
+     *     group whose parent user groups should be retrieved. This identifier
+     *     corresponds to an AuthenticationProvider within the Guacamole web
+     *     application.
+     *
+     * @param {String} identifier
+     *     The identifier of the user or group for which the URL of the proper
+     *     REST resource should be derived.
+     *
+     * @param {Boolean} [group]
+     *     Whether the provided identifier refers to a user group. If false or
+     *     omitted, the identifier given is assumed to refer to a user.
+     *
+     * @returns {String}
+     *     The URL for the REST resource representing the parent user groups of
+     *     the user or group having the given identifier.
+     */
+    var getUserGroupsResourceURL = function getUserGroupsResourceURL(dataSource, identifier, group) {
+
+        // Create base URL for data source
+        var base = 'api/session/data/' + encodeURIComponent(dataSource);
+
+        // Access parent groups directly (there is no "self" for user groups
+        // as there is for users)
+        if (group)
+            return base + '/userGroups/' + encodeURIComponent(identifier) + '/userGroups';
+
+        // If the username is that of the current user, do not rely on the
+        // user actually existing (they may not). Access their parent groups via
+        // "self" rather than the collection of defined users.
+        if (identifier === authenticationService.getCurrentUsername())
+            return base + '/self/userGroups';
+
+        // Otherwise, the user must exist for their parent groups to be
+        // accessible. Use the collection of defined users.
+        return base + '/users/' + encodeURIComponent(identifier) + '/userGroups';
+
+    };
+
+    /**
+     * Makes a request to the REST API to retrieve the identifiers of all
+     * parent user groups of which a given user or group is a member, returning
+     * a promise that can be used for processing the results of the call.
+     *
+     * @param {String} dataSource
+     *     The unique identifier of the data source containing the user or
+     *     group whose parent user groups should be retrieved. This identifier
+     *     corresponds to an AuthenticationProvider within the Guacamole web
+     *     application.
+     *
+     * @param {String} identifier
+     *     The identifier of the user or group to retrieve the parent user
+     *     groups of.
+     *
+     * @param {Boolean} [group]
+     *     Whether the provided identifier refers to a user group. If false or
+     *     omitted, the identifier given is assumed to refer to a user.
+     *
+     * @returns {Promise.<String[]>}
+     *     A promise for the HTTP call which will resolve with an array
+     *     containing the requested identifiers upon success.
+     */
+    service.getUserGroups = function getUserGroups(dataSource, identifier, group) {
+
+        // Build HTTP parameters set
+        var httpParameters = {
+            token : authenticationService.getCurrentToken()
+        };
+
+        // Retrieve parent groups
+        return requestService({
+            cache   : cacheService.users,
+            method  : 'GET',
+            url     : getUserGroupsResourceURL(dataSource, identifier, group),
+            params  : httpParameters
+        });
+
+    };
+
+    /**
+     * Makes a request to the REST API to modify the parent user groups of
+     * which a given user or group is a member, returning a promise that can be
+     * used for processing the results of the call.
+     *
+     * @param {String} dataSource
+     *     The unique identifier of the data source containing the user or
+     *     group whose parent user groups should be modified. This identifier
+     *     corresponds to an AuthenticationProvider within the Guacamole web
+     *     application.
+     *
+     * @param {String} identifier
+     *     The identifier of the user or group to modify the parent user
+     *     groups of.
+     *
+     * @param {String[]} [addToUserGroups]
+     *     The identifier of all parent user groups to which the given user or
+     *     group should be added as a member, if any.
+     *
+     * @param {String[]} [removeFromUserGroups]
+     *     The identifier of all parent user groups from which the given member
+     *     user or group should be removed, if any.
+     *
+     * @param {Boolean} [group]
+     *     Whether the provided identifier refers to a user group. If false or
+     *     omitted, the identifier given is assumed to refer to a user.
+     *
+     * @returns {Promise}
+     *     A promise for the HTTP call which will succeed if and only if the
+     *     patch operation is successful.
+     */
+    service.patchUserGroups = function patchUserGroups(dataSource, identifier,
+            addToUserGroups, removeFromUserGroups, group) {
+
+        // Build HTTP parameters set
+        var httpParameters = {
+            token : authenticationService.getCurrentToken()
+        };
+
+        // Update parent user groups
+        return requestService({
+            method  : 'PATCH',
+            url     : getUserGroupsResourceURL(dataSource, identifier, group),
+            params  : httpParameters,
+            data    : getRelatedObjectPatch(addToUserGroups, removeFromUserGroups)
+        })
+
+        // Clear the cache
+        .then(function parentUserGroupsChanged(){
+            cacheService.users.removeAll();
+        });
+
+    };
+
+    /**
+     * Makes a request to the REST API to retrieve the identifiers of all
+     * users which are members of the given user group, returning a promise
+     * that can be used for processing the results of the call.
+     *
+     * @param {String} dataSource
+     *     The unique identifier of the data source containing the user group
+     *     whose member users should be retrieved. This identifier corresponds
+     *     to an AuthenticationProvider within the Guacamole web application.
+     *
+     * @param {String} identifier
+     *     The identifier of the user group to retrieve the member users of.
+     *
+     * @returns {Promise.<String[]>}
+     *     A promise for the HTTP call which will resolve with an array
+     *     containing the requested identifiers upon success.
+     */
+    service.getMemberUsers = function getMemberUsers(dataSource, identifier) {
+
+        // Build HTTP parameters set
+        var httpParameters = {
+            token : authenticationService.getCurrentToken()
+        };
+
+        // Retrieve member users
+        return requestService({
+            cache   : cacheService.users,
+            method  : 'GET',
+            url     : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier) + '/memberUsers',
+            params  : httpParameters
+        });
+
+    };
+
+    /**
+     * Makes a request to the REST API to modify the member users of a given
+     * user group, returning a promise that can be used for processing the
+     * results of the call.
+     *
+     * @param {String} dataSource
+     *     The unique identifier of the data source containing the user group
+     *     whose member users should be modified. This identifier corresponds
+     *     to an AuthenticationProvider within the Guacamole web application.
+     *
+     * @param {String} identifier
+     *     The identifier of the user group to modify the member users of.
+     *
+     * @param {String[]} [usersToAdd]
+     *     The identifier of all users to add as members of the given user
+     *     group, if any.
+     *
+     * @param {String[]} [usersToRemove]
+     *     The identifier of all users to remove from the given user group,
+     *     if any.
+     *
+     * @returns {Promise}
+     *     A promise for the HTTP call which will succeed if and only if the
+     *     patch operation is successful.
+     */
+    service.patchMemberUsers = function patchMemberUsers(dataSource, identifier,
+            usersToAdd, usersToRemove) {
+
+        // Build HTTP parameters set
+        var httpParameters = {
+            token : authenticationService.getCurrentToken()
+        };
+
+        // Update member users
+        return requestService({
+            method  : 'PATCH',
+            url     : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier) + '/memberUsers',
+            params  : httpParameters,
+            data    : getRelatedObjectPatch(usersToAdd, usersToRemove)
+        })
+
+        // Clear the cache
+        .then(function memberUsersChanged(){
+            cacheService.users.removeAll();
+        });
+
+    };
+
+    /**
+     * Makes a request to the REST API to retrieve the identifiers of all
+     * user groups which are members of the given user group, returning a
+     * promise that can be used for processing the results of the call.
+     *
+     * @param {String} dataSource
+     *     The unique identifier of the data source containing the user group
+     *     whose member user groups should be retrieved. This identifier
+     *     corresponds to an AuthenticationProvider within the Guacamole web
+     *     application.
+     *
+     * @param {String} identifier
+     *     The identifier of the user group to retrieve the member user
+     *     groups of.
+     *
+     * @returns {Promise.<String[]>}
+     *     A promise for the HTTP call which will resolve with an array
+     *     containing the requested identifiers upon success.
+     */
+    service.getMemberUserGroups = function getMemberUserGroups(dataSource, identifier) {
+
+        // Build HTTP parameters set
+        var httpParameters = {
+            token : authenticationService.getCurrentToken()
+        };
+
+        // Retrieve member user groups
+        return requestService({
+            cache   : cacheService.users,
+            method  : 'GET',
+            url     : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier) + '/memberUserGroups',
+            params  : httpParameters
+        });
+
+    };
+
+    /**
+     * Makes a request to the REST API to modify the member user groups of a
+     * given user group, returning a promise that can be used for processing
+     * the results of the call.
+     *
+     * @param {String} dataSource
+     *     The unique identifier of the data source containing the user group
+     *     whose member user groups should be modified. This identifier
+     *     corresponds to an AuthenticationProvider within the Guacamole web
+     *     application.
+     *
+     * @param {String} identifier
+     *     The identifier of the user group to modify the member user groups of.
+     *
+     * @param {String[]} [userGroupsToAdd]
+     *     The identifier of all user groups to add as members of the given
+     *     user group, if any.
+     *
+     * @param {String[]} [userGroupsToRemove]
+     *     The identifier of all member user groups to remove from the given
+     *     user group, if any.
+     *
+     * @returns {Promise}
+     *     A promise for the HTTP call which will succeed if and only if the
+     *     patch operation is successful.
+     */
+    service.patchMemberUserGroups = function patchMemberUserGroups(dataSource,
+            identifier, userGroupsToAdd, userGroupsToRemove) {
+
+        // Build HTTP parameters set
+        var httpParameters = {
+            token : authenticationService.getCurrentToken()
+        };
+
+        // Update member user groups
+        return requestService({
+            method  : 'PATCH',
+            url     : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier) + '/memberUserGroups',
+            params  : httpParameters,
+            data    : getRelatedObjectPatch(userGroupsToAdd, userGroupsToRemove)
+        })
+
+        // Clear the cache
+        .then(function memberUserGroupsChanged(){
+            cacheService.users.removeAll();
+        });
+
+    };
+    
+    return service;
+
+}]);

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/9f01fcb1/guacamole/src/main/webapp/app/rest/services/userGroupService.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/rest/services/userGroupService.js b/guacamole/src/main/webapp/app/rest/services/userGroupService.js
new file mode 100644
index 0000000..ad29837
--- /dev/null
+++ b/guacamole/src/main/webapp/app/rest/services/userGroupService.js
@@ -0,0 +1,223 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Service for operating on user groups via the REST API.
+ */
+angular.module('rest').factory('userGroupService', ['$injector',
+        function userGroupService($injector) {
+
+    // Required services
+    var requestService        = $injector.get('requestService');
+    var authenticationService = $injector.get('authenticationService');
+    var cacheService          = $injector.get('cacheService');
+
+    var service = {};
+
+    /**
+     * Makes a request to the REST API to get the list of user groups,
+     * returning a promise that provides an array of @link{UserGroup} objects if
+     * successful.
+     *
+     * @param {String} dataSource
+     *     The unique identifier of the data source containing the user groups
+     *     to be retrieved. This identifier corresponds to an
+     *     AuthenticationProvider within the Guacamole web application.
+     *
+     * @param {String[]} [permissionTypes]
+     *     The set of permissions to filter with. A user group must have one or
+     *     more of these permissions for a user group to appear in the result.
+     *     If null, no filtering will be performed. Valid values are listed
+     *     within PermissionSet.ObjectType.
+     *
+     * @returns {Promise.<Object.<String, UserGroup>>}
+     *     A promise which will resolve with a map of @link{UserGroup} objects
+     *     where each key is the identifier of the corresponding user group.
+     */
+    service.getUserGroups = function getUserGroups(dataSource, permissionTypes) {
+
+        // Build HTTP parameters set
+        var httpParameters = {
+            token : authenticationService.getCurrentToken()
+        };
+
+        // Add permission filter if specified
+        if (permissionTypes)
+            httpParameters.permission = permissionTypes;
+
+        // Retrieve user groups
+        return requestService({
+            cache   : cacheService.users,
+            method  : 'GET',
+            url     : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups',
+            params  : httpParameters
+        });
+
+    };
+
+    /**
+     * Makes a request to the REST API to get the user group having the given
+     * identifier, returning a promise that provides the corresponding
+     * @link{UserGroup} if successful.
+     *
+     * @param {String} dataSource
+     *     The unique identifier of the data source containing the user group to
+     *     be retrieved. This identifier corresponds to an
+     *     AuthenticationProvider within the Guacamole web application.
+     *
+     * @param {String} identifier
+     *     The identifier of the user group to retrieve.
+     *
+     * @returns {Promise.<UserGroup>}
+     *     A promise which will resolve with a @link{UserGroup} upon success.
+     */
+    service.getUserGroup = function getUserGroup(dataSource, identifier) {
+
+        // Build HTTP parameters set
+        var httpParameters = {
+            token : authenticationService.getCurrentToken()
+        };
+
+        // Retrieve user group
+        return requestService({
+            cache   : cacheService.users,
+            method  : 'GET',
+            url     : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier),
+            params  : httpParameters
+        });
+
+    };
+
+    /**
+     * Makes a request to the REST API to delete a user group, returning a
+     * promise that can be used for processing the results of the call.
+     *
+     * @param {String} dataSource
+     *     The unique identifier of the data source containing the user group to
+     *     be deleted. This identifier corresponds to an AuthenticationProvider
+     *     within the Guacamole web application.
+     *
+     * @param {UserGroup} userGroup
+     *     The user group to delete.
+     *
+     * @returns {Promise}
+     *     A promise for the HTTP call which will succeed if and only if the
+     *     delete operation is successful.
+     */
+    service.deleteUserGroup = function deleteUserGroup(dataSource, userGroup) {
+
+        // Build HTTP parameters set
+        var httpParameters = {
+            token : authenticationService.getCurrentToken()
+        };
+
+        // Delete user group
+        return requestService({
+            method  : 'DELETE',
+            url     : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(userGroup.identifier),
+            params  : httpParameters
+        })
+
+        // Clear the cache
+        .then(function userGroupDeleted(){
+            cacheService.users.removeAll();
+        });
+
+
+    };
+
+    /**
+     * Makes a request to the REST API to create a user group, returning a promise
+     * that can be used for processing the results of the call.
+     *
+     * @param {String} dataSource
+     *     The unique identifier of the data source in which the user group
+     *     should be created. This identifier corresponds to an
+     *     AuthenticationProvider within the Guacamole web application.
+     *
+     * @param {UserGroup} userGroup
+     *     The user group to create.
+     *
+     * @returns {Promise}
+     *     A promise for the HTTP call which will succeed if and only if the
+     *     create operation is successful.
+     */
+    service.createUserGroup = function createUserGroup(dataSource, userGroup) {
+
+        // Build HTTP parameters set
+        var httpParameters = {
+            token : authenticationService.getCurrentToken()
+        };
+
+        // Create user group
+        return requestService({
+            method  : 'POST',
+            url     : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups',
+            params  : httpParameters,
+            data    : userGroup
+        })
+
+        // Clear the cache
+        .then(function userGroupCreated(){
+            cacheService.users.removeAll();
+        });
+
+    };
+
+    /**
+     * Makes a request to the REST API to save a user group, returning a
+     * promise that can be used for processing the results of the call.
+     *
+     * @param {String} dataSource
+     *     The unique identifier of the data source containing the user group to
+     *     be updated. This identifier corresponds to an AuthenticationProvider
+     *     within the Guacamole web application.
+     *
+     * @param {UserGroup} userGroup
+     *     The user group to update.
+     *
+     * @returns {Promise}
+     *     A promise for the HTTP call which will succeed if and only if the
+     *     save operation is successful.
+     */
+    service.saveUserGroup = function saveUserGroup(dataSource, userGroup) {
+
+        // Build HTTP parameters set
+        var httpParameters = {
+            token : authenticationService.getCurrentToken()
+        };
+
+        // Update user group
+        return requestService({
+            method  : 'PUT',
+            url     : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(userGroup.identifier),
+            params  : httpParameters,
+            data    : userGroup
+        })
+
+        // Clear the cache
+        .then(function userGroupUpdated(){
+            cacheService.users.removeAll();
+        });
+
+    };
+
+    return service;
+
+}]);

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/9f01fcb1/guacamole/src/main/webapp/app/rest/types/RelatedObjectPatch.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/rest/types/RelatedObjectPatch.js b/guacamole/src/main/webapp/app/rest/types/RelatedObjectPatch.js
new file mode 100644
index 0000000..bb82def
--- /dev/null
+++ b/guacamole/src/main/webapp/app/rest/types/RelatedObjectPatch.js
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Service which defines the RelatedObjectPatch class.
+ */
+angular.module('rest').factory('RelatedObjectPatch', [function defineRelatedObjectPatch() {
+            
+    /**
+     * The object returned by REST API calls when representing changes to an
+     * arbitrary set of objects which share some common relation.
+     * 
+     * @constructor
+     * @param {RelatedObjectPatch|Object} [template={}]
+     *     The object whose properties should be copied within the new
+     *     RelatedObjectPatch.
+     */
+    var RelatedObjectPatch = function RelatedObjectPatch(template) {
+
+        // Use empty object by default
+        template = template || {};
+
+        /**
+         * The operation to apply to the objects indicated by the path. Valid
+         * operation values are defined within RelatedObjectPatch.Operation.
+         *
+         * @type String
+         */
+        this.op = template.op;
+
+        /**
+         * The path of the objects to modify. This will always be "/".
+         *
+         * @type String
+         * @default '/'
+         */
+        this.path = template.path || '/';
+
+        /**
+         * The identifier of the object being added or removed from the
+         * relation.
+         *
+         * @type String
+         */
+        this.value = template.value;
+
+    };
+
+    /**
+     * All valid patch operations for objects sharing some common relation.
+     * Currently, only add and remove are supported.
+     */
+    RelatedObjectPatch.Operation = {
+
+        /**
+         * Adds the specified object to the relation.
+         */
+        ADD : "add",
+
+        /**
+         * Removes the specified object from the relation.
+         */
+        REMOVE : "remove"
+
+    };
+
+    return RelatedObjectPatch;
+
+}]);
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/9f01fcb1/guacamole/src/main/webapp/app/rest/types/UserGroup.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/rest/types/UserGroup.js b/guacamole/src/main/webapp/app/rest/types/UserGroup.js
new file mode 100644
index 0000000..03b73e2
--- /dev/null
+++ b/guacamole/src/main/webapp/app/rest/types/UserGroup.js
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Service which defines the UserGroup class.
+ */
+angular.module('rest').factory('UserGroup', [function defineUserGroup() {
+
+    /**
+     * The object returned by REST API calls when representing the data
+     * associated with a user group.
+     *
+     * @constructor
+     * @param {UserGroup|Object} [template={}]
+     *     The object whose properties should be copied within the new
+     *     UserGroup.
+     */
+    var UserGroup = function UserGroup(template) {
+
+        // Use empty object by default
+        template = template || {};
+
+        /**
+         * The name which uniquely identifies this user group.
+         *
+         * @type String
+         */
+        this.identifier = template.identifier;
+
+        /**
+         * Arbitrary name/value pairs which further describe this user group.
+         * The semantics and validity of these attributes are dictated by the
+         * extension which defines them.
+         *
+         * @type Object.<String, String>
+         */
+        this.attributes = {};
+
+    };
+
+    return UserGroup;
+
+}]);
\ No newline at end of file


[05/12] guacamole-client git commit: GUACAMOLE-220: Allow manipulation and retrieval of user group permissions via JavaScript.

Posted by vn...@apache.org.
GUACAMOLE-220: Allow manipulation and retrieval of user group permissions via JavaScript.


Project: http://git-wip-us.apache.org/repos/asf/guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/guacamole-client/commit/55bcf25a
Tree: http://git-wip-us.apache.org/repos/asf/guacamole-client/tree/55bcf25a
Diff: http://git-wip-us.apache.org/repos/asf/guacamole-client/diff/55bcf25a

Branch: refs/heads/staging/1.0.0
Commit: 55bcf25a1c53f85ba2383fc83b46e2bc2ccd05af
Parents: 9f01fcb
Author: Michael Jumper <mj...@apache.org>
Authored: Thu Apr 19 14:39:04 2018 -0700
Committer: Michael Jumper <mj...@apache.org>
Committed: Wed Aug 8 09:00:06 2018 -0700

----------------------------------------------------------------------
 .../app/rest/services/permissionService.js      | 91 +++++++++++++-------
 .../webapp/app/rest/types/PermissionFlagSet.js  | 23 ++++-
 .../main/webapp/app/rest/types/PermissionSet.js | 82 +++++++++++++++++-
 3 files changed, 163 insertions(+), 33 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/55bcf25a/guacamole/src/main/webapp/app/rest/services/permissionService.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/rest/services/permissionService.js b/guacamole/src/main/webapp/app/rest/services/permissionService.js
index 6d3dfdf..21c5a02 100644
--- a/guacamole/src/main/webapp/app/rest/services/permissionService.js
+++ b/guacamole/src/main/webapp/app/rest/services/permissionService.js
@@ -45,6 +45,11 @@ angular.module('rest').factory('permissionService', ['$injector',
      * within that data source (and thus cannot be found beneath
      * "api/session/data/{dataSource}/users")
      *
+     * NOTE: Unlike getPermissionsResourceURL(),
+     * getEffectivePermissionsResourceURL() CANNOT be applied to user groups.
+     * Only users have retrievable effective permissions as far as the REST API
+     * is concerned.
+     *
      * @param {String} dataSource
      *     The unique identifier of the data source containing the user whose
      *     permissions should be retrieved. This identifier corresponds to an
@@ -82,6 +87,10 @@ angular.module('rest').factory('permissionService', ['$injector',
      * from the permissions returned via getPermissions() in that permissions
      * which are not directly granted to the user are included.
      *
+     * NOTE: Unlike getPermissions(), getEffectivePermissions() CANNOT be
+     * applied to user groups. Only users have retrievable effective
+     * permissions as far as the REST API is concerned.
+     *
      * @param {String} dataSource
      *     The unique identifier of the data source containing the user whose
      *     permissions should be retrieved. This identifier corresponds to an
@@ -113,10 +122,10 @@ angular.module('rest').factory('permissionService', ['$injector',
 
     /**
      * Returns the URL for the REST resource most appropriate for accessing
-     * the permissions of the user having the given identifier. The permissions
-     * retrieved differ from effective permissions (those returned by
-     * getEffectivePermissions()) in that only permissions which are directly
-     * granted to the user are included.
+     * the permissions of the user or group having the given identifier. The
+     * permissions retrieved differ from effective permissions (those returned
+     * by getEffectivePermissions()) in that only permissions which are directly
+     * granted to the user or group are included.
      * 
      * It is important to note that a particular data source can authenticate
      * and provide permissions for a user, even if that user does not exist
@@ -129,18 +138,27 @@ angular.module('rest').factory('permissionService', ['$injector',
      *     AuthenticationProvider within the Guacamole web application.
      *
      * @param {String} identifier
-     *     The identifier of the user for which the URL of the proper REST
-     *     resource should be derived.
+     *     The identifier of the user or group for which the URL of the proper
+     *     REST resource should be derived.
+     *
+     * @param {Boolean} [group]
+     *     Whether the provided identifier refers to a user group. If false or
+     *     omitted, the identifier given is assumed to refer to a user.
      *
      * @returns {String}
-     *     The URL for the REST resource representing the user having the given
-     *     identifier.
+     *     The URL for the REST resource representing the user or group having
+     *     the given identifier.
      */
-    var getPermissionsResourceURL = function getPermissionsResourceURL(dataSource, identifier) {
+    var getPermissionsResourceURL = function getPermissionsResourceURL(dataSource, identifier, group) {
 
         // Create base URL for data source
         var base = 'api/session/data/' + encodeURIComponent(dataSource);
 
+        // Access group permissions directly (there is no "self" for user groups
+        // as there is for users)
+        if (group)
+            return base + '/userGroups/' + encodeURIComponent(identifier) + '/permissions';
+
         // If the username is that of the current user, do not rely on the
         // user actually existing (they may not). Access their permissions via
         // "self" rather than the collection of defined users.
@@ -155,36 +173,41 @@ angular.module('rest').factory('permissionService', ['$injector',
 
     /**
      * Makes a request to the REST API to get the list of permissions for a
-     * given user, returning a promise that provides an array of
+     * given user or user group, returning a promise that provides an array of
      * @link{Permission} objects if successful. The permissions retrieved
      * differ from effective permissions (those returned by
-     * getEffectivePermissions()) in that only permissions which are directly
-     * granted to the user included.
+     * getEffectivePermissions()) in that both users and groups may be queried,
+     * and only permissions which are directly granted to the user or group are
+     * included.
      * 
      * @param {String} dataSource
-     *     The unique identifier of the data source containing the user whose
-     *     permissions should be retrieved. This identifier corresponds to an
-     *     AuthenticationProvider within the Guacamole web application.
+     *     The unique identifier of the data source containing the user or group
+     *     whose permissions should be retrieved. This identifier corresponds to
+     *     an AuthenticationProvider within the Guacamole web application.
      *
      * @param {String} identifier
-     *     The identifier of the user to retrieve the permissions for.
+     *     The identifier of the user or group to retrieve the permissions for.
+     *
+     * @param {Boolean} [group]
+     *     Whether the provided identifier refers to a user group. If false or
+     *     omitted, the identifier given is assumed to refer to a user.
      *
      * @returns {Promise.<PermissionSet>}
      *     A promise which will resolve with a @link{PermissionSet} upon
      *     success.
      */
-    service.getPermissions = function getPermissions(dataSource, identifier) {
+    service.getPermissions = function getPermissions(dataSource, identifier, group) {
 
         // Build HTTP parameters set
         var httpParameters = {
             token : authenticationService.getCurrentToken()
         };
 
-        // Retrieve user permissions
+        // Retrieve user/group permissions
         return requestService({
             cache   : cacheService.users,
             method  : 'GET',
-            url     : getPermissionsResourceURL(dataSource, identifier),
+            url     : getPermissionsResourceURL(dataSource, identifier, group),
             params  : httpParameters
         });
 
@@ -261,6 +284,10 @@ angular.module('rest').factory('permissionService', ['$injector',
         addObjectPatchOperations(patch, operation, "/userPermissions",
             permissions.userPermissions);
 
+        // Add user group permission operations to patch
+        addObjectPatchOperations(patch, operation, "/userGroupPermissions",
+            permissions.userGroupPermissions);
+
         // Add system operations to patch
         permissions.systemPermissions.forEach(function addSystemPatch(type) {
             patch.push({
@@ -274,18 +301,18 @@ angular.module('rest').factory('permissionService', ['$injector',
             
     /**
      * Makes a request to the REST API to modify the permissions for a given
-     * user, returning a promise that can be used for processing the results of
-     * the call. This request affects only the permissions directly granted to
-     * the user, and may not affect permissions inherited through other means
-     * (effective permissions).
+     * user or group, returning a promise that can be used for processing the
+     * results of the call. This request affects only the permissions directly
+     * granted to the user or group, and may not affect permissions inherited
+     * through other means (effective permissions).
      * 
      * @param {String} dataSource
-     *     The unique identifier of the data source containing the user whose
-     *     permissions should be modified. This identifier corresponds to an
-     *     AuthenticationProvider within the Guacamole web application.
+     *     The unique identifier of the data source containing the user or group
+     *     whose permissions should be modified. This identifier corresponds to
+     *     an AuthenticationProvider within the Guacamole web application.
      *
      * @param {String} identifier
-     *     The identifier of the user to modify the permissions of.
+     *     The identifier of the user or group to modify the permissions of.
      *                          
      * @param {PermissionSet} [permissionsToAdd]
      *     The set of permissions to add, if any.
@@ -293,12 +320,16 @@ angular.module('rest').factory('permissionService', ['$injector',
      * @param {PermissionSet} [permissionsToRemove]
      *     The set of permissions to remove, if any.
      *
+     * @param {Boolean} [group]
+     *     Whether the provided identifier refers to a user group. If false or
+     *     omitted, the identifier given is assumed to refer to a user.
+     *
      * @returns {Promise}
      *     A promise for the HTTP call which will succeed if and only if the
      *     patch operation is successful.
      */
     service.patchPermissions = function patchPermissions(dataSource, identifier,
-            permissionsToAdd, permissionsToRemove) {
+            permissionsToAdd, permissionsToRemove, group) {
 
         var permissionPatch = [];
         
@@ -313,10 +344,10 @@ angular.module('rest').factory('permissionService', ['$injector',
         // Add all the remove operations to the patch
         addPatchOperations(permissionPatch, PermissionPatch.Operation.REMOVE, permissionsToRemove);
 
-        // Patch user permissions
+        // Patch user/group permissions
         return requestService({
             method  : 'PATCH', 
-            url     : getPermissionsResourceURL(dataSource, identifier),
+            url     : getPermissionsResourceURL(dataSource, identifier, group),
             params  : httpParameters,
             data    : permissionPatch
         })

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/55bcf25a/guacamole/src/main/webapp/app/rest/types/PermissionFlagSet.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/rest/types/PermissionFlagSet.js b/guacamole/src/main/webapp/app/rest/types/PermissionFlagSet.js
index 64b942b..f79e3b9 100644
--- a/guacamole/src/main/webapp/app/rest/types/PermissionFlagSet.js
+++ b/guacamole/src/main/webapp/app/rest/types/PermissionFlagSet.js
@@ -133,7 +133,7 @@ angular.module('rest').factory('PermissionFlagSet', ['PermissionSet',
          * true. Valid permission type strings are defined within
          * PermissionSet.ObjectPermissionType. Permissions which are not
          * granted may be set to false, but this is not required.
-         * 
+         *
          * @type Object.<String, Object.<String, Boolean>>
          */
         this.userPermissions = template.userPermissions || {
@@ -143,6 +143,24 @@ angular.module('rest').factory('PermissionFlagSet', ['PermissionSet',
             'ADMINISTER' : {}
         };
 
+        /**
+         * The granted state of each permission for each user group, as a map of
+         * object permission type string to permission map. The permission map
+         * is, in turn, a map of group identifier to boolean value. A particular
+         * permission is granted if its corresponding boolean value is set to
+         * true. Valid permission type strings are defined within
+         * PermissionSet.ObjectPermissionType. Permissions which are not
+         * granted may be set to false, but this is not required.
+         *
+         * @type Object.<String, Object.<String, Boolean>>
+         */
+        this.userGroupPermissions = template.userGroupPermissions || {
+            'READ'       : {},
+            'UPDATE'     : {},
+            'DELETE'     : {},
+            'ADMINISTER' : {}
+        };
+
     };
 
     /**
@@ -216,6 +234,9 @@ angular.module('rest').factory('PermissionFlagSet', ['PermissionSet',
         // Add all granted user permissions
         addObjectPermissions(permissionSet.userPermissions, permissionFlagSet.userPermissions);
 
+        // Add all granted user group permissions
+        addObjectPermissions(permissionSet.userGroupPermissions, permissionFlagSet.userGroupPermissions);
+
         return permissionFlagSet;
 
     };

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/55bcf25a/guacamole/src/main/webapp/app/rest/types/PermissionSet.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/rest/types/PermissionSet.js b/guacamole/src/main/webapp/app/rest/types/PermissionSet.js
index 8fd1ef6..2bc2e9e 100644
--- a/guacamole/src/main/webapp/app/rest/types/PermissionSet.js
+++ b/guacamole/src/main/webapp/app/rest/types/PermissionSet.js
@@ -82,6 +82,15 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet()
         this.userPermissions = template.userPermissions || {};
 
         /**
+         * Map of user group identifiers to the corresponding array of granted
+         * permissions. Each permission is represented by a string listed
+         * within PermissionSet.ObjectPermissionType.
+         *
+         * @type Object.<String, String[]>
+         */
+        this.userGroupPermissions = template.userGroupPermissions || {};
+
+        /**
          * Array of granted system permissions. Each permission is represented
          * by a string listed within PermissionSet.SystemPermissionType.
          *
@@ -306,7 +315,7 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet()
     };
 
     /**
-     * Returns whether the given permission is granted for the user having the 
+     * Returns whether the given permission is granted for the user having the
      * given ID.
      *
      * @param {PermissionSet|Object} permSet
@@ -315,7 +324,7 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet()
      * @param {String} type
      *     The permission to search for, as defined by
      *     PermissionSet.ObjectPermissionType.
-     *     
+     *
      * @param {String} identifier
      *     The identifier of the user to which the permission applies.
      *
@@ -327,6 +336,27 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet()
     };
 
     /**
+     * Returns whether the given permission is granted for the user group having
+     * the given identifier.
+     *
+     * @param {PermissionSet|Object} permSet
+     *     The permission set to check.
+     *
+     * @param {String} type
+     *     The permission to search for, as defined by
+     *     PermissionSet.ObjectPermissionType.
+     *
+     * @param {String} identifier
+     *     The identifier of the user group to which the permission applies.
+     *
+     * @returns {Boolean}
+     *     true if the permission is present (granted), false otherwise.
+     */
+    PermissionSet.hasUserGroupPermission = function hasUserGroupPermission(permSet, type, identifier) {
+        return hasPermission(permSet.userGroupPermissions, type, identifier);
+    };
+
+    /**
      * Returns whether the given permission is granted at the system level.
      *
      * @param {PermissionSet|Object} permSet
@@ -733,6 +763,54 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet()
         return removeObjectPermission(permSet.userPermissions, type, identifier);
     };
 
+    /**
+     * Adds the given user group permission applying to the user group with the
+     * given identifier to the given permission set, if not already present. If
+     * the permission is already present, this function has no effect.
+     *
+     * @param {PermissionSet} permSet
+     *     The permission set to modify.
+     *
+     * @param {String} type
+     *     The permission to add, as defined by
+     *     PermissionSet.ObjectPermissionType.
+     *
+     * @param {String} identifier
+     *     The identifier of the user group to which the permission applies.
+     *
+     * @returns {Boolean}
+     *     true if the permission was added, false if the permission was
+     *     already present in the given permission set.
+     */
+    PermissionSet.addUserGroupPermission = function addUserGroupPermission(permSet, type, identifier) {
+        permSet.userGroupPermissions = permSet.userGroupPermissions || {};
+        return addObjectPermission(permSet.userGroupPermissions, type, identifier);
+    };
+
+    /**
+     * Removes the given user group permission applying to the user group with
+     * the given identifier from the given permission set, if present. If the
+     * permission is not present, this function has no effect.
+     *
+     * @param {PermissionSet} permSet
+     *     The permission set to modify.
+     *
+     * @param {String} type
+     *     The permission to remove, as defined by
+     *     PermissionSet.ObjectPermissionType.
+     *
+     * @param {String} identifier
+     *     The identifier of the user group to whom the permission applies.
+     *
+     * @returns {Boolean}
+     *     true if the permission was removed, false if the permission was not
+     *     present in the given permission set.
+     */
+    PermissionSet.removeUserGroupPermission = function removeUserGroupPermission(permSet, type, identifier) {
+        permSet.userGroupPermissions = permSet.userGroupPermissions || {};
+        return removeObjectPermission(permSet.userGroupPermissions, type, identifier);
+    };
+
     return PermissionSet;
 
 }]);
\ No newline at end of file


[03/12] guacamole-client git commit: GUACAMOLE-220: Implement generic editor directive for manipulating sets of identifiers.

Posted by vn...@apache.org.
GUACAMOLE-220: Implement generic editor directive for manipulating sets of identifiers.


Project: http://git-wip-us.apache.org/repos/asf/guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/guacamole-client/commit/229b0dee
Tree: http://git-wip-us.apache.org/repos/asf/guacamole-client/tree/229b0dee
Diff: http://git-wip-us.apache.org/repos/asf/guacamole-client/diff/229b0dee

Branch: refs/heads/staging/1.0.0
Commit: 229b0dee4882352e7583c4f5872bee92158da712
Parents: 1cf16d1
Author: Michael Jumper <mj...@apache.org>
Authored: Wed Jul 25 02:34:27 2018 -0700
Committer: Michael Jumper <mj...@apache.org>
Committed: Wed Aug 8 09:00:06 2018 -0700

----------------------------------------------------------------------
 .../manage/directives/identifierSetEditor.js    | 267 +++++++++++++++++++
 .../app/manage/styles/related-objects.css       |  82 ++++++
 .../manage/templates/identifierSetEditor.html   |  46 ++++
 .../src/main/webapp/images/arrows/right.png     | Bin 0 -> 264 bytes
 guacamole/src/main/webapp/images/x-red.png      | Bin 0 -> 583 bytes
 5 files changed, 395 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/229b0dee/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js b/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js
new file mode 100644
index 0000000..82f1109
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/directives/identifierSetEditor.js
@@ -0,0 +1,267 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * A directive for manipulating a set of objects sharing some common relation
+ * and represented by an array of their identifiers. The specific objects
+ * added or removed are tracked within a separate pair of arrays of
+ * identifiers.
+ */
+angular.module('manage').directive('identifierSetEditor', ['$injector',
+    function identifierSetEditor($injector) {
+
+    var directive = {
+
+        // Element only
+        restrict: 'E',
+        replace: true,
+
+        scope: {
+
+            /**
+             * The translation key of the text which should be displayed within
+             * the main header of the identifier set editor.
+             *
+             * @type String
+             */
+            header : '@',
+
+            /**
+             * The translation key of the text which should be displayed if no
+             * identifiers are currently present within the set.
+             *
+             * @type String
+             */
+            emptyPlaceholder : '@',
+
+            /**
+             * The translation key of the text which should be displayed if no
+             * identifiers are available to be added within the set.
+             *
+             * @type String
+             */
+            unavailablePlaceholder : '@',
+
+            /**
+             * All identifiers which are available to be added to or removed
+             * from the identifier set being edited.
+             *
+             * @type String[]
+             */
+            identifiersAvailable : '=',
+
+            /**
+             * The current state of the identifier set being manipulated. This
+             * array will be modified as changes are made through this
+             * identifier set editor.
+             *
+             * @type String[]
+             */
+            identifiers : '=',
+
+            /**
+             * The set of identifiers that have been added, relative to the
+             * initial state of the identifier set being manipulated.
+             *
+             * @type String[]
+             */
+            identifiersAdded : '=',
+
+            /**
+             * The set of identifiers that have been removed, relative to the
+             * initial state of the identifier set being manipulated.
+             *
+             * @type String[]
+             */
+            identifiersRemoved : '='
+
+        },
+
+        templateUrl: 'app/manage/templates/identifierSetEditor.html'
+
+    };
+
+    directive.controller = ['$scope', function identifierSetEditorController($scope) {
+
+        /**
+         * Whether the full list of available identifiers should be displayed.
+         * Initially, only an abbreviated list of identifiers currently present
+         * is shown.
+         *
+         * @type Boolean
+         */
+        $scope.expanded = false;
+
+        /**
+         * Map of identifiers to boolean flags indicating whether that
+         * identifier is currently present (true) or absent (false). If an
+         * identifier is absent, it may also be absent from this map.
+         *
+         * @type Object.<String, Boolean>
+         */
+        $scope.identifierFlags = {};
+
+        /**
+         * Adds the given identifier to the given sorted array of identifiers,
+         * preserving the sorted order of the array. If the identifier is
+         * already present, no change is made to the array. The given array
+         * must already be sorted in ascending order.
+         *
+         * @param {String[]} arr
+         *     The sorted array of identifiers to add the given identifier to.
+         *
+         * @param {String} identifier
+         *     The identifier to add to the given array.
+         */
+        var addIdentifier = function addIdentifier(arr, identifier) {
+
+            // Determine location that the identifier should be added to
+            // maintain sorted order
+            var index = _.sortedIndex(arr, identifier);
+
+            // Do not add if already present
+            if (arr[index] === identifier)
+                return;
+
+            // Insert identifier at determined location
+            arr.splice(index, 0, identifier);
+
+        };
+
+        /**
+         * Removes the given identifier from the given sorted array of
+         * identifiers, preserving the sorted order of the array. If the
+         * identifier is already absent, no change is made to the array. The
+         * given array must already be sorted in ascending order.
+         *
+         * @param {String[]} arr
+         *     The sorted array of identifiers to remove the given identifier
+         *     from.
+         *
+         * @param {String} identifier
+         *     The identifier to remove from the given array.
+         *
+         * @returns {Boolean}
+         *     true if the identifier was present in the given array and has
+         *     been removed, false otherwise.
+         */
+        var removeIdentifier = function removeIdentifier(arr, identifier) {
+
+            // Search for identifier in sorted array
+            var index = _.sortedIndexOf(arr, identifier);
+
+            // Nothing to do if already absent
+            if (index === -1)
+                return false;
+
+            // Remove identifier
+            arr.splice(index, 1);
+            return true;
+
+        };
+
+        // Keep identifierFlags up to date when identifiers array is replaced
+        // or initially assigned
+        $scope.$watch('identifiers', function identifiersChanged(identifiers) {
+
+            // Maintain identifiers in sorted order so additions and removals
+            // can be made more efficiently
+            if (identifiers)
+                identifiers.sort();
+
+            // Convert array of identifiers into set of boolean
+            // presence/absence flags
+            $scope.identifierFlags = {};
+            angular.forEach(identifiers, function storeIdentifierFlag(identifier) {
+                $scope.identifierFlags[identifier] = true;
+            });
+
+        });
+
+        /**
+         * Notifies the controller that a change has been made to the flag
+         * denoting presence/absence of a particular identifier within the
+         * <code>identifierFlags</code> map. The <code>identifiers</code>,
+         * <code>identifiersAdded</code>, and <code>identifiersRemoved</code>
+         * arrays are updated accordingly.
+         *
+         * @param {String} identifier
+         *     The identifier which has been added or removed through modifying
+         *     its boolean flag within <code>identifierFlags</code>.
+         */
+        $scope.identifierChanged = function identifierChanged(identifier) {
+
+            // Determine status of modified identifier
+            var present = !!$scope.identifierFlags[identifier];
+
+            // Add/remove identifier from added/removed sets depending on
+            // change in flag state
+            if (present) {
+
+                addIdentifier($scope.identifiers, identifier);
+
+                if (!removeIdentifier($scope.identifiersRemoved, identifier))
+                    addIdentifier($scope.identifiersAdded, identifier);
+
+            }
+            else {
+
+                removeIdentifier($scope.identifiers, identifier);
+
+                if (!removeIdentifier($scope.identifiersAdded, identifier))
+                    addIdentifier($scope.identifiersRemoved, identifier);
+
+            }
+
+        };
+
+        /**
+         * Removes the given identifier, updating <code>identifierFlags</code>,
+         * <code>identifiers</code>, <code>identifiersAdded</code>, and
+         * <code>identifiersRemoved</code> accordingly.
+         *
+         * @param {String} identifier
+         *     The identifier to remove.
+         */
+        $scope.removeIdentifier = function removeIdentifier(identifier) {
+            $scope.identifierFlags[identifier] = false;
+            $scope.identifierChanged(identifier);
+        };
+
+        /**
+         * Shows the full list of available identifiers. If the full list is
+         * already shown, this function has no effect.
+         */
+        $scope.expand = function expand() {
+            $scope.expanded = true;
+        };
+
+        /**
+         * Hides the full list of available identifiers. If the full list is
+         * already hidden, this function has no effect.
+         */
+        $scope.collapse = function collapse() {
+            $scope.expanded = false;
+        };
+
+    }];
+
+    return directive;
+
+}]);

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/229b0dee/guacamole/src/main/webapp/app/manage/styles/related-objects.css
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/manage/styles/related-objects.css b/guacamole/src/main/webapp/app/manage/styles/related-objects.css
new file mode 100644
index 0000000..ddc85b1
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/styles/related-objects.css
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.related-objects .abbreviated-related-objects {
+    display: table;
+    margin: 1em 0;
+}
+
+.related-objects .abbreviated-related-objects ul {
+    display: table-cell;
+    vertical-align: top;
+}
+
+.related-objects .abbreviated-related-objects ul,
+.related-objects .all-related-objects ul {
+    padding: 0;
+    list-style: none;
+}
+
+.related-objects .abbreviated-related-objects ul li {
+
+    display: inline-block;
+    margin: 0.25em;
+    padding: 0.25em;
+
+    border: 1px solid silver;
+    background: #F5F5F5;
+    -moz-border-radius: 0.25em;
+    -webkit-border-radius: 0.25em;
+    -khtml-border-radius: 0.25em;
+    border-radius: 0.25em;
+
+}
+
+.related-objects .abbreviated-related-objects ul li img.remove {
+    max-height: 0.75em;
+    max-width: 0.75em;
+    margin: 0 0.25em;
+}
+
+.related-objects .abbreviated-related-objects ul li .identifier {
+    margin: 0 0.25em;
+}
+
+.related-objects .abbreviated-related-objects img.expand,
+.related-objects .abbreviated-related-objects img.collapse {
+    display: table-cell;
+    max-height: 1.5em;
+    max-width: 1.5em;
+    margin: 0.375em 0;
+}
+
+.related-objects .all-related-objects {
+    border-top: 1px solid silver;
+}
+
+.related-objects .abbreviated-related-objects p.no-related-objects,
+.related-objects .all-related-objects p.no-objects-available {
+    font-style: italic;
+    opacity: 0.5;
+}
+
+.related-objects .abbreviated-related-objects p.no-related-objects {
+    display: table-cell;
+    vertical-align: middle;
+}

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/229b0dee/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html b/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html
new file mode 100644
index 0000000..838decf
--- /dev/null
+++ b/guacamole/src/main/webapp/app/manage/templates/identifierSetEditor.html
@@ -0,0 +1,46 @@
+<div class="related-objects">
+    <div class="header">
+        <h2>{{ header | translate }}</h2>
+        <div class="filter">
+            <input class="search-string" type="text"
+                   placeholder="{{ 'SETTINGS_USERS.FIELD_PLACEHOLDER_FILTER' | translate }}"
+                   ng-model="filterString"/>
+        </div>
+    </div>
+
+    <div class="section">
+
+        <!-- Abbreviated list of only the currently selected objects -->
+        <div class="abbreviated-related-objects">
+            <img src="images/arrows/right.png" alt="Expand" class="expand" ng-hide="expanded" ng-click="expand()"/>
+            <img src="images/arrows/down.png" alt="Collapse" class="collapse" ng-show="expanded" ng-click="collapse()"/>
+            <p ng-hide="identifiers.length" class="no-related-objects">{{ emptyPlaceholder | translate }}</p>
+            <ul>
+                <li ng-repeat="identifier in identifiers | filter: filterString">
+                    <label><img src="images/x-red.png" alt="Remove" class="remove"
+                                ng-click="removeIdentifier(identifier)"/><span class="identifier">{{ identifier }}</span>
+                    </label>
+                </li>
+            </ul>
+        </div>
+
+        <!-- Exhaustive, paginated list of all objects -->
+        <div class="all-related-objects" ng-show="expanded">
+            <p ng-hide="identifiersAvailablePage.length" class="no-objects-available">{{ unavailablePlaceholder | translate }}</p>
+            <ul>
+                <li ng-repeat="identifier in identifiersAvailablePage">
+                    <label><input type="checkbox"
+                           ng-model="identifierFlags[identifier]"
+                           ng-change="identifierChanged(identifier)"/>
+                        <span class="identifier">{{ identifier }}</span>
+                    </label>
+                </li>
+            </ul>
+
+            <!-- Pager controls for user list -->
+            <guac-pager page="identifiersAvailablePage" page-size="25"
+                        items="identifiersAvailable | orderBy | filter: filterString"></guac-pager>
+        </div>
+
+    </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/229b0dee/guacamole/src/main/webapp/images/arrows/right.png
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/images/arrows/right.png b/guacamole/src/main/webapp/images/arrows/right.png
new file mode 100644
index 0000000..1b3483e
Binary files /dev/null and b/guacamole/src/main/webapp/images/arrows/right.png differ

http://git-wip-us.apache.org/repos/asf/guacamole-client/blob/229b0dee/guacamole/src/main/webapp/images/x-red.png
----------------------------------------------------------------------
diff --git a/guacamole/src/main/webapp/images/x-red.png b/guacamole/src/main/webapp/images/x-red.png
new file mode 100644
index 0000000..e5497f3
Binary files /dev/null and b/guacamole/src/main/webapp/images/x-red.png differ


[12/12] guacamole-client git commit: GUACAMOLE-220: Merge add user interface for managing user groups and membership.

Posted by vn...@apache.org.
GUACAMOLE-220: Merge add user interface for managing user groups and membership.


Project: http://git-wip-us.apache.org/repos/asf/guacamole-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/guacamole-client/commit/402ddb57
Tree: http://git-wip-us.apache.org/repos/asf/guacamole-client/tree/402ddb57
Diff: http://git-wip-us.apache.org/repos/asf/guacamole-client/diff/402ddb57

Branch: refs/heads/staging/1.0.0
Commit: 402ddb577f9c7ab204d04640e12d983ecd3f311d
Parents: c36d333 7917f46
Author: Nick Couchman <vn...@apache.org>
Authored: Tue Aug 14 19:33:02 2018 -0400
Committer: Nick Couchman <vn...@apache.org>
Committed: Tue Aug 14 19:33:02 2018 -0400

----------------------------------------------------------------------
 guacamole/pom.xml                               |   4 +-
 guacamole/src/licenses/LICENSE                  |  60 ++-
 .../bundled/jquery-2.1.3/MIT-LICENSE.txt        |  21 -
 .../licenses/bundled/jquery-3.3.1/LICENSE.txt   |  36 ++
 .../licenses/bundled/lodash-2.4.1/LICENSE.txt   |  22 -
 .../src/licenses/bundled/lodash-4.17.10/LICENSE |  47 ++
 .../webapp/app/index/config/indexRouteConfig.js |   9 +
 .../src/main/webapp/app/index/styles/lists.css  |   4 +
 .../src/main/webapp/app/index/styles/ui.css     |   8 +
 .../manage/controllers/manageUserController.js  |  69 ++-
 .../controllers/manageUserGroupController.js    | 538 +++++++++++++++++++
 .../app/manage/directives/dataSourceTabs.js     |   5 +-
 .../manage/directives/identifierSetEditor.js    | 300 +++++++++++
 .../manage/directives/systemPermissionEditor.js |   4 +
 .../app/manage/styles/manage-user-group.css     |  71 +++
 .../app/manage/styles/related-objects.css       |  82 +++
 .../manage/templates/identifierSetEditor.html   |  47 ++
 .../webapp/app/manage/templates/manageUser.html |  11 +
 .../app/manage/templates/manageUserGroup.html   | 101 ++++
 .../app/manage/types/ManageableUserGroup.js     |  53 ++
 .../app/navigation/services/userPageService.js  |  27 +
 .../webapp/app/rest/services/cacheService.js    |   3 +-
 .../app/rest/services/membershipService.js      | 385 +++++++++++++
 .../app/rest/services/permissionService.js      |  91 ++--
 .../webapp/app/rest/services/schemaService.js   |  34 ++
 .../app/rest/services/userGroupService.js       | 223 ++++++++
 .../main/webapp/app/rest/types/Connection.js    |   2 +-
 .../webapp/app/rest/types/ConnectionGroup.js    |   2 +-
 .../webapp/app/rest/types/PermissionFlagSet.js  |  23 +-
 .../main/webapp/app/rest/types/PermissionSet.js |  82 ++-
 .../webapp/app/rest/types/RelatedObjectPatch.js |  85 +++
 .../webapp/app/rest/types/SharingProfile.js     |   2 +-
 .../src/main/webapp/app/rest/types/User.js      |   2 +-
 .../src/main/webapp/app/rest/types/UserGroup.js |  59 ++
 .../settings/controllers/settingsController.js  |   4 +-
 .../directives/guacSettingsUserGroups.js        | 272 ++++++++++
 .../settings/directives/guacSettingsUsers.js    |   4 +-
 .../main/webapp/app/settings/styles/buttons.css |   6 +
 .../app/settings/styles/user-group-list.css     |  36 ++
 .../webapp/app/settings/templates/settings.html |   1 +
 .../settings/templates/settingsUserGroups.html  |  48 ++
 .../images/action-icons/guac-user-group-add.png | Bin 0 -> 1222 bytes
 .../src/main/webapp/images/arrows/right.png     | Bin 0 -> 264 bytes
 .../images/user-icons/guac-user-group.png       | Bin 0 -> 1428 bytes
 guacamole/src/main/webapp/images/x-red.png      | Bin 0 -> 583 bytes
 guacamole/src/main/webapp/index.html            |   4 +-
 guacamole/src/main/webapp/translations/en.json  |  73 ++-
 47 files changed, 2854 insertions(+), 106 deletions(-)
----------------------------------------------------------------------