You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by an...@apache.org on 2018/12/05 11:43:18 UTC

[syncope] branch 2_1_X updated: [SYNCOPE-1381] User requests and user request forms management in enduser UI

This is an automated email from the ASF dual-hosted git repository.

andreapatricelli pushed a commit to branch 2_1_X
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/2_1_X by this push:
     new c666f56  [SYNCOPE-1381] User requests and user request forms management in enduser UI
c666f56 is described below

commit c666f56d9ea2a693f7c074ebe07bfaeb103a8ce2
Author: Andrea Patricelli <an...@apache.org>
AuthorDate: Wed Dec 5 12:42:09 2018 +0100

    [SYNCOPE-1381] User requests and user request forms management in enduser UI
---
 .../client/enduser/resources/BaseResource.java     |   4 +-
 .../app/css/accessibility/accessibilityFont.css    |   5 +-
 .../app/css/accessibility/accessibilityHC.css      |  57 +++++-
 .../resources/META-INF/resources/app/css/app.css   |  10 ++
 .../META-INF/resources/app/css/editUser.css        |  14 +-
 .../resources/META-INF/resources/app/index.html    |   8 +
 .../resources/META-INF/resources/app/js/app.js     |  69 ++++++++
 .../resources/app/js/controllers/UserController.js |  19 +-
 .../resources/app/js/directives/bpmnProcesses.js   |  39 +++++
 .../resources/app/js/directives/formProperty.js    |  83 +++++++++
 .../resources/app/js/directives/modalContent.js    |  31 ++++
 .../resources/app/js/directives/modalWindow.js     |  43 +++++
 .../resources/app/js/directives/requestForms.js    | 110 ++++++++++++
 .../resources/app/js/directives/requests.js        | 192 +++++++++++++++++++++
 .../app/js/services/bpmnProcessService.js          |  38 ++++
 .../app/js/services/userRequestsService.js         |  78 +++++++++
 .../resources/app/languages/de/static.json         |   9 +-
 .../resources/app/languages/en/static.json         |   7 +
 .../resources/app/languages/it/static.json         |   7 +
 .../resources/app/languages/ja/static.json         |   7 +
 .../resources/app/views/bpmnProcesses.html         |  31 ++++
 .../META-INF/resources/app/views/formProperty.html |  92 ++++++++++
 .../META-INF/resources/app/views/modalWindow.html  |  30 ++++
 .../META-INF/resources/app/views/requestForms.html |  61 +++++++
 .../META-INF/resources/app/views/requests.html     |  76 ++++++++
 .../app/views/templates/editUserTemplate.html      |   2 +-
 .../resources/app/views/user-request-forms.html    |  48 ++++++
 .../resources/app/views/user-requests.html         |  48 ++++++
 .../enduser/src/main/resources/customTemplate.json |   6 +
 .../syncope/client/console/pages/Flowable.java     |   4 +-
 .../common/lib/types/FlowableEntitlement.java      |   2 -
 .../core/flowable/impl/FlowableRuntimeUtils.java   |   4 +-
 .../syncope/core/logic/BpmnProcessLogic.java       |   2 +-
 ext/flowable/pom.xml                               |   1 +
 .../syncope-ext-flowable-client-enduser/pom.xml    |  80 +++++++++
 .../client/enduser/resources/BpmnProcessList.java  |  87 ++++++++++
 .../resources/UserRequestCancelResource.java       | 101 +++++++++++
 .../resources/UserRequestFormClaimResource.java    |  91 ++++++++++
 .../resources/UserRequestsFormsResource.java       | 165 ++++++++++++++++++
 .../enduser/resources/UserRequestsResource.java    | 130 ++++++++++++++
 .../resources/UserRequestsStartResource.java       |  86 +++++++++
 fit/enduser-reference/pom.xml                      |   6 +
 .../src/main/resources/customTemplate.json         |   3 +
 .../src/test/resources/customTemplate.json         |   6 +
 44 files changed, 1974 insertions(+), 18 deletions(-)

diff --git a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/BaseResource.java b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/BaseResource.java
index 2540f5d..9e7ebb5 100644
--- a/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/BaseResource.java
+++ b/client/enduser/src/main/java/org/apache/syncope/client/enduser/resources/BaseResource.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.client.enduser.resources;
 
+import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import javax.servlet.http.HttpServletRequest;
 import org.apache.commons.lang3.StringUtils;
@@ -34,7 +35,8 @@ public abstract class BaseResource extends AbstractResource {
 
     protected static final Logger LOG = LoggerFactory.getLogger(BaseResource.class);
 
-    protected static final ObjectMapper MAPPER = new ObjectMapper();
+    protected static final ObjectMapper MAPPER = new ObjectMapper()
+            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
 
     protected final boolean xsrfCheck(final HttpServletRequest request) {
         final String requestXSRFHeader = request.getHeader(SyncopeEnduserConstants.XSRF_HEADER_NAME);
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/css/accessibility/accessibilityFont.css b/client/enduser/src/main/resources/META-INF/resources/app/css/accessibility/accessibilityFont.css
index fa4310c..0f4ef7f 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/css/accessibility/accessibilityFont.css
+++ b/client/enduser/src/main/resources/META-INF/resources/app/css/accessibility/accessibilityFont.css
@@ -34,7 +34,10 @@ body {
 .suggestions,
 .k-notification-wrap,
 .btn-secondary,
- .card-body #attribute .fa {
+div[role="tablist"] .card-header a,
+div[role="tablist"] .card-collapse,
+ul.pagination,
+.card-body #attribute .fa {
   font-size: 20px;
 }
 
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/css/accessibility/accessibilityHC.css b/client/enduser/src/main/resources/META-INF/resources/app/css/accessibility/accessibilityHC.css
index 235daaf..0d9c90d 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/css/accessibility/accessibilityHC.css
+++ b/client/enduser/src/main/resources/META-INF/resources/app/css/accessibility/accessibilityHC.css
@@ -37,7 +37,8 @@ body {
 #next,
 #save,
 #finish,
-#resetpassword {
+#resetpassword,
+.modal-content {
   background: #464646;
   color: #ffffff;
 }
@@ -104,6 +105,59 @@ span.k-dropdown,
   color: #ffffff;
 }
 
+.modal-content {
+  border: solid 1px #ffffff;
+}
+
+.btn-warning {
+  background-color: #70420A;
+}
+
+.btn-warning:hover {
+  background-color: #F3AD57;
+  color: #000000;
+}
+
+.btn-primary {
+  background-color: #205179;
+}
+
+.btn-primary:hover {
+  background-color: #2F78B3;
+  color: #ffffff;
+}
+
+.pagination>.disabled>a, .pagination>.disabled>a:focus, .pagination>.disabled>a:hover, .pagination>.disabled>span, 
+.pagination>.disabled>span:focus, .pagination>.disabled>span:hover {
+    color: #ffffff;
+    background-color: #343434;
+    border-color: #ddd;
+}
+
+.pagination>.active>a, .pagination>.active>a:focus, .pagination>.active>a:hover, .pagination>.active>span, 
+.pagination>.active>span:focus, .pagination>.active>span:hover {
+    color: #ffffff;
+    background-color: #343434;
+    border-color: #ffffff;
+}
+
+.pagination>.active>a, .pagination>.active>span {
+    color: #343434;
+    background-color: #ffffff;
+    border-color: #ffffff;
+}
+
+.pagination>li>a, .pagination>li>span {
+    color: #ffffff;
+    background-color: #343434;
+    border-color: #ffffff;
+}
+
+.pagination>li>a:focus, .pagination>li>a:hover, .pagination>li>span:focus, .pagination>li>span:hover {
+    color: #343434;
+    background-color: #ffffff;
+    border-color: #ffffff;
+}
 
 /* Login
 ============================================================================= */
@@ -141,7 +195,6 @@ span.k-dropdown,
 #login-container .login-btn {
   color: #ffffff;
   background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#6a9647), color-stop(100%,#48543d));
-
   background: #464646;
 }
 
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/css/app.css b/client/enduser/src/main/resources/META-INF/resources/app/css/app.css
index 494fb66..301a19b 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/css/app.css
+++ b/client/enduser/src/main/resources/META-INF/resources/app/css/app.css
@@ -97,4 +97,14 @@ under the License.
   background-color: #fff;
   border-color: #ccc;
 }
+
+.pagination-size {
+  margin: 20px 0;
+  width: 20%;
+}
+
+.pagination-size-sm {
+  margin: 20px 0;
+  width: 12%;
+}
 /* Useless with Bootstrap > 3 --> */
\ No newline at end of file
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/css/editUser.css b/client/enduser/src/main/resources/META-INF/resources/app/css/editUser.css
index 536b41a..47d64d6 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/css/editUser.css
+++ b/client/enduser/src/main/resources/META-INF/resources/app/css/editUser.css
@@ -179,7 +179,7 @@ span.k-datetimepicker{
   width: 170%;
 }
 
-#date{
+#date, #date-property{
   position: relative;
   display: table; 
   border-collapse: separate;
@@ -309,6 +309,14 @@ div[role="tablist"] {
   margin-bottom: 2px;
 }
 
+.schema-type #date-property {
+    margin-bottom: 2px;
+}
+
+.property-label{
+  margin-top: 15px
+}
+
 .multivalue input{
   width: calc(100% - 70px);
   margin-top: 10px
@@ -636,7 +644,7 @@ div[role="tablist"] {
     width:100px;
   }
 
-  #date{
+  #date, #date-property{
     position: relative;
     display: table; 
     border-collapse: separate;
@@ -686,7 +694,7 @@ div[role="tablist"] {
     margin-top: 0px
   }
 
-  #date{
+  #date, #date-property{
     position: relative;
     display: table; 
     border-collapse: separate;
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/index.html b/client/enduser/src/main/resources/META-INF/resources/app/index.html
index 8819fdd..7122b50 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/index.html
+++ b/client/enduser/src/main/resources/META-INF/resources/app/index.html
@@ -124,6 +124,8 @@ under the License.
   <script src="js/services/oidcProviderService.js"></script>
   <script src="js/services/saml2SPService.js"></script>
   <script src="js/services/oidcClientService.js"></script>
+  <script src="js/services/bpmnProcessService.js"></script>
+  <script src="js/services/userRequestsService.js"></script>
   <!--controllers-->
   <script src="js/controllers/HomeController.js"></script>
   <script src="js/controllers/LoginController.js"></script>
@@ -142,6 +144,9 @@ under the License.
   <script src="js/directives/loader.js"></script>
   <script src="js/directives/captcha.js"></script>
   <script src="js/directives/resources.js"></script>
+  <script src="js/directives/requests.js"></script>
+  <script src="js/directives/requestForms.js"></script>
+  <script src="js/directives/formProperty.js"></script>
   <script src="js/directives/groups.js"></script>
   <script src="js/directives/auxClasses.js"></script>
   <script src="js/directives/validate.js"></script>
@@ -149,6 +154,9 @@ under the License.
   <script src="js/directives/validateDropdown.js"></script>
   <script src="js/directives/fileInput.js"></script>
   <script src="js/directives/dynamicTemplateItem.js"></script>
+  <script src="js/directives/modalWindow.js"></script>
+  <script src="js/directives/bpmnProcesses.js"></script>
+  <script src="js/directives/modalContent.js"></script>
   <script src="js/directives/ngEnter.js"></script>
   <!--validator-->
   <script src="js/validator/validationRules.js"></script>
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/app.js b/client/enduser/src/main/resources/META-INF/resources/app/js/app.js
index 9e3128b..18f0d53 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/js/app.js
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/app.js
@@ -226,6 +226,26 @@ app.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', '$translate
                   }]
               }
             })
+            .state('update.userRequests', {
+              url: '/user-requests',
+              templateUrl: 'views/user-requests.html',
+              resolve: {
+                'authenticated': ['AuthService',
+                  function (AuthService) {
+                    return AuthService.islogged();
+                  }]
+              }
+            })
+            .state('update.userRequestForms', {
+              url: '/user-request-forms',
+              templateUrl: 'views/user-request-forms.html',
+              resolve: {
+                'authenticated': ['AuthService',
+                  function (AuthService) {
+                    return AuthService.islogged();
+                  }]
+              }
+            })
             .state('update.finish', {
               url: '/finish',
               templateUrl: 'views/user-form-finish.html',
@@ -430,6 +450,9 @@ app.controller('ApplicationController', ['$scope', '$rootScope', 'InfoService',
                      * Wizard steps from JSON
                      */
                     $scope.wizard = response.wizard.steps;
+                    $scope.creationWizard = $scope.clone($scope.wizard);
+                    delete $scope.creationWizard['userRequests'];
+                    delete $scope.creationWizard['userRequestForms'];
                     $scope.wizardFirstStep = response.wizard.firstStep;
 
                     callback($rootScope.dynTemplate);
@@ -549,6 +572,15 @@ app.controller('ApplicationController', ['$scope', '$rootScope', 'InfoService',
         }
       };
 
+      /* 
+       * Date formatters
+       */
+      
+      // from timestamp
+      $rootScope.formatDate = function (timestamp) {
+        return new Date(timestamp).toLocaleString();
+      };
+
       /*
        |--------------------------------------------------------------------------
        | Notification mgmt
@@ -650,4 +682,41 @@ app.controller('ApplicationController', ['$scope', '$rootScope', 'InfoService',
         $templateCache.removeAll();
       };
     };
+
+    $scope.clone = function clone(obj) {
+      var copy;
+
+      // Handle the 3 simple types, and null or undefined
+      if (null == obj || "object" != typeof obj)
+        return obj;
+
+      // Handle Date
+      if (obj instanceof Date) {
+        copy = new Date();
+        copy.setTime(obj.getTime());
+        return copy;
+      }
+
+      // Handle Array
+      if (obj instanceof Array) {
+        copy = [];
+        for (var i = 0, len = obj.length; i < len; i++) {
+          copy[i] = clone(obj[i]);
+        }
+        return copy;
+      }
+
+      // Handle Object
+      if (obj instanceof Object) {
+        copy = {};
+        for (var attr in obj) {
+          if (obj.hasOwnProperty(attr))
+            copy[attr] = clone(obj[attr]);
+        }
+        return copy;
+      }
+
+      throw new Error("Unable to copy obj! Its type isn't supported.");
+    };
+
   }]);
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/UserController.js b/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/UserController.js
index f919b08..f81747b 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/UserController.js
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/controllers/UserController.js
@@ -23,10 +23,11 @@
 
 angular.module("self").controller("UserController", ['$scope', '$rootScope', '$location', "$state",
   'UserSelfService', 'SchemaService', 'RealmService', 'ResourceService', 'SecurityQuestionService',
-  'GroupService', 'AnyService', 'UserUtil', 'GenericUtil', 'ValidationExecutor', '$translate', '$filter',
+  'GroupService', 'AnyService', 'BpmnProcessService', 'UserRequestsService', 'UserUtil', 'GenericUtil',
+  'ValidationExecutor', '$translate', '$filter',
   function ($scope, $rootScope, $location, $state, UserSelfService, SchemaService, RealmService,
-          ResourceService, SecurityQuestionService, GroupService, AnyService, UserUtil, GenericUtil, ValidationExecutor,
-          $translate, $filter) {
+          ResourceService, SecurityQuestionService, GroupService, AnyService, BpmnProcessService, UserRequestsService,
+          UserUtil, GenericUtil, ValidationExecutor, $translate, $filter) {
 
     $scope.user = {};
     $scope.confirmPassword = {
@@ -37,6 +38,9 @@ angular.module("self").controller("UserController", ['$scope', '$rootScope', '$l
 
     $scope.availableRealms = [];
     $scope.availableSecurityQuestions = [];
+    $scope.bpmnProcesses = [];
+    $scope.userRequests = [];
+    $scope.userRequestsForms = [];
 
     $scope.initialSecurityQuestion = '';
     $scope.captchaInput = {
@@ -334,6 +338,15 @@ angular.module("self").controller("UserController", ['$scope', '$rootScope', '$l
         initUserSchemas();
         // initialize available resources
         initResources();
+        // initialize user requests
+//        if (!$scope.createMode && $scope.wizard.userRequests) {
+//          console.debug("About to init user requests data");
+//          initBpmnProcesses();
+//          // this call will ever get the first 10 User Requests
+//          initUserRequests();
+//          // this call will ever get the first 10 User Requests Forms
+//          initUserRequestsForms();
+//        }
       };
 
       var readUser = function () {
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/directives/bpmnProcesses.js b/client/enduser/src/main/resources/META-INF/resources/app/js/directives/bpmnProcesses.js
new file mode 100644
index 0000000..58c8e71
--- /dev/null
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/directives/bpmnProcesses.js
@@ -0,0 +1,39 @@
+/* 
+ * 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.
+ */
+angular.module('self').directive('bpmnProcesses', [function () {
+    return {
+      restrict: 'E',
+      templateUrl: 'views/bpmnProcesses.html',
+      scope: {
+      },
+      controller: function ($scope) {
+        $scope.bpmnProcesses = $scope.$parent.$parent.resolve.bpmnProcesses;
+        $scope.selectedProcesses = $scope.$parent.$parent.resolve.selectedProcesses;
+        $scope.toggleSelection =
+                function (bpmnProcessKey) {
+                  var index = $scope.selectedProcesses.indexOf(bpmnProcessKey);
+                  if (index > -1) {
+                    $scope.selectedProcesses.splice(index, 1);
+                  } else {
+                    $scope.selectedProcesses.push(bpmnProcessKey);
+                  }
+                };
+      }};
+  }]);
+
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/directives/formProperty.js b/client/enduser/src/main/resources/META-INF/resources/app/js/directives/formProperty.js
new file mode 100644
index 0000000..39ad82a
--- /dev/null
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/directives/formProperty.js
@@ -0,0 +1,83 @@
+/* 
+ * 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.
+ */
+'use strict';
+
+angular.module('self')
+        .directive('formProperty', ['$rootScope', function ($rootScope) {
+            return {
+              restrict: 'E',
+              templateUrl: 'views/formProperty.html',
+              scope: {
+                property: "="
+              },
+              controller: function ($scope) {
+                $scope.initAttribute = function () {
+                  switch ($scope.property.type) {
+                    case "Long":
+                      $scope.property.value = Number($scope.property.value) || undefined;
+                      break;
+                    case "Enum":
+                      if ($scope.property.required !== "true") {
+                        $scope.property.enumValues.empty = "";
+                      }
+                      $scope.enumKeys = Object.keys($scope.property.enumValues);
+                      $scope.property.value = $scope.property.value || $scope.property.enumValues.empty;
+                      break;
+                    case "Dropdown":
+                      if ($scope.property.required !== "true") {
+                        $scope.property.dropdownValues.empty = "";
+                      }
+                      $scope.dropdownKeys = Object.keys($scope.property.dropdownValues);
+                      $scope.property.value = $scope.property.value || $scope.property.dropdownValues.empty;
+                      break;
+                    case "Date":
+                      $scope.getType = function (x) {
+                        return typeof x;
+                      };
+                      $scope.isDate = function (x) {
+                        return x instanceof Date;
+                      };
+                      $scope.languageid = $rootScope.languages.selectedLanguage.id;
+                      $scope.isDateOnly = $scope.property.datePattern.indexOf("H") === -1
+                              && $scope.property.datePattern.indexOf("h") === -1;
+                      $scope.languageFormat = $scope.isDateOnly
+                              ? $rootScope.languages.selectedLanguage.format.replace(" HH:mm", "")
+                              : $rootScope.languages.selectedLanguage.format;
+                      $scope.languageCulture = $rootScope.languages.selectedLanguage.culture;
+                      // read date in milliseconds
+                      $scope.selectedDate = $scope.property.value === null
+                              ? undefined
+                              : new Date($scope.property.value * 1);
+                      $scope.bindDateToModel = function (selectedDate, extendedDate) {
+                        if (selectedDate) {
+                          // save date in milliseconds
+                          $scope.property.value = new Date(extendedDate).getTime();
+                        }
+                      };
+                      break;
+                    case "Boolean":
+                      $scope.property.value = $scope.property.value === "true" ? "true" : "false";
+                      break;
+
+                  }
+                };
+
+              }
+            };
+          }]);
\ No newline at end of file
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/directives/modalContent.js b/client/enduser/src/main/resources/META-INF/resources/app/js/directives/modalContent.js
new file mode 100644
index 0000000..f0aed20
--- /dev/null
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/directives/modalContent.js
@@ -0,0 +1,31 @@
+/* 
+ * 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.
+ */
+angular.module('self').directive('modalContent', ['$compile', function ($compile) {
+    return {
+      restrict: 'E',
+      scope: {
+        modalHtml: "="
+      },
+      replace: true,
+      link: function ($scope, element, attrs) {
+        var template = $compile($scope.modalHtml)($scope);
+        element.replaceWith(template);
+      }};
+  }]);
+
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/directives/modalWindow.js b/client/enduser/src/main/resources/META-INF/resources/app/js/directives/modalWindow.js
new file mode 100644
index 0000000..3ad18e9
--- /dev/null
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/directives/modalWindow.js
@@ -0,0 +1,43 @@
+/* 
+ * 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.
+ */
+angular.module('self').directive('modalWindow', [function () {
+    return {
+      restrict: 'E',
+      templateUrl: 'views/modalWindow.html',
+      scope: {
+        resolve: '<',
+        close: '&',
+        dismiss: '&'
+      },
+      controller: function ($scope) {
+        $scope.init = function () {
+          // Please provide an init logic, by default do nothing
+        };
+
+        $scope.ok = function () {
+          $scope.close({$value: $scope.resolve.result});
+        };
+
+        $scope.cancel = function () {
+          $scope.dismiss({$value: 'cancel'});
+        };
+
+      }};
+  }]);
+
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/directives/requestForms.js b/client/enduser/src/main/resources/META-INF/resources/app/js/directives/requestForms.js
new file mode 100644
index 0000000..4512257
--- /dev/null
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/directives/requestForms.js
@@ -0,0 +1,110 @@
+/* 
+ * 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.
+ */
+'use strict';
+
+angular.module('self')
+        .directive('requestForms', ['UserRequestsService',
+          function (UserRequestsService) {
+            return {
+              restrict: 'E',
+              templateUrl: 'views/requestForms.html',
+              scope: {
+                user: "="
+              },
+              controller: function ($scope) {
+                // Initialization
+                $scope.query = {
+                  user: $scope.user.username,
+                  page: 1,
+                  size: 10
+                };
+
+                var calculatePages = function () {
+                  $scope.totalPages = Math.ceil($scope.forms.totalCount / $scope.query.size);
+                  $scope.pages = _.range(1, $scope.totalPages + 1);
+                };
+
+                // <Pagination>
+                $scope.reloadPage = function (page, size, successMsg) {
+                  // update query pagination parameters
+                  $scope.query.page = page;
+                  $scope.query.size = size;
+                  // recalculate pages
+                  calculatePages();
+                  if (page < 1 || page > $scope.totalPages) {
+                    return;
+                  }
+                  // get current page of items
+
+                  $scope.getUserRequestForms($scope.query, function (forms) {
+                    $scope.forms = forms;
+                    $scope.$parent.showSuccess(successMsg, $scope.$parent.notification);
+                  });
+                };
+                // </Pagination>
+
+                var init = function () {
+                  $scope.getUserRequestForms({
+                    user: $scope.user.username,
+                    page: 1,
+                    size: 10
+                  }, function (requestsForms) {
+                    $scope.forms = requestsForms;
+                    calculatePages();
+                    $scope.availableSizes = [{id: 1, value: 10}, {id: 2, value: 25}, {id: 3, value: 50}];
+                    $scope.selectedSize = $scope.availableSizes[0];
+                  });
+                };
+
+                $scope.getUserRequestForms = function (query, callback) {
+                  UserRequestsService.getUserRequestForms(query).then(function (response) {
+                    callback(response);
+                  }, function (response) {
+                    var errorMessage;
+                    // parse error response 
+                    if (response !== undefined) {
+                      errorMessage = response.split("ErrorMessage{{")[1];
+                      errorMessage = errorMessage.split("}}")[0];
+                    }
+                    console.error("Error retrieving User Request Forms: ", errorMessage);
+                  });
+                };
+
+                init();
+
+                $scope.submit = function (form) {
+                  UserRequestsService.submitForm(form).then(function (response) {
+                    console.debug("Form successfully submitted");
+                    $scope.$parent.showSuccess("Form successfully submitted", $scope.$parent.notification);
+                    $scope.reloadPage($scope.query.page, $scope.query.size, "Form successfully submitted");
+                  }, function (response) {
+                    var errorMessage;
+                    // parse error response 
+                    if (response !== undefined) {
+                      errorMessage = response.split("ErrorMessage{{")[1];
+                      errorMessage = errorMessage.split("}}")[0];
+                    }
+                    console.error("Error retrieving User Request Forms: ", errorMessage);
+                    $scope.$parent.showError("Error: " + (errorMessage || response), $scope.$parent.notification);
+                  });
+
+                };
+              }
+            };
+          }]);
\ No newline at end of file
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/directives/requests.js b/client/enduser/src/main/resources/META-INF/resources/app/js/directives/requests.js
new file mode 100644
index 0000000..64d15a5
--- /dev/null
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/directives/requests.js
@@ -0,0 +1,192 @@
+/* 
+ * 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.
+ */
+'use strict';
+
+angular.module('self')
+        .directive('requests', ['UserRequestsService', 'BpmnProcessService', "$uibModal", "$document", '$filter',
+          '$rootScope',
+          function (UserRequestsService, BpmnProcessService, $uibModal, $document, $filter, $rootScope) {
+            return {
+              restrict: 'E',
+              templateUrl: 'views/requests.html',
+              scope: {
+                user: "="
+              },
+              controller: function ($scope) {
+                // Initialization
+                $scope.query = {
+                  user: $scope.user.username,
+                  page: 1,
+                  size: 10
+                };
+
+                var calculatePages = function () {
+                  $scope.totalPages = Math.ceil($scope.requests.totalCount / $scope.query.size);
+                  $scope.pages = _.range(1, $scope.totalPages + 1);
+                };
+
+                // <Pagination>
+                $scope.reloadPage = function (page, size, successMsg) {
+                  // update query pagination parameters
+                  $scope.query.page = page;
+                  $scope.query.size = size;
+                  // recalculate pages
+                  calculatePages();
+                  if (page < 1 || page > $scope.totalPages) {
+                    return;
+                  }
+                  // get current page of items
+
+                  $scope.getUserRequests($scope.query, function (requests) {
+                    $scope.requests = requests;
+                    if (successMsg) {
+                      $scope.$parent.showSuccess(successMsg, $scope.$parent.notification);
+                    }
+                  });
+                };
+                // </Pagination>
+
+                $scope.getUserRequests = function (query, callback) {
+                  UserRequestsService.getUserRequests(query).then(function (response) {
+                    callback(response);
+                  }, function (response) {
+                    var errorMessage;
+                    // parse error response 
+                    console.log("ERROR ", response);
+                    if (response !== undefined) {
+                      errorMessage = response.split("ErrorMessage{{")[1];
+                      errorMessage = errorMessage.split("}}")[0];
+                    }
+                    console.error("Error retrieving User Requests: ", errorMessage);
+                    $scope.$parent.showError("Error: " + (errorMessage || response), $scope.$parent.notification);
+                  });
+                };
+
+                var init = function () {
+                  $scope.requests = [];
+                  $scope.getUserRequests($scope.query, function (requests) {
+                    $scope.requests = requests;
+                    calculatePages();
+                    $scope.availableSizes = [{id: 1, value: 10}, {id: 2, value: 25}, {id: 3, value: 50}];
+                    $scope.selectedSize = $scope.availableSizes[0];
+                    // date formatting
+                    $scope.formatDate = $rootScope.formatDate;
+                  });
+
+                };
+
+                var initBpmnProcesses = function () {
+                  $scope.bpmnProcesses = [];
+                  BpmnProcessService.getBpmnProcesses().then(function (response) {
+                    $scope.bpmnProcesses = response;
+                  }, function (response) {
+                    // parse error response and log
+                    if (response !== undefined) {
+                      var errorMessages = response.toString().split("ErrorMessage{{");
+                      if (errorMessages.length > 1) {
+                        console.error("Error retrieving BPMN Processes: ", response.toString()
+                                .split("ErrorMessage{{")[1].split("}}")[0]);
+                      } else {
+                        console.error("Error retrieving BPMN Processes: ", errorMessages);
+                      }
+                    }
+                  });
+                };
+
+                init();
+                initBpmnProcesses();
+
+                $scope.cancel = function (request, reason) {
+                  console.log("Cancel request ", request.executionId, reason);
+                  UserRequestsService.cancel(request.executionId, reason).then(function (response) {
+                    var index = $scope.requests.result.indexOf(request);
+                    if (index > -1) {
+                      $scope.requests.result.splice(index, 1);
+                      $scope.requests.totalCount--;
+                      $scope.reloadPage($scope.query.page, $scope.query.size, 
+                      "Process " + request.executionId + " successfully canceled");
+                    }
+                  }, function (response) {
+                    var errorMessage;
+                    // parse error response 
+                    if (response !== undefined) {
+                      errorMessage = response.split("ErrorMessage{{")[1];
+                      errorMessage = errorMessage.split("}}")[0];
+                    }
+                    console.error("Error canceling User Request: ", request.executionId, errorMessage);
+                  });
+                };
+
+                $scope.openComponentModal = function (size, parentSelector) {
+                  $scope.selectedProcesses = [];
+                  var parentElem = parentSelector ?
+                          angular.element($document[0].querySelector(parentSelector)) : undefined;
+                  var modalInstance = $uibModal.open({
+                    animation: true,
+                    ariaLabelledBy: 'modal-title',
+                    ariaDescribedBy: 'modal-body',
+                    component: 'modalWindow',
+                    appendTo: parentElem,
+                    size: size,
+                    windowClass: 'in',
+                    backdropClass: 'in',
+                    resolve: {
+                      bpmnProcesses: function () {
+                        return $scope.bpmnProcesses;
+                      },
+                      selectedProcesses: function () {
+                        return $scope.selectedProcesses;
+                      },
+                      modalHtml: function () {
+                        return '<bpmn-processes></bpmn-processes>';
+                      },
+                      title: function () {
+                        return $filter('translate')(["SELECT_PROCESS"]).SELECT_PROCESS;
+                      }
+                    }
+                  });
+
+                  modalInstance.result.then(function () {
+                    for (var i = 0; i < $scope.selectedProcesses.length; i++) {
+                      startRequest(i);
+                    }
+                  }, function () {
+                  });
+                };
+
+                var startRequest = function (i) {
+                  var currentProc = $scope.selectedProcesses[i];
+                  UserRequestsService.start(currentProc).then(function (response) {
+                    console.log("Process " + currentProc + " successfully started");
+                    $scope.reloadPage($scope.query.page, $scope.query.size,
+                            "Process " + currentProc + " successfully started");
+                  }, function (response) {
+                    var errorMessage;
+                    // parse error response 
+                    if (response !== undefined) {
+                      errorMessage = response.split("ErrorMessage{{")[1];
+                      errorMessage = errorMessage.split("}}")[0];
+                    }
+                    $scope.$parent.showError("Error: " + (errorMessage || response), $scope.$parent.notification);
+                    console.error("Error starting User Request: ", errorMessage);
+                  });
+                };
+              }
+            };
+          }]);
\ No newline at end of file
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/services/bpmnProcessService.js b/client/enduser/src/main/resources/META-INF/resources/app/js/services/bpmnProcessService.js
new file mode 100644
index 0000000..73c8d7a
--- /dev/null
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/services/bpmnProcessService.js
@@ -0,0 +1,38 @@
+/* 
+ * 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.
+ */
+
+'use strict';
+
+angular.module('self')
+        .factory('BpmnProcessService', ['$resource', '$q', '$http',
+          function ($resource, $q, $http) {
+
+            var bpmnService = {};
+
+            bpmnService.getBpmnProcesses = function () {
+              return  $http.get("../api/flowable/bpmnProcesses/")
+                      .then(function (response) {
+                        return response.data;
+                      }, function (response) {
+                        return $q.reject(response.data || response.statusText);
+                      });
+            };
+
+            return bpmnService;
+          }]);
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/js/services/userRequestsService.js b/client/enduser/src/main/resources/META-INF/resources/app/js/services/userRequestsService.js
new file mode 100644
index 0000000..b887784
--- /dev/null
+++ b/client/enduser/src/main/resources/META-INF/resources/app/js/services/userRequestsService.js
@@ -0,0 +1,78 @@
+/* 
+ * 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.
+ */
+
+'use strict';
+
+angular.module('self')
+        .factory('UserRequestsService', ['$q', '$http', function ($q, $http) {
+
+            var userRequestsService = {};
+
+            userRequestsService.getUserRequests = function (query) {
+              return  $http.get("../api/flowable/userRequests?user=" + query.user
+                      + (query.page ? "&page=" + query.page + (query.size ? "&size=" + query.size : "") : "")
+                      + (query.orderBy ? "&orderBy=" + query.orderby : ""))
+                      .then(function (response) {
+                        return response.data;
+                      }, function (response) {
+                        return $q.reject(response.data || response.statusText);
+                      });
+            };
+
+            userRequestsService.getUserRequestForms = function (query) {
+              return  $http.get("../api/flowable/userRequests/forms?user=" + query.user
+                      + (query.page ? "&page=" + query.page + (query.size ? "&size=" + query.size : "") : "")
+                      + (query.orderBy ? "&orderBy=" + query.orderby : ""))
+                      .then(function (response) {
+                        return response.data;
+                      }, function (response) {
+                        return $q.reject(response.data || response.statusText);
+                      });
+            };
+
+            userRequestsService.submitForm = function (form) {
+              return  $http.post("../api/flowable/userRequests/forms", form)
+                      .then(function (response) {
+                        return response.data;
+                      }, function (response) {
+                        return $q.reject(response.data || response.statusText);
+                      });
+            };
+
+            userRequestsService.cancel = function (executionId, reason) {
+              return  $http.delete("../api/flowable/userRequests?executionId=" + executionId
+                      + (reason ? "&reason=" + reason : ""))
+                      .then(function (response) {
+                        return response.data;
+                      }, function (response) {
+                        return $q.reject(response.data || response.statusText);
+                      });
+            };
+
+            userRequestsService.start = function (bpmnProcess) {
+              return  $http.post("../api/flowable/userRequests/start/" + bpmnProcess)
+                      .then(function (response) {
+                        return response.data;
+                      }, function (response) {
+                        return $q.reject(response.data || response.statusText);
+                      });
+            };
+
+            return userRequestsService;
+          }]);
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/languages/de/static.json b/client/enduser/src/main/resources/META-INF/resources/app/languages/de/static.json
index 9b0c4c2..b685f6b 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/languages/de/static.json
+++ b/client/enduser/src/main/resources/META-INF/resources/app/languages/de/static.json
@@ -54,6 +54,13 @@
   "PASSWORD_UPDATED": "Passwort erfolgreich reset",
   "GOBACKHOME": "Hier klicken um zur Homepage zurück zu gelangen",
   "CONFIRM_REMOVE": "Löschen bestätigen",
-  "own": "Besitzen"
+  "SELECT_PROCESS": "Wählen Sie einen oder mehrere Prozesse aus, um die Anfragen zu starten",
+  "PAGE_SIZE": "Seitengröße",
+  "EXECUTION_ID": "Ausführungs-ID",
+  "START_TIME": "Startzeit",
+  "START": "Start",
+  "own": "Besitzen",
+  "userRequests": "Anfragen",
+  "userRequestForms": "Formen"
 }
 
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/languages/en/static.json b/client/enduser/src/main/resources/META-INF/resources/app/languages/en/static.json
index e580e26..31574e4 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/languages/en/static.json
+++ b/client/enduser/src/main/resources/META-INF/resources/app/languages/en/static.json
@@ -45,6 +45,8 @@
   "derivedSchemas": "DerivedSchemas",
   "virtualSchemas": "VirtualSchemas",
   "resources": "Resources",
+  "userRequests": "Requests",
+  "userRequestForms": "Forms",
   "finish": "Finish",
   "RESOURCES_PLACEHOLDER": "Click to select resources...",
   "REALM": "Realm",
@@ -54,6 +56,11 @@
   "PASSWORD_UPDATED": "Password successfully reset",
   "GOBACKHOME": "Click on this link to go back to the home page",
   "CONFIRM_REMOVE": "This will remove the current value. Continue?",
+  "SELECT_PROCESS": "Select one (or many) processes to start request(s)",
+  "PAGE_SIZE": "Page size",
+  "EXECUTION_ID": "Execution id",
+  "START_TIME": "Start time",
+  "START": "Start",
   "own": "Own"
 }
 
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/languages/it/static.json b/client/enduser/src/main/resources/META-INF/resources/app/languages/it/static.json
index 55b16f5..3194dfc 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/languages/it/static.json
+++ b/client/enduser/src/main/resources/META-INF/resources/app/languages/it/static.json
@@ -45,6 +45,8 @@
   "derivedSchemas": "DerivedSchemas",
   "virtualSchemas": "VirtualSchemas",
   "resources": "Risorse",
+  "userRequests": "Richieste",
+  "userRequestForms": "Form",
   "finish": "Fine",
   "RESOURCES_PLACEHOLDER": "Clicca per selezionare risorse...",
   "NEWUSER": "Nuovo utente",
@@ -54,5 +56,10 @@
   "PASSWORD_UPDATED": "Password resettata con successo",
   "GOBACKHOME": "Clicca su questo link per tornare alla home page",
   "CONFIRM_REMOVE": "Questa azione rimuoverà il valore corrente. Continuare?",
+  "SELECT_PROCESS": "Selezionare uno (o più) processi per far partire una richiesta(e)",
+  "PAGE_SIZE": "Elementi per pagina",
+  "EXECUTION_ID": "Id Esecuzione",
+  "START_TIME": "Effettuata il",
+  "START": "Start",
   "own": "Propri"
 }
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/languages/ja/static.json b/client/enduser/src/main/resources/META-INF/resources/app/languages/ja/static.json
index 046b29d..51cd728 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/languages/ja/static.json
+++ b/client/enduser/src/main/resources/META-INF/resources/app/languages/ja/static.json
@@ -54,6 +54,13 @@
   "PASSWORD_UPDATED": "正常にパスワードをリセットしました",
   "GOBACKHOME": "このリンクをクリックするとホームページに戻ります",
   "CONFIRM_REMOVE": "これにより現在の値を削除します。続行しますか?",
+  "SELECT_PROCESS": "プロセスを1つ(または複数)選択して、要求を開始します。",
+  "PAGE_SIZE": "ページサイズ",
+  "EXECUTION_ID": "実行ID",
+  "START_TIME": "始まる時間",
+  "START": "開始",
+  "userRequests": "リクエスト",
+  "userRequestForms": "フォーム",
   "own": "自分"
 }
 
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/views/bpmnProcesses.html b/client/enduser/src/main/resources/META-INF/resources/app/views/bpmnProcesses.html
new file mode 100644
index 0000000..b2e5be8
--- /dev/null
+++ b/client/enduser/src/main/resources/META-INF/resources/app/views/bpmnProcesses.html
@@ -0,0 +1,31 @@
+<!--
+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.
+-->
+<ul>
+  <li style="list-style-type: none;" ng-repeat="bpmnProcess in bpmnProcesses">
+    <label>
+      <input
+        type="checkbox"
+        name="selected[]"
+        value="{{bpmnProcess.key}}"
+        ng-checked="selectedProcesses.indexOf(bpmnProcess.key) > -1"
+        ng-click="toggleSelection(bpmnProcess.key)"
+        > {{bpmnProcess.key}}
+    </label>
+  </li>
+</ul>
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/views/formProperty.html b/client/enduser/src/main/resources/META-INF/resources/app/views/formProperty.html
new file mode 100644
index 0000000..4827a3a
--- /dev/null
+++ b/client/enduser/src/main/resources/META-INF/resources/app/views/formProperty.html
@@ -0,0 +1,92 @@
+<!--
+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.
+-->
+<div ng-switch="property.type" class="schema-type">
+  <input ng-switch-when="String" class="form-control" type="text"
+         ng-model="property.value"
+         ng-required="{{property.required}}" validate="true"
+         ng-disabled="!property.writable"
+         name="{{property.name}}"/>
+
+  <div ng-switch-when="Boolean">
+    <input type="checkbox"
+           ng-true-value="'true'"
+           ng-false-value="'false'" 
+           ng-model="property.value"
+           ng-required="{{property.required}}" validate="true"
+           ng-disabled="!property.writable"
+           ng-init="initAttribute()"
+           name="{{property.name}}"/>
+  </div>
+
+  <input ng-switch-when="Long" class="form-control"
+         type="number"
+         ng-model="property.value"
+         ng-required="{{property.required}}" validate="true"
+         ng-disabled="!property.writable"
+         ng-init="initAttribute()"
+         name="{{property.name}}"/>
+
+  <div ng-switch-when="Date" id="date-property">
+    <input type="text" class="dateTimePicker"
+           kendo-date-time-picker
+           ng-show="!isDateOnly"
+           ng-model="extendedDate"
+           ng-required="{{property.required}}" close-text="Close"
+           ng-init="initAttribute()"
+           ng-change="bindDateToModel(selectedDate, extendedDate)"
+           ng-disabled="!property.writable"
+           k-ng-model="selectedDate"
+           data-k-format="languageFormat"
+           />
+    <input type="text" class="datePicker"
+           kendo-date-picker
+           ng-show="isDateOnly"
+           ng-model="extendedDate"
+           ng-required="{{property.required}}" close-text="Close"
+           ng-init="initAttribute()"
+           ng-change="bindDateToModel(selectedDate, extendedDate)"
+           ng-disabled="!property.writable"
+           k-ng-model="selectedDate"
+           data-k-format="languageFormat"
+           />
+  </div>  
+
+  <div ng-switch-when="Enum" 
+       ng-init="initAttribute()">
+    <select class="form-control custom-select"
+  ng-model="property.value"
+  ng-required="{{property.required}}"
+  ng-disabled="!property.writable">
+      <option ng-repeat="key in enumKeys" value="{{key}}">
+        {{"empty" === key ? "" : property.enumValues[key]}}
+      </option>
+    </select>
+  </div>
+  <div ng-switch-when="Dropdown" 
+       ng-init="initAttribute()">
+    <select class="form-control custom-select"
+            ng-model="property.value"
+            ng-required="{{property.required}}"
+            ng-disabled="!property.writable">
+      <option ng-repeat="key in dropdownKeys" value="{{key}}">
+        {{"empty" === key ? "" : property.dropdownValues[key]}}
+      </option>
+    </select>
+  </div>
+</div>
\ No newline at end of file
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/views/modalWindow.html b/client/enduser/src/main/resources/META-INF/resources/app/views/modalWindow.html
new file mode 100644
index 0000000..9f15200
--- /dev/null
+++ b/client/enduser/src/main/resources/META-INF/resources/app/views/modalWindow.html
@@ -0,0 +1,30 @@
+<!--
+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.
+-->
+<div ng-init="init()">
+  <div class="modal-header">
+    <h3 class="modal-title" id="modal-title">{{resolve.title}}</h3>
+  </div>
+  <div class="modal-body" id="modal-body">
+    <modal-content id="modal-content" modal-html="resolve.modalHtml"></modal-content>
+  </div>
+  <div class="modal-footer">
+    <button class="btn btn-primary" type="button" ng-click="ok()">{{ 'START' | translate}}</button>
+    <button class="btn btn-warning" type="button" ng-click="cancel()">{{'CANCEL'| translate}}</button>
+  </div>
+</div>
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/views/requestForms.html b/client/enduser/src/main/resources/META-INF/resources/app/views/requestForms.html
new file mode 100644
index 0000000..aa44b2c
--- /dev/null
+++ b/client/enduser/src/main/resources/META-INF/resources/app/views/requestForms.html
@@ -0,0 +1,61 @@
+<!--
+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.
+-->
+<div ng-repeat="form in forms.result">
+  <uib-accordion ng-if="forms.result.length">
+    <div  uib-accordion-group heading="{{form.bpmnProcess| translate}}" class="breadcrumb-header panel panel-default">
+      <div id="attribute" class="form-group" ng-repeat="property in form.properties">
+        <label class="property-label" for="property.name">{{property.name}}
+          <span ng-if="property.required">*</span>
+        </label>
+        <form-property property="property"></form-property>
+        <validation-message name="{{property.name}}"/>
+      </div>
+      <div style="text-align: right">
+        <button class="btn btn-secondary btn-default" type="button" ng-click="submit(form)">{{ 'SUBMIT' | translate}}</button>
+      </div>
+    </div>
+  </uib-accordion>
+</div>
+<div class="row">
+  <div class="col-md-9" style="text-align: center">
+    <!-- pager -->
+    <ul ng-if="totalPages > 1" class="pagination">
+      <li ng-class="{disabled:query.page === 1}">
+        <a ng-click="reloadPage(1, query.size)">First</a>
+      </li>
+      <li ng-class="{disabled:query.page === 1}">
+        <a ng-click="reloadPage(query.page - 1, query.size)">Previous</a>
+      </li>
+      <li ng-repeat="page in pages" ng-class="{active:query.page === page}">
+        <a ng-click="reloadPage(page, query.size)">{{page}}</a>
+      </li>                
+      <li ng-class="{disabled:query.page === totalPages}">
+        <a ng-click="reloadPage(query.page + 1, query.size)">Next</a>
+      </li>
+      <li ng-class="{disabled:query.page === totalPages}">
+        <a ng-click="reloadPage(totalPages, query.size)">Last</a>
+      </li>
+    </ul>
+  </div>
+  <div ng-if="totalPages > 1" class="form-group col-md-3 pagination-size-sm" style="text-align: left">
+    <select class="form-control" name="sizeSelect" id="sizeSelect"
+            ng-options="option.value for option in availableSizes track by option.id"
+            ng-model="selectedSize" ng-change="reloadPage(query.page, selectedSize.value)"></select>
+  </div>
+</div>
\ No newline at end of file
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/views/requests.html b/client/enduser/src/main/resources/META-INF/resources/app/views/requests.html
new file mode 100644
index 0000000..68c1da3
--- /dev/null
+++ b/client/enduser/src/main/resources/META-INF/resources/app/views/requests.html
@@ -0,0 +1,76 @@
+<!--
+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.
+-->
+<div ng-repeat="request in requests.result">
+  <uib-accordion ng-if="requests.result.length">
+    <div uib-accordion-group heading="{{request.bpmnProcess| translate}}" class="breadcrumb-header panel panel-default">
+      <table class="table">
+        <thead> 
+          <tr> 
+            <th>{{'EXECUTION_ID'| translate}}</th> 
+            <th>{{'START_TIME'| translate}}</th>
+          </tr> 
+        </thead>
+
+        <tbody> 
+          <tr>
+            <td>{{request.executionId}}</td> 
+            <td>{{formatDate(request.startTime)}}</td> 
+          </tr>
+        </tbody>
+      </table>
+      <div style="text-align: right">
+        <a id="cancelRequest" class="btn btn-secondary btn-default" ng-click="cancel(request)">
+          <i class="fa fa-trash" aria-hidden="true"></i>
+        </a>
+      </div>
+    </div>
+  </uib-accordion>
+</div>
+<div class="row">
+  <div class="col-md-9" style="text-align: center">
+    <!-- pager -->
+    <ul ng-if="totalPages > 1" class="pagination">
+      <li ng-class="{disabled:query.page === 1}">
+        <a ng-click="reloadPage(1, query.size)">First</a>
+      </li>
+      <li ng-class="{disabled:query.page === 1}">
+        <a ng-click="reloadPage(query.page - 1, query.size)">Previous</a>
+      </li>
+      <li ng-repeat="page in pages" ng-class="{active:query.page === page}">
+        <a ng-click="reloadPage(page, query.size)">{{page}}</a>
+      </li>                
+      <li ng-class="{disabled:query.page === totalPages}">
+        <a ng-click="reloadPage(query.page + 1, query.size)">Next</a>
+      </li>
+      <li ng-class="{disabled:query.page === totalPages}">
+        <a ng-click="reloadPage(totalPages, query.size)">Last</a>
+      </li>
+    </ul>
+  </div>
+  <div ng-if="totalPages > 1" class="form-group col-md-3 pagination-size" style="text-align: left">
+    <select class="form-control" name="sizeSelect" id="sizeSelect"
+            ng-options="option.value for option in availableSizes track by option.id"
+            ng-model="selectedSize" ng-change="reloadPage(query.page, selectedSize.value)"></select>
+  </div>
+</div>
+<div class="row" style="text-align: right">
+  <button class="btn btn-default btn-sm" type="button" ng-click="openComponentModal()">
+    <i class="fa fa-plus" title="Start requests"></i>
+  </button>
+</div>
\ No newline at end of file
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/views/templates/editUserTemplate.html b/client/enduser/src/main/resources/META-INF/resources/app/views/templates/editUserTemplate.html
index 396bfe7..5d16434 100644
--- a/client/enduser/src/main/resources/META-INF/resources/app/views/templates/editUserTemplate.html
+++ b/client/enduser/src/main/resources/META-INF/resources/app/views/templates/editUserTemplate.html
@@ -40,7 +40,7 @@ under the License.
                 <i class="fa fa-power-off" style="color:red"></i>
               </a>
               <!-- add class breadcrumb-disabled-link to buttons to prevent click -->              
-              <a ng-repeat="(key, value) in wizard" ui-sref-active="active" ui-sref=".{{key}}" 
+              <a ng-repeat="(key, value) in (createMode ? creationWizard : wizard)" ui-sref-active="active" ui-sref=".{{key}}" 
                  class="btn btn-secondary btn-default breadcrumb-btn-elem" 
                  ng-class="createMode && !endReached ? 'disable-link' : ''">{{key| translate}}</a>       
             </div>
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/views/user-request-forms.html b/client/enduser/src/main/resources/META-INF/resources/app/views/user-request-forms.html
new file mode 100644
index 0000000..4971143
--- /dev/null
+++ b/client/enduser/src/main/resources/META-INF/resources/app/views/user-request-forms.html
@@ -0,0 +1,48 @@
+<!--
+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.
+-->
+<div>
+    <request-forms user="user"></request-forms>
+</div>
+
+<div id="attribute" class="form-group row justify-content-between p-0">
+  <div class="col-xs-3">
+    <a id="cancel" class="btn btn-danger float-left nav-button" tabindex="0" 
+       ng-enter="logout()" ng-click="logout()">
+      {{'CANCEL'| translate}}
+    </a>
+  </div>
+  <div class="col-xs-9">
+    <div id="navButtons" class="float-left"
+         ng-class="(!createMode || (createMode && endReached)) ? 'col-xs-10' : 'col-xs-12'">
+      <navigation-buttons-partial ng-show="createMode" base="create" current="userRequestForms" wizard="{{wizard}}">
+      </navigation-buttons-partial>
+      <navigation-buttons-partial ng-show="!createMode" base="update" current="userRequestForms" wizard="{{wizard}}">
+      </navigation-buttons-partial>
+    </div>
+    <div class="float-right p-0" 
+         ng-class="(!createMode || (createMode && endReached)) ? 'col-xs-2' : ''" 
+         ng-show="!createMode || (createMode && endReached)">
+      <button id="finish" type="button" tabindex="0"
+              class="btn btn-secondary btn-default float-right nav-button" 
+              ng-enter="finish()" ng-click="finish()">
+        {{'FINISH'| translate}}
+      </button>
+    </div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/client/enduser/src/main/resources/META-INF/resources/app/views/user-requests.html b/client/enduser/src/main/resources/META-INF/resources/app/views/user-requests.html
new file mode 100644
index 0000000..62c3e01
--- /dev/null
+++ b/client/enduser/src/main/resources/META-INF/resources/app/views/user-requests.html
@@ -0,0 +1,48 @@
+<!--
+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.
+-->
+<div id="attribute" class="form-group row upper-select ng-scope">
+    <requests user="user"></requests>
+</div>
+
+<div id="attribute" class="form-group row justify-content-between p-0">
+  <div class="col-xs-3">
+    <a id="cancel" class="btn btn-danger float-left nav-button" tabindex="0" 
+       ng-enter="logout()" ng-click="logout()">
+      {{'CANCEL'| translate}}
+    </a>
+  </div>
+  <div class="col-xs-9">
+    <div id="navButtons" class="float-left"
+         ng-class="(!createMode || (createMode && endReached)) ? 'col-xs-10' : 'col-xs-12'">
+      <navigation-buttons-partial ng-show="createMode" base="create" current="userRequests" wizard="{{wizard}}">
+      </navigation-buttons-partial>
+      <navigation-buttons-partial ng-show="!createMode" base="update" current="userRequests" wizard="{{wizard}}">
+      </navigation-buttons-partial>
+    </div>
+    <div class="float-right p-0" 
+         ng-class="(!createMode || (createMode && endReached)) ? 'col-xs-2' : ''" 
+         ng-show="!createMode || (createMode && endReached)">
+      <button id="finish" type="button" tabindex="0"
+              class="btn btn-secondary btn-default float-right nav-button" 
+              ng-enter="finish()" ng-click="finish()">
+        {{'FINISH'| translate}}
+      </button>
+    </div>
+  </div>
+</div>
\ No newline at end of file
diff --git a/client/enduser/src/main/resources/customTemplate.json b/client/enduser/src/main/resources/customTemplate.json
index e55ed4c..12c7f3b 100644
--- a/client/enduser/src/main/resources/customTemplate.json
+++ b/client/enduser/src/main/resources/customTemplate.json
@@ -74,6 +74,12 @@
               "resources": {
                 "url": "/resources"
               },
+              "userRequests": {
+                "url": "/user-requests"
+              },
+              "userRequestForms": {
+                "url": "/user-request-forms"
+              },
               "finish": {
                 "url": "/finish"
               }
diff --git a/ext/flowable/client-console/src/main/java/org/apache/syncope/client/console/pages/Flowable.java b/ext/flowable/client-console/src/main/java/org/apache/syncope/client/console/pages/Flowable.java
index 4784913..1eb40c6 100644
--- a/ext/flowable/client-console/src/main/java/org/apache/syncope/client/console/pages/Flowable.java
+++ b/ext/flowable/client-console/src/main/java/org/apache/syncope/client/console/pages/Flowable.java
@@ -29,7 +29,7 @@ import org.apache.wicket.markup.html.WebMarkupContainer;
 import org.apache.wicket.request.mapper.parameter.PageParameters;
 
 @ExtPage(label = "Flowable", icon = "fa-briefcase",
-        listEntitlement = FlowableEntitlement.BPMN_PROCESS_LIST, priority = 200)
+        listEntitlement = FlowableEntitlement.BPMN_PROCESS_GET, priority = 200)
 public class Flowable extends BaseExtPage {
 
     private static final long serialVersionUID = -8781434495150074529L;
@@ -49,7 +49,7 @@ public class Flowable extends BaseExtPage {
 
         }.disableCheckBoxes().build("bpmnProcessesPanel");
         bpmnProcessesPanel.setOutputMarkupPlaceholderTag(true);
-        MetaDataRoleAuthorizationStrategy.authorize(bpmnProcessesPanel, ENABLE, FlowableEntitlement.BPMN_PROCESS_LIST);
+        MetaDataRoleAuthorizationStrategy.authorize(bpmnProcessesPanel, ENABLE, FlowableEntitlement.BPMN_PROCESS_GET);
 
         content.add(bpmnProcessesPanel);
     }
diff --git a/ext/flowable/common-lib/src/main/java/org/apache/syncope/common/lib/types/FlowableEntitlement.java b/ext/flowable/common-lib/src/main/java/org/apache/syncope/common/lib/types/FlowableEntitlement.java
index 0c41c99..40943de 100644
--- a/ext/flowable/common-lib/src/main/java/org/apache/syncope/common/lib/types/FlowableEntitlement.java
+++ b/ext/flowable/common-lib/src/main/java/org/apache/syncope/common/lib/types/FlowableEntitlement.java
@@ -26,8 +26,6 @@ import java.util.TreeSet;
 
 public final class FlowableEntitlement {
 
-    public static final String BPMN_PROCESS_LIST = "BPMN_PROCESS_LIST";
-
     public static final String BPMN_PROCESS_GET = "BPMN_PROCESS_GET";
 
     public static final String BPMN_PROCESS_SET = "BPMN_PROCESS_SET";
diff --git a/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/impl/FlowableRuntimeUtils.java b/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/impl/FlowableRuntimeUtils.java
index 9ff621c..e7bfa00 100644
--- a/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/impl/FlowableRuntimeUtils.java
+++ b/ext/flowable/flowable-bpmn/src/main/java/org/apache/syncope/core/flowable/impl/FlowableRuntimeUtils.java
@@ -209,7 +209,9 @@ public final class FlowableRuntimeUtils {
     }
 
     public static void throwException(final FlowableException e, final String defaultMessage) {
-        if (e.getCause() instanceof SyncopeClientException) {
+        if (e.getCause() == null) {
+            throw new WorkflowException(defaultMessage, e);
+        } else if (e.getCause() instanceof SyncopeClientException) {
             throw (SyncopeClientException) e.getCause();
         } else if (e.getCause() instanceof ParsingValidationException) {
             throw (ParsingValidationException) e.getCause();
diff --git a/ext/flowable/logic/src/main/java/org/apache/syncope/core/logic/BpmnProcessLogic.java b/ext/flowable/logic/src/main/java/org/apache/syncope/core/logic/BpmnProcessLogic.java
index ad76605..c28516b 100644
--- a/ext/flowable/logic/src/main/java/org/apache/syncope/core/logic/BpmnProcessLogic.java
+++ b/ext/flowable/logic/src/main/java/org/apache/syncope/core/logic/BpmnProcessLogic.java
@@ -36,7 +36,7 @@ public class BpmnProcessLogic extends AbstractTransactionalLogic<BpmnProcess> {
     @Autowired
     private BpmnProcessManager bpmnProcessManager;
 
-    @PreAuthorize("hasRole('" + FlowableEntitlement.BPMN_PROCESS_LIST + "')")
+    @PreAuthorize("isAuthenticated()")
     @Transactional(readOnly = true)
     public List<BpmnProcess> list() {
         return bpmnProcessManager.getProcesses();
diff --git a/ext/flowable/pom.xml b/ext/flowable/pom.xml
index dfe532d..15591fd 100644
--- a/ext/flowable/pom.xml
+++ b/ext/flowable/pom.xml
@@ -44,6 +44,7 @@ under the License.
     <module>rest-cxf</module>
     <module>flowable-bpmn</module>
     <module>client-console</module>
+    <module>syncope-ext-flowable-client-enduser</module>
   </modules>
 
 </project>
diff --git a/ext/flowable/syncope-ext-flowable-client-enduser/pom.xml b/ext/flowable/syncope-ext-flowable-client-enduser/pom.xml
new file mode 100644
index 0000000..09d5844
--- /dev/null
+++ b/ext/flowable/syncope-ext-flowable-client-enduser/pom.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.syncope.ext</groupId>
+    <artifactId>syncope-ext-flowable</artifactId>
+    <version>2.1.3-SNAPSHOT</version>
+  </parent>
+
+  <name>Apache Syncope Ext: Flowable Client Enduser</name>
+  <description>Apache Syncope Ext: Flowable Client Enduser</description>
+  <groupId>org.apache.syncope.ext.flowable</groupId>
+  <artifactId>syncope-ext-flowable-client-enduser</artifactId>
+  <packaging>jar</packaging>
+  
+  <properties>
+    <rootpom.basedir>${basedir}/../../..</rootpom.basedir>
+  </properties>
+  
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.syncope.ext.flowable</groupId>
+      <artifactId>syncope-ext-flowable-common-lib</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.syncope.ext.flowable</groupId>
+      <artifactId>syncope-ext-flowable-rest-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    
+    <dependency>
+      <groupId>org.apache.syncope.client</groupId>
+      <artifactId>syncope-client-enduser</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+ 
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>javax.servlet-api</artifactId>
+    </dependency>
+ 
+  </dependencies>
+  
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-checkstyle-plugin</artifactId>
+      </plugin>
+    </plugins>
+    <resources>
+      <resource>
+        <directory>src/main/resources</directory>
+        <filtering>true</filtering>
+      </resource>
+    </resources>
+  </build>
+  
+</project>
\ No newline at end of file
diff --git a/ext/flowable/syncope-ext-flowable-client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/BpmnProcessList.java b/ext/flowable/syncope-ext-flowable-client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/BpmnProcessList.java
new file mode 100644
index 0000000..4b8d80b
--- /dev/null
+++ b/ext/flowable/syncope-ext-flowable-client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/BpmnProcessList.java
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+package org.apache.syncope.client.enduser.resources;
+
+import static org.apache.syncope.client.enduser.resources.BaseResource.LOG;
+import static org.apache.syncope.client.enduser.resources.BaseResource.MAPPER;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.enduser.annotations.Resource;
+import org.apache.syncope.common.lib.to.BpmnProcess;
+import org.apache.syncope.common.rest.api.service.BpmnProcessService;
+import org.apache.wicket.request.resource.AbstractResource;
+import org.apache.wicket.request.resource.IResource;
+
+@Resource(key = "bpmnProcessesList", path = "/api/flowable/bpmnProcesses/")
+public class BpmnProcessList extends BaseResource {
+
+    private static final long serialVersionUID = 7273151109078469253L;
+
+    @Override
+    protected ResourceResponse newResourceResponse(final IResource.Attributes attributes) {
+        LOG.debug("List available Flowable BPMN processes definitions [{}] useful to start User Requests");
+
+        ResourceResponse response = new AbstractResource.ResourceResponse();
+        response.setContentType(MediaType.APPLICATION_JSON);
+        try {
+            HttpServletRequest request = (HttpServletRequest) attributes.getRequest().getContainerRequest();
+            if (!xsrfCheck(request)) {
+                LOG.error("XSRF TOKEN does not match");
+                response.setError(Response.Status.BAD_REQUEST.getStatusCode(), "XSRF TOKEN does not match");
+                return response;
+            }
+
+            final List<BpmnProcess> bpmnProcesses = SyncopeEnduserSession.get().
+                    getService(BpmnProcessService.class).list();
+
+            response.setWriteCallback(new AbstractResource.WriteCallback() {
+
+                @Override
+                public void writeData(final IResource.Attributes attributes) throws IOException {
+                    // retain also not userWorkflow processes
+                    attributes.getResponse().write(MAPPER.writeValueAsString(bpmnProcesses == null
+                            ? Collections.<BpmnProcess>emptyList()
+                            : bpmnProcesses.stream().filter(bpmnProcess -> !bpmnProcess.isUserWorkflow()).collect(
+                                    Collectors.toList())));
+                }
+            });
+
+            response.setContentType(MediaType.APPLICATION_JSON);
+            response.setTextEncoding(StandardCharsets.UTF_8.name());
+            response.setStatusCode(Response.Status.OK.getStatusCode());
+        } catch (Exception e) {
+            LOG.error("Error retrieving BPMN processes", e);
+            response.setError(Response.Status.BAD_REQUEST.getStatusCode(), new StringBuilder()
+                    .append("ErrorMessage{{ ")
+                    .append(e.getMessage())
+                    .append(" }}")
+                    .toString());
+        }
+
+        return response;
+    }
+}
diff --git a/ext/flowable/syncope-ext-flowable-client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestCancelResource.java b/ext/flowable/syncope-ext-flowable-client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestCancelResource.java
new file mode 100644
index 0000000..4012b69
--- /dev/null
+++ b/ext/flowable/syncope-ext-flowable-client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestCancelResource.java
@@ -0,0 +1,101 @@
+/*
+ * 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.
+ */
+package org.apache.syncope.client.enduser.resources;
+
+import static org.apache.syncope.client.enduser.resources.BaseResource.LOG;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.enduser.annotations.Resource;
+import org.apache.syncope.common.rest.api.service.UserRequestService;
+import org.apache.wicket.request.IRequestParameters;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.request.resource.AbstractResource;
+import org.apache.wicket.request.resource.IResource;
+import org.apache.wicket.util.string.StringValue;
+
+@Resource(key = "userRequestCancel", path = "/api/flowable/userRequests/${executionId}")
+public class UserRequestCancelResource extends BaseResource {
+
+    private static final long serialVersionUID = 7273151109078469253L;
+
+    @Override
+    protected ResourceResponse newResourceResponse(final IResource.Attributes attributes) {
+
+        ResourceResponse response = new AbstractResource.ResourceResponse();
+        response.setContentType(MediaType.APPLICATION_JSON);
+        StringValue executionId;
+        try {
+            HttpServletRequest request = (HttpServletRequest) attributes.getRequest().getContainerRequest();
+            if (!xsrfCheck(request)) {
+                LOG.error("XSRF TOKEN does not match");
+                response.setError(Response.Status.BAD_REQUEST.getStatusCode(), "XSRF TOKEN does not match");
+                return response;
+            }
+
+            if (!HttpMethod.DELETE.equals(request.getMethod())) {
+                throw new UnsupportedOperationException("Unsupported operation, only DELETE allowed");
+            }
+
+            PageParameters parameters = attributes.getParameters();
+            executionId = parameters.get("executionId");
+            IRequestParameters requestParameters = attributes.getRequest().getQueryParameters();
+            StringValue reason = requestParameters.getParameterValue("reason");
+            LOG.debug("Cancel Flowable User Request with execution id [{}] for user [{}] with reason [{}]", executionId,
+                    SyncopeEnduserSession.get().getSelfTO().getUsername(), reason);
+            if (executionId.isEmpty()) {
+                throw new IllegalArgumentException("Empty executionId, please provide a value");
+            }
+
+            SyncopeEnduserSession.get().getService(UserRequestService.class).cancel(executionId.toString(),
+                    reason.toString());
+
+            final String outcomeMessage = String.format(
+                    "User Request with execution id [%s] successfully canceled for User [%s]", executionId.
+                            toString(), SyncopeEnduserSession.get().getSelfTO().getUsername());
+
+            response.setWriteCallback(new AbstractResource.WriteCallback() {
+
+                @Override
+                public void writeData(final IResource.Attributes attributes) throws IOException {
+                    attributes.getResponse().write(outcomeMessage);
+                }
+            });
+
+            response.setContentType(MediaType.APPLICATION_JSON);
+            response.setTextEncoding(StandardCharsets.UTF_8.name());
+            response.setStatusCode(Response.Status.OK.getStatusCode());
+        } catch (Exception e) {
+            LOG.error("Error cancelling User Request for [{}]", SyncopeEnduserSession.get().getSelfTO().getUsername(),
+                    e);
+            response.setError(Response.Status.BAD_REQUEST.getStatusCode(), new StringBuilder()
+                    .append("ErrorMessage{{ ")
+                    .append(e.getMessage())
+                    .append(" }}")
+                    .toString());
+        }
+
+        return response;
+    }
+}
diff --git a/ext/flowable/syncope-ext-flowable-client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestFormClaimResource.java b/ext/flowable/syncope-ext-flowable-client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestFormClaimResource.java
new file mode 100644
index 0000000..a342c7f
--- /dev/null
+++ b/ext/flowable/syncope-ext-flowable-client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestFormClaimResource.java
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ */
+package org.apache.syncope.client.enduser.resources;
+
+import static org.apache.syncope.client.enduser.resources.BaseResource.LOG;
+import static org.apache.syncope.client.enduser.resources.BaseResource.MAPPER;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.enduser.annotations.Resource;
+import org.apache.syncope.common.lib.to.UserRequestForm;
+import org.apache.syncope.common.rest.api.service.UserRequestService;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.request.resource.AbstractResource;
+import org.apache.wicket.request.resource.IResource;
+import org.apache.wicket.util.string.StringValue;
+
+@Resource(key = "userRequestCancelByUsername", path = "/api/flowable/userRequests/forms/${taskId}/claim")
+public class UserRequestFormClaimResource extends BaseResource {
+
+    private static final long serialVersionUID = 7273151109078469253L;
+
+    @Override
+    protected ResourceResponse newResourceResponse(final IResource.Attributes attributes) {
+
+        ResourceResponse response = new AbstractResource.ResourceResponse();
+        response.setContentType(MediaType.APPLICATION_JSON);
+        StringValue taskId;
+        try {
+            HttpServletRequest request = (HttpServletRequest) attributes.getRequest().getContainerRequest();
+            if (!xsrfCheck(request)) {
+                LOG.error("XSRF TOKEN does not match");
+                response.setError(Response.Status.BAD_REQUEST.getStatusCode(), "XSRF TOKEN does not match");
+                return response;
+            }
+
+            PageParameters parameters = attributes.getParameters();
+            taskId = parameters.get("taskId");
+            LOG.debug("Claim Flowable User Request Form with task id [{}] for user [{}] with reason [{}]", taskId,
+                    SyncopeEnduserSession.get().getSelfTO().getUsername());
+            if (taskId.isEmpty()) {
+                throw new IllegalArgumentException("Empty taskId, please provide a value");
+            }
+            UserRequestForm requestForm = SyncopeEnduserSession.get().getService(UserRequestService.class).claimForm(
+                    taskId.toString());
+
+            response.setWriteCallback(new AbstractResource.WriteCallback() {
+
+                @Override
+                public void writeData(final IResource.Attributes attributes) throws IOException {
+                    attributes.getResponse().write(MAPPER.writeValueAsString(requestForm));
+                }
+            });
+
+            response.setContentType(MediaType.APPLICATION_JSON);
+            response.setTextEncoding(StandardCharsets.UTF_8.name());
+            response.setStatusCode(Response.Status.OK.getStatusCode());
+        } catch (Exception e) {
+            LOG.
+                    error("Error claiming User Request Form for [{}]", SyncopeEnduserSession.get().getSelfTO().
+                            getUsername(), e);
+            response.setError(Response.Status.BAD_REQUEST.getStatusCode(), new StringBuilder()
+                    .append("ErrorMessage{{ ")
+                    .append(e.getMessage())
+                    .append(" }}")
+                    .toString());
+        }
+
+        return response;
+    }
+}
diff --git a/ext/flowable/syncope-ext-flowable-client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestsFormsResource.java b/ext/flowable/syncope-ext-flowable-client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestsFormsResource.java
new file mode 100644
index 0000000..f303070
--- /dev/null
+++ b/ext/flowable/syncope-ext-flowable-client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestsFormsResource.java
@@ -0,0 +1,165 @@
+/*
+ * 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.
+ */
+package org.apache.syncope.client.enduser.resources;
+
+import static org.apache.syncope.client.enduser.resources.BaseResource.LOG;
+import static org.apache.syncope.client.enduser.resources.BaseResource.MAPPER;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.text.ParseException;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.time.FastDateFormat;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.enduser.annotations.Resource;
+import org.apache.syncope.common.lib.to.PagedResult;
+import org.apache.syncope.common.lib.to.UserRequestForm;
+import org.apache.syncope.common.lib.types.UserRequestFormPropertyType;
+import org.apache.syncope.common.rest.api.beans.UserRequestFormQuery;
+import org.apache.syncope.common.rest.api.service.UserRequestService;
+import org.apache.wicket.request.IRequestParameters;
+import org.apache.wicket.request.resource.AbstractResource;
+import org.apache.wicket.request.resource.IResource;
+import org.apache.wicket.util.string.StringValue;
+
+@Resource(key = "userRequestsForms", path = "/api/flowable/userRequests/forms")
+public class UserRequestsFormsResource extends BaseResource {
+
+    private static final long serialVersionUID = 7273151109078469253L;
+
+    @Override
+    protected ResourceResponse newResourceResponse(final IResource.Attributes attributes) {
+
+        ResourceResponse response = new AbstractResource.ResourceResponse();
+        response.setContentType(MediaType.APPLICATION_JSON);
+        response.setTextEncoding(StandardCharsets.UTF_8.name());
+        StringValue username = StringValue.valueOf(SyncopeEnduserSession.get().getSelfTO().getUsername());
+        try {
+            HttpServletRequest request = (HttpServletRequest) attributes.getRequest().getContainerRequest();
+            if (!xsrfCheck(request)) {
+                LOG.error("XSRF TOKEN does not match");
+                response.setError(Response.Status.BAD_REQUEST.getStatusCode(), "XSRF TOKEN does not match");
+                return response;
+            }
+
+            switch (request.getMethod()) {
+                case HttpMethod.GET:
+                    IRequestParameters requestParameters = attributes.getRequest().getQueryParameters();
+                    StringValue page = requestParameters.getParameterValue("page");
+                    StringValue size = requestParameters.getParameterValue("size");
+                    LOG.debug("List available Flowable User Requests Forms by user [{}]", username);
+                    final PagedResult<UserRequestForm> userRequestForms = SyncopeEnduserSession.get().
+                            getService(UserRequestService.class).getForms(
+                            new UserRequestFormQuery.Builder()
+                                    .user(username.isEmpty()
+                                            ? SyncopeEnduserSession.get().getSelfTO().getUsername()
+                                            : username.toString())
+                                    .page(page.isEmpty()
+                                            ? 1
+                                            : Integer.parseInt(page.toString()))
+                                    .size(size.isEmpty()
+                                            ? 10
+                                            : Integer.parseInt(size.toString())).build());
+
+                    // Date -> millis conversion for Date properties of the form
+                    userRequestForms.getResult().stream().forEach(form
+                            -> form.getProperties().stream()
+                                    .filter(prop -> UserRequestFormPropertyType.Date == prop.getType()
+                                    && StringUtils.isNotBlank(prop.getValue()))
+                                    .forEach(prop -> {
+                                        try {
+                                            prop.setValue(String.valueOf(FastDateFormat.getInstance(prop.
+                                                    getDatePattern()).parse(prop.getValue()).getTime()));
+                                        } catch (ParseException e) {
+                                            LOG.error("Unable to parse date", e);
+                                        }
+                                    }));
+
+                    response.setWriteCallback(new AbstractResource.WriteCallback() {
+
+                        @Override
+                        public void writeData(final IResource.Attributes attributes) throws IOException {
+                            attributes.getResponse().write(MAPPER.writeValueAsString(userRequestForms));
+                        }
+                    });
+                    break;
+                case HttpMethod.POST:
+                    UserRequestForm requestForm = MAPPER.
+                            readValue(request.getReader().readLine(), UserRequestForm.class);
+                    if (requestForm == null) {
+                        throw new IllegalArgumentException("Empty userRequestForm, please provide a valid one");
+                    }
+
+                    UserRequestService userRequestService = SyncopeEnduserSession.get().getService(
+                            UserRequestService.class);
+                    // 1. claim form as logged user
+                    userRequestService.claimForm(requestForm.getTaskId());
+                    // millis -> Date conversion for Date properties of the form
+                    requestForm.getProperties().stream()
+                            .filter(prop -> UserRequestFormPropertyType.Date == prop.getType()
+                            && StringUtils.isNotBlank(prop.getValue()))
+                            .forEach(prop -> {
+                                try {
+                                    prop.setValue(FastDateFormat.getInstance(prop.getDatePattern()).format(Long.valueOf(
+                                            prop.getValue())));
+                                } catch (NumberFormatException e) {
+                                    LOG.error("Unable to format date", e);
+                                }
+                            });
+                    // 2. Submit form
+                    LOG.debug("Submit Flowable User Request Form for user [{}]", requestForm.getUsername());
+                    userRequestService.submitForm(requestForm);
+
+                    response.setStatusCode(Response.Status.NO_CONTENT.getStatusCode());
+                    response.setWriteCallback(new AbstractResource.WriteCallback() {
+
+                        @Override
+                        public void writeData(final IResource.Attributes attributes) throws IOException {
+                            // DO NOTHING
+                        }
+                    });
+                    break;
+                default:
+                    LOG.error("Method [{}] not supported", request.getMethod());
+                    response.setError(Response.Status.BAD_REQUEST.getStatusCode(), new StringBuilder()
+                            .append("ErrorMessage{{ ")
+                            .append("Method not supported")
+                            .append(" }}")
+                            .toString());
+                    break;
+            }
+            response.setContentType(MediaType.APPLICATION_JSON);
+            response.setTextEncoding(StandardCharsets.UTF_8.name());
+            response.setStatusCode(Response.Status.OK.getStatusCode());
+        } catch (Exception e) {
+            LOG.error("Error dealing with forms of user [{}]", username, e);
+            response.setError(Response.Status.BAD_REQUEST.getStatusCode(), new StringBuilder()
+                    .append("ErrorMessage{{ ")
+                    .append(e.getMessage())
+                    .append(" }}")
+                    .toString());
+        }
+
+        return response;
+    }
+}
diff --git a/ext/flowable/syncope-ext-flowable-client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestsResource.java b/ext/flowable/syncope-ext-flowable-client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestsResource.java
new file mode 100644
index 0000000..812e408
--- /dev/null
+++ b/ext/flowable/syncope-ext-flowable-client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestsResource.java
@@ -0,0 +1,130 @@
+/*
+ * 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.
+ */
+package org.apache.syncope.client.enduser.resources;
+
+import static org.apache.syncope.client.enduser.resources.BaseResource.LOG;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.enduser.annotations.Resource;
+import org.apache.syncope.common.lib.to.PagedResult;
+import org.apache.syncope.common.lib.to.UserRequest;
+import org.apache.syncope.common.rest.api.beans.UserRequestQuery;
+import org.apache.syncope.common.rest.api.service.UserRequestService;
+import org.apache.wicket.request.IRequestParameters;
+import org.apache.wicket.request.resource.AbstractResource;
+import org.apache.wicket.request.resource.IResource;
+import org.apache.wicket.util.string.StringValue;
+
+@Resource(key = "userRequests", path = "/api/flowable/userRequests")
+public class UserRequestsResource extends BaseResource {
+
+    private static final long serialVersionUID = 7273151109078469253L;
+
+    @Override
+    protected ResourceResponse newResourceResponse(final IResource.Attributes attributes) {
+
+        ResourceResponse response = new AbstractResource.ResourceResponse();
+        response.setContentType(MediaType.APPLICATION_JSON);
+        StringValue username = StringValue.valueOf(SyncopeEnduserSession.get().getSelfTO().getUsername());
+        try {
+            HttpServletRequest request = (HttpServletRequest) attributes.getRequest().getContainerRequest();
+            if (!xsrfCheck(request)) {
+                LOG.error("XSRF TOKEN does not match");
+                response.setError(Response.Status.BAD_REQUEST.getStatusCode(), "XSRF TOKEN does not match");
+                return response;
+            }
+
+            IRequestParameters requestParameters = attributes.getRequest().getQueryParameters();
+            switch (request.getMethod()) {
+                case HttpMethod.DELETE:
+                    StringValue executionId = requestParameters.getParameterValue("executionId");
+                    StringValue reason = requestParameters.getParameterValue("reason");
+                    LOG.debug("Cancel Flowable User Request with execution id [{}] for user [{}] with reason [{}]",
+                            executionId, SyncopeEnduserSession.get().getSelfTO().getUsername(), reason);
+                    if (executionId.isEmpty()) {
+                        throw new IllegalArgumentException("Empty executionId, please provide a value");
+                    }
+                    SyncopeEnduserSession.get().getService(UserRequestService.class).cancel(executionId.toString(),
+                            reason.toString());
+                    response.setStatusCode(Response.Status.NO_CONTENT.getStatusCode());
+                    response.setWriteCallback(new AbstractResource.WriteCallback() {
+
+                        @Override
+                        public void writeData(final IResource.Attributes attributes) throws IOException {
+                            // DO NOTHING
+                        }
+                    });
+                    break;
+
+                case HttpMethod.GET:
+                    StringValue page = requestParameters.getParameterValue("page");
+                    StringValue size = requestParameters.getParameterValue("size");
+                    LOG.debug("List available Flowable User Requests for user [{}]", username);
+                    final PagedResult<UserRequest> userRequests = SyncopeEnduserSession.get().
+                            getService(UserRequestService.class).list(
+                            new UserRequestQuery.Builder()
+                                    .user(username.isEmpty()
+                                            ? SyncopeEnduserSession.get().getSelfTO().getUsername()
+                                            : username.toString())
+                                    .page(page.isEmpty()
+                                            ? 1
+                                            : Integer.parseInt(
+                                                    page.toString()))
+                                    .size(size.isEmpty()
+                                            ? 10
+                                            : Integer.parseInt(
+                                                    size.toString())).build());
+                    response.setWriteCallback(new AbstractResource.WriteCallback() {
+
+                        @Override
+                        public void writeData(final IResource.Attributes attributes) throws IOException {
+                            attributes.getResponse().write(MAPPER.writeValueAsString(userRequests));
+                        }
+                    });
+                    break;
+                default:
+                    LOG.error("Method [{}] not supported", request.getMethod());
+                    response.setError(Response.Status.BAD_REQUEST.getStatusCode(), new StringBuilder()
+                            .append("ErrorMessage{{ ")
+                            .append("Method not supported")
+                            .append(" }}")
+                            .toString());
+                    break;
+            }
+            response.setContentType(MediaType.APPLICATION_JSON);
+            response.setTextEncoding(StandardCharsets.UTF_8.name());
+            response.setStatusCode(Response.Status.OK.getStatusCode());
+        } catch (Exception e) {
+            LOG.error("Error retrieving user requests for [{}]", username, e);
+            response.setError(Response.Status.BAD_REQUEST.getStatusCode(), new StringBuilder()
+                    .append("ErrorMessage{{ ")
+                    .append(e.getMessage())
+                    .append(" }}")
+                    .toString());
+        }
+
+        return response;
+    }
+}
diff --git a/ext/flowable/syncope-ext-flowable-client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestsStartResource.java b/ext/flowable/syncope-ext-flowable-client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestsStartResource.java
new file mode 100644
index 0000000..4c65768
--- /dev/null
+++ b/ext/flowable/syncope-ext-flowable-client-enduser/src/main/java/org/apache/syncope/client/enduser/resources/UserRequestsStartResource.java
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+package org.apache.syncope.client.enduser.resources;
+
+import static org.apache.syncope.client.enduser.resources.BaseResource.LOG;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.syncope.client.enduser.SyncopeEnduserSession;
+import org.apache.syncope.client.enduser.annotations.Resource;
+import org.apache.syncope.common.rest.api.service.UserRequestService;
+import org.apache.wicket.request.mapper.parameter.PageParameters;
+import org.apache.wicket.request.resource.AbstractResource;
+import org.apache.wicket.request.resource.IResource;
+import org.apache.wicket.util.string.StringValue;
+
+@Resource(key = "userRequests", path = "/api/flowable/userRequests/start/${bpmnProcess}")
+public class UserRequestsStartResource extends BaseResource {
+
+    private static final long serialVersionUID = 7273151109078469253L;
+
+    @Override
+    protected ResourceResponse newResourceResponse(final IResource.Attributes attributes) {
+        ResourceResponse response = new AbstractResource.ResourceResponse();
+        response.setContentType(MediaType.APPLICATION_JSON);
+        StringValue bpmnProcess = null;
+        try {
+            HttpServletRequest request = (HttpServletRequest) attributes.getRequest().getContainerRequest();
+            if (!xsrfCheck(request)) {
+                LOG.error("XSRF TOKEN does not match");
+                response.setError(Response.Status.BAD_REQUEST.getStatusCode(), "XSRF TOKEN does not match");
+                return response;
+            }
+
+            PageParameters parameters = attributes.getParameters();
+            bpmnProcess = parameters.get("bpmnProcess");
+
+            LOG.debug("Start Flowable User Request from process [{}] for user [{}]",
+                    bpmnProcess, SyncopeEnduserSession.get().getSelfTO().getUsername());
+            if (bpmnProcess.isEmpty()) {
+                throw new IllegalArgumentException("Empty bpmnProcess, please provide a value");
+            }
+            SyncopeEnduserSession.get().getService(UserRequestService.class).start(bpmnProcess.toString(), null);
+            response.setStatusCode(Response.Status.NO_CONTENT.getStatusCode());
+            response.setWriteCallback(new AbstractResource.WriteCallback() {
+
+                @Override
+                public void writeData(final IResource.Attributes attributes) throws IOException {
+                    // DO NOTHING
+                }
+            });
+
+            response.setContentType(MediaType.APPLICATION_JSON);
+            response.setTextEncoding(StandardCharsets.UTF_8.name());
+            response.setStatusCode(Response.Status.OK.getStatusCode());
+        } catch (Exception e) {
+            LOG.error("Error starting user request from [{}]", bpmnProcess, e);
+            response.setError(Response.Status.BAD_REQUEST.getStatusCode(), new StringBuilder()
+                    .append("ErrorMessage{{ ")
+                    .append(e.getMessage())
+                    .append(" }}")
+                    .toString());
+        }
+
+        return response;
+    }
+}
diff --git a/fit/enduser-reference/pom.xml b/fit/enduser-reference/pom.xml
index 47de321..de369e5 100644
--- a/fit/enduser-reference/pom.xml
+++ b/fit/enduser-reference/pom.xml
@@ -73,6 +73,12 @@ under the License.
     </dependency>
 
     <dependency>
+      <groupId>org.apache.syncope.ext.flowable</groupId>
+      <artifactId>syncope-ext-flowable-client-enduser</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
     </dependency>
diff --git a/fit/enduser-reference/src/main/resources/customTemplate.json b/fit/enduser-reference/src/main/resources/customTemplate.json
index e55ed4c..7223610 100644
--- a/fit/enduser-reference/src/main/resources/customTemplate.json
+++ b/fit/enduser-reference/src/main/resources/customTemplate.json
@@ -74,6 +74,9 @@
               "resources": {
                 "url": "/resources"
               },
+              "userRequests": {
+                "url": "/user-requests"
+              },
               "finish": {
                 "url": "/finish"
               }
diff --git a/fit/enduser-reference/src/test/resources/customTemplate.json b/fit/enduser-reference/src/test/resources/customTemplate.json
index e55ed4c..12c7f3b 100644
--- a/fit/enduser-reference/src/test/resources/customTemplate.json
+++ b/fit/enduser-reference/src/test/resources/customTemplate.json
@@ -74,6 +74,12 @@
               "resources": {
                 "url": "/resources"
               },
+              "userRequests": {
+                "url": "/user-requests"
+              },
+              "userRequestForms": {
+                "url": "/user-request-forms"
+              },
               "finish": {
                 "url": "/finish"
               }