Ver Fonte

Merge branch 'master' into feature/advanced-search

Jason Rivard há 7 anos atrás
pai
commit
5b4b69bc01
100 ficheiros alterados com 3341 adições e 973 exclusões
  1. 22 10
      .gitignore
  2. 2 4
      client/.gitignore
  3. 141 172
      client/package-lock.json
  4. 14 11
      client/package.json
  5. 1 0
      client/pom.xml
  6. 1 1
      client/src/components/changepassword/autogen-change-password.controller.ts
  7. 1 1
      client/src/components/changepassword/type-change-password.controller.ts
  8. 2 7
      client/src/index-dev.html
  9. 4 0
      client/src/modules/configeditor/configeditor.module.ts
  10. 6 6
      client/src/modules/helpdesk/helpdesk-detail.component.ts
  11. 2 2
      client/src/modules/helpdesk/helpdesk-search-cards.component.ts
  12. 2 2
      client/src/modules/helpdesk/helpdesk-search-table.component.ts
  13. 1 0
      client/src/modules/helpdesk/helpdesk.module.ts
  14. 5 1
      client/src/modules/helpdesk/main.dev.ts
  15. 5 1
      client/src/modules/helpdesk/main.ts
  16. 4 1
      client/src/modules/peoplesearch/main.dev.ts
  17. 4 1
      client/src/modules/peoplesearch/main.ts
  18. 2 2
      client/src/modules/peoplesearch/orgchart-search.component.ts
  19. 2 2
      client/src/modules/peoplesearch/orgchart.component.ts
  20. 2 2
      client/src/modules/peoplesearch/peoplesearch-cards.component.ts
  21. 2 2
      client/src/modules/peoplesearch/peoplesearch-table.component.ts
  22. 1 0
      client/src/modules/peoplesearch/peoplesearch.module.ts
  23. 1 1
      client/src/modules/peoplesearch/person-card.component.ts
  24. 2 2
      client/src/modules/peoplesearch/person-details-dialog.component.ts
  25. 1 1
      client/src/services/helpdesk.service.dev.ts
  26. 1 1
      client/src/services/people.service.dev.ts
  27. 3 3
      client/test/karma.conf.js
  28. 0 53
      client/webpack.build.js
  29. 0 110
      client/webpack.common.js
  30. 141 0
      client/webpack.config.js
  31. 0 49
      client/webpack.dev.js
  32. 2 0
      data-service/README.md
  33. 325 0
      data-service/checkstyle.xml
  34. 335 0
      data-service/pom.xml
  35. 55 0
      data-service/src/main/java/password/pwm/receiver/ContextManager.java
  36. 35 0
      data-service/src/main/java/password/pwm/receiver/CsvDownloadServlet.java
  37. 162 0
      data-service/src/main/java/password/pwm/receiver/FtpDataIngestor.java
  38. 41 0
      data-service/src/main/java/password/pwm/receiver/Logger.java
  39. 96 0
      data-service/src/main/java/password/pwm/receiver/PwmReceiverApp.java
  40. 40 0
      data-service/src/main/java/password/pwm/receiver/PwmReceiverLogger.java
  41. 95 0
      data-service/src/main/java/password/pwm/receiver/Settings.java
  42. 38 0
      data-service/src/main/java/password/pwm/receiver/Status.java
  43. 188 0
      data-service/src/main/java/password/pwm/receiver/Storage.java
  44. 194 0
      data-service/src/main/java/password/pwm/receiver/SummaryBean.java
  45. 97 0
      data-service/src/main/java/password/pwm/receiver/TelemetryRestReceiver.java
  46. 71 0
      data-service/src/main/java/password/pwm/receiver/TelemetryViewerServlet.java
  47. 0 0
      data-service/src/main/resources/password/pwm/receiver/package-info.java
  48. 28 0
      data-service/src/main/webapp/META-INF/context.xml
  49. 207 0
      data-service/src/main/webapp/WEB-INF/jsp/telemetry-viewer.jsp
  50. 58 0
      data-service/src/main/webapp/WEB-INF/web.xml
  51. 32 0
      data-service/src/main/webapp/index.jsp
  52. 94 0
      docker/pom.xml
  53. 11 0
      docker/readme.txt
  54. 23 0
      docker/src/main/docker/Dockerfile
  55. 3 0
      docker/src/main/docker/java.vmoptions
  56. 9 0
      docker/src/main/docker/startup.sh
  57. 0 18
      onejar/.gitignore
  58. 3 3
      onejar/pom.xml
  59. 1 1
      onejar/src/main/java/password/pwm/onejar/Argument.java
  60. 1 1
      onejar/src/main/java/password/pwm/onejar/ArgumentParser.java
  61. 1 1
      onejar/src/main/java/password/pwm/onejar/ArgumentParserException.java
  62. 1 1
      onejar/src/main/java/password/pwm/onejar/Resource.java
  63. 1 1
      onejar/src/main/java/password/pwm/onejar/TomcatConfig.java
  64. 1 1
      onejar/src/main/java/password/pwm/onejar/TomcatOneJarException.java
  65. 1 1
      onejar/src/main/java/password/pwm/onejar/TomcatOneJarMain.java
  66. 4 4
      onejar/src/main/java/password/pwm/onejar/WebServer.java
  67. 0 0
      onejar/src/main/resources/password/pwm/onejar/Resource.properties
  68. 3 0
      pom.xml
  69. 0 18
      server/.gitignore
  70. 63 57
      server/pom.xml
  71. 1 0
      server/src/build/checkstyle-import.xml
  72. 4 0
      server/src/build/spotbugs-exclude.xml
  73. 3 0
      server/src/main/java/password/pwm/AppProperty.java
  74. 93 180
      server/src/main/java/password/pwm/PwmAboutProperty.java
  75. 68 14
      server/src/main/java/password/pwm/PwmConstants.java
  76. 0 1
      server/src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java
  77. 20 9
      server/src/main/java/password/pwm/config/value/ActionValue.java
  78. 14 0
      server/src/main/java/password/pwm/config/value/EmailValue.java
  79. 3 0
      server/src/main/java/password/pwm/config/value/data/ActionConfiguration.java
  80. 5 1
      server/src/main/java/password/pwm/error/PwmError.java
  81. 1 0
      server/src/main/java/password/pwm/http/HttpContentType.java
  82. 18 17
      server/src/main/java/password/pwm/http/HttpHeader.java
  83. 11 3
      server/src/main/java/password/pwm/http/JspUtility.java
  84. 22 0
      server/src/main/java/password/pwm/http/PwmURL.java
  85. 2 0
      server/src/main/java/password/pwm/http/bean/ConfigGuideBean.java
  86. 2 0
      server/src/main/java/password/pwm/http/bean/ForgottenPasswordBean.java
  87. 2 0
      server/src/main/java/password/pwm/http/bean/UpdateProfileBean.java
  88. 2 2
      server/src/main/java/password/pwm/http/filter/RequestInitializationFilter.java
  89. 2 2
      server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java
  90. 216 34
      server/src/main/java/password/pwm/http/servlet/admin/AdminServlet.java
  91. 1 1
      server/src/main/java/password/pwm/http/servlet/admin/AppDashboardData.java
  92. 43 72
      server/src/main/java/password/pwm/http/servlet/admin/ReportStatusBean.java
  93. 2 2
      server/src/main/java/password/pwm/http/servlet/command/CommandServlet.java
  94. 71 50
      server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java
  95. 2 2
      server/src/main/java/password/pwm/http/servlet/forgottenpw/RemoteVerificationMethod.java
  96. 2 2
      server/src/main/java/password/pwm/http/servlet/oauth/OAuthMachine.java
  97. 6 6
      server/src/main/java/password/pwm/http/servlet/resource/ResourceFileServlet.java
  98. 15 1
      server/src/main/java/password/pwm/i18n/Admin.java
  99. 16 10
      server/src/main/java/password/pwm/ldap/LdapConnectionService.java
  100. 0 9
      server/src/main/java/password/pwm/ldap/ViewableUserInfoDisplayReader.java

+ 22 - 10
.gitignore

@@ -1,10 +1,22 @@
-/.idea
-/*.iml
-/client/target
-/client/.node
-/client/.idea
-/client/*.iml
-pwm-parent.iml
-pwmconfig/
-ssprconfig/
-/server/src/main/webapp/public/resources/webjars
+# PWM Project gitignore files
+
+# Eclipse Project Files
+/.project
+/.settings
+/.classpath
+
+# IntelliJ Project Files
+.idea/
+*.iml
+*/.idea/
+*/*.iml
+
+# OS folder info
+.directory
+.DS_Store
+.DS_Store?
+
+# Maven Output
+*/target
+
+

+ 2 - 4
client/.gitignore

@@ -1,5 +1,6 @@
 # NPM
 /node_modules
+.node
 
 # Generated Javascript files
 /src/**/*.js
@@ -7,8 +8,5 @@
 # Build output
 /dist
 
-# IDEA personal files
-/.idea/workspace.xml
-
 # Generated log files
-*.log
+*.log

Diff do ficheiro suprimidas por serem muito extensas
+ 141 - 172
client/package-lock.json


+ 14 - 11
client/package.json

@@ -8,31 +8,33 @@
         "npm": ">=3.9"
     },
     "scripts": {
-        "build": "webpack --config=webpack.build.js --mode=production",
+        "build": "webpack --mode=production",
         "clean": "rimraf dist/",
-        "test": "karma start --mode=development",
-        "test-single-run": "karma start --mode=development --singleRun --no-auto-watch",
-        "start": "webpack-dev-server --config=webpack.dev.js --mode=development --colors",
-        "sync": "webpack --config=webpack.build.js --mode=production --output-path=../server/target/pwm-1.8.0-SNAPSHOT/public/resources/webjars/pwm-client --watch --progress --colors"
+        "test": "karma start test/karma.conf.js --mode=development",
+        "test-single-run": "karma start test/karma.conf.js --mode=development --singleRun --no-auto-watch",
+        "start": "webpack-dev-server --mode=development --port 4000 --history-api-fallback --colors",
+        "sync": "webpack --mode=production --output-path=../server/target/pwm-1.8.0-SNAPSHOT/public/resources/webjars/pwm-client --watch --progress --colors"
     },
     "author": "",
     "license": "ISC",
-    "dependencies": {},
-    "devDependencies": {
+    "dependencies": {
         "@microfocus/ias-icons": "1.0.0-alpha",
         "@microfocus/ng-ias": "1.0.0-alpha.2",
         "@microfocus/ux-ias": "1.0.0-alpha.1",
+        "@uirouter/angularjs": "1.0.15",
+        "angular": "1.6.9",
+        "angular-aria": "1.6.9",
+        "angular-translate": "2.17.0",
+        "textangular": "1.5.16"
+    },
+    "devDependencies": {
         "@types/angular": "1.6.43",
         "@types/angular-mocks": "1.5.11",
         "@types/angular-translate": "2.15.2",
         "@types/angular-ui-router": "1.1.40",
         "@types/jasmine": "2.8.6",
         "@types/node": "9.4.7",
-        "@uirouter/angularjs": "1.0.15",
-        "angular": "1.6.9",
-        "angular-aria": "1.6.9",
         "angular-mocks": "1.6.9",
-        "angular-translate": "2.17.0",
         "autoprefixer": "8.1.0",
         "copy-webpack-plugin": "4.5.1",
         "css-loader": "0.28.10",
@@ -40,6 +42,7 @@
         "html-loader": "0.5.5",
         "html-webpack-plugin": "3.0.6",
         "ignore-loader": "0.1.2",
+        "imports-loader": "0.8.0",
         "jasmine": "3.1.0",
         "jasmine-core": "3.1.0",
         "jshint": "2.9.5",

+ 1 - 0
client/pom.xml

@@ -131,6 +131,7 @@
                             <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
                             <Implementation-URL>${project.organization.url}</Implementation-URL>
                             <Implementation-Build>${build.number}</Implementation-Build>
+                            <Implementation-Build-Timestamp>${timestamp.iso}</Implementation-Build-Timestamp>
                             <Implementation-Revision>${build.revision}</Implementation-Revision>
                             <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
                         </manifestEntries>

+ 1 - 1
client/src/components/changepassword/autogen-change-password.controller.ts

@@ -27,7 +27,7 @@ import {IChangePasswordSuccess} from './success-change-password.controller';
 
 const RANDOM_MAPPING_SIZE = 20;
 
-require('components/changepassword/autogen-change-password.component.scss');
+require('./autogen-change-password.component.scss');
 
 export default class AutogenChangePasswordController {
     fetchingRandoms: boolean;

+ 1 - 1
client/src/components/changepassword/type-change-password.controller.ts

@@ -28,7 +28,7 @@ import {IChangePasswordSuccess} from './success-change-password.controller';
 import {IPasswordService, IValidatePasswordData} from '../../services/password.service';
 import IPwmService from '../../services/pwm.service';
 
-require('components/changepassword/type-change-password.component.scss');
+require('./type-change-password.component.scss');
 
 const EMPTY_MATCH_STATUS = 'EMPTY';
 const IN_PROGRESS_MESSAGE_WAIT_MS = 5;

+ 2 - 7
client/index.html → client/src/index-dev.html

@@ -27,8 +27,8 @@
     <meta name="viewport" content="initial-scale=1, maximum-scale=1">
     <title>PWM Development</title>
 
-    <link rel="stylesheet" href="vendor/ias-icons.css">
-    <link rel="stylesheet" href="vendor/ux-ias.css">
+    <link rel="stylesheet" href="vendor/ux-ias/ias-icons.css">
+    <link rel="stylesheet" href="vendor/ux-ias/ux-ias.css">
     <style>
         html, body {
             margin: 0;
@@ -40,10 +40,5 @@
 <body ng-cloak>
 <ui-view>Loading...</ui-view>
 
-<script src="vendor/angular.js"></script>
-<script src="vendor/angular-aria.js"></script>
-<script src="vendor/angular-translate.js"></script>
-<script src="vendor/angular-ui-router.js"></script>
-<script src="vendor/ng-ias.js"></script>
 </body>
 </html>

+ 4 - 0
client/src/modules/configeditor/configeditor.module.ts

@@ -20,7 +20,11 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
+import 'textangular';
+import 'textangular/dist/textAngular-sanitize';
+
 import { module } from 'angular';
+
 import ConfigEditorController from './configeditor.controller';
 
 module('configeditor.module', ['textAngular'])

+ 6 - 6
client/src/modules/helpdesk/helpdesk-detail.component.ts

@@ -29,11 +29,11 @@ import {IActionButton, IHelpDeskConfigService, PASSWORD_UI_MODES} from '../../se
 import {IPerson} from '../../models/person.model';
 import {IChangePasswordSuccess} from '../../components/changepassword/success-change-password.controller';
 
-let autogenChangePasswordTemplateUrl = require('components/changepassword/autogen-change-password.component.html');
+let autogenChangePasswordTemplateUrl = require('../../components/changepassword/autogen-change-password.component.html');
 let helpdeskDetailDialogTemplateUrl = require('./helpdesk-detail-dialog.template.html');
-let randomChangePasswordTemplateUrl = require('components/changepassword/random-change-password.component.html');
-let successChangePasswordTemplateUrl = require('components/changepassword/success-change-password.component.html');
-let typeChangePasswordTemplateUrl = require('components/changepassword/type-change-password.component.html');
+let randomChangePasswordTemplateUrl = require('../../components/changepassword/random-change-password.component.html');
+let successChangePasswordTemplateUrl = require('../../components/changepassword/success-change-password.component.html');
+let typeChangePasswordTemplateUrl = require('../../components/changepassword/type-change-password.component.html');
 let verificationsDialogTemplateUrl = require('./verifications-dialog.template.html');
 
 const STATUS_WAIT = 'wait';
@@ -42,8 +42,8 @@ const STATUS_SUCCESS = 'success';
 const PROFILE_TAB_NAME = 'profileTab';
 
 @Component({
-    stylesheetUrl: require('modules/helpdesk/helpdesk-detail.component.scss'),
-    templateUrl: require('modules/helpdesk/helpdesk-detail.component.html')
+    stylesheetUrl: require('./helpdesk-detail.component.scss'),
+    templateUrl: require('./helpdesk-detail.component.html')
 })
 export default class HelpDeskDetailComponent {
     customButtons: {[key: string]: IActionButton};

+ 2 - 2
client/src/modules/helpdesk/helpdesk-search-cards.component.ts

@@ -33,8 +33,8 @@ import {IHelpDeskService} from '../../services/helpdesk.service';
 import IPwmService from '../../services/pwm.service';
 
 @Component({
-    stylesheetUrl: require('modules/helpdesk/helpdesk-search.component.scss'),
-    templateUrl: require('modules/helpdesk/helpdesk-search-cards.component.html')
+    stylesheetUrl: require('./helpdesk-search.component.scss'),
+    templateUrl: require('./helpdesk-search-cards.component.html')
 })
 export default class HelpDeskSearchCardsComponent extends HelpDeskSearchBaseComponent {
     static $inject = [

+ 2 - 2
client/src/modules/helpdesk/helpdesk-search-table.component.ts

@@ -32,8 +32,8 @@ import {IHelpDeskService} from '../../services/helpdesk.service';
 import IPwmService from '../../services/pwm.service';
 
 @Component({
-    stylesheetUrl: require('modules/helpdesk/helpdesk-search.component.scss'),
-    templateUrl: require('modules/helpdesk/helpdesk-search-table.component.html')
+    stylesheetUrl: require('./helpdesk-search.component.scss'),
+    templateUrl: require('./helpdesk-search-table.component.html')
 })
 export default class HelpDeskSearchTableComponent extends HelpDeskSearchBaseComponent {
     columnConfiguration: any;

+ 1 - 0
client/src/modules/helpdesk/helpdesk.module.ts

@@ -20,6 +20,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
+import 'angular-aria';
 
 import {IComponentOptions, module} from 'angular';
 import DateFilter from './date.filters';

+ 5 - 1
client/src/modules/helpdesk/main.dev.ts

@@ -20,6 +20,10 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
+import 'angular';
+import 'angular-translate';
+import '@microfocus/ng-ias/dist/ng-ias';
+
 import { bootstrap, module } from 'angular';
 import helpDeskModule from './helpdesk.module';
 import routes from './routes';
@@ -38,7 +42,7 @@ module('app', [
     'ng-ias'
 ])
     .config(['$translateProvider', ($translateProvider: angular.translate.ITranslateProvider) => {
-        $translateProvider.translations('en', require('i18n/translations_en.json'));
+        $translateProvider.translations('en', require('../../i18n/translations_en.json'));
         $translateProvider.useSanitizeValueStrategy('escapeParameters');
         $translateProvider.preferredLanguage('en');
     }])

+ 5 - 1
client/src/modules/helpdesk/main.ts

@@ -20,6 +20,10 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
+import 'angular';
+import 'angular-translate';
+import '@microfocus/ng-ias/dist/ng-ias';
+
 import { bootstrap, module } from 'angular';
 import helpDeskModule from './helpdesk.module';
 import PeopleService from '../../services/people.service';
@@ -43,7 +47,7 @@ module('app', [
         '$translateProvider',
         ($translateProvider: angular.translate.ITranslateProvider) => {
             $translateProvider
-                .translations('fallback', require('i18n/translations_en.json'))
+                .translations('fallback', require('../../i18n/translations_en.json'))
                 .useLoader('translationsLoader')
                 .useSanitizeValueStrategy('escapeParameters')
                 .preferredLanguage('en')

+ 4 - 1
client/src/modules/peoplesearch/main.dev.ts

@@ -20,6 +20,9 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
+import 'angular';
+import 'angular-translate';
+import '@microfocus/ng-ias/dist/ng-ias';
 
 import { bootstrap, module } from 'angular';
 import ConfigService from '../../services/peoplesearch-config.service.dev';
@@ -40,7 +43,7 @@ module('app', [
 
     .config(routes)
     .config(['$translateProvider', ($translateProvider: angular.translate.ITranslateProvider) => {
-        $translateProvider.translations('en', require('i18n/translations_en.json'));
+        $translateProvider.translations('en', require('../../i18n/translations_en.json'));
         $translateProvider.useSanitizeValueStrategy('escapeParameters');
         $translateProvider.preferredLanguage('en');
     }])

+ 4 - 1
client/src/modules/peoplesearch/main.ts

@@ -20,6 +20,9 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
+import 'angular';
+import 'angular-translate';
+import '@microfocus/ng-ias/dist/ng-ias';
 
 import { bootstrap, module } from 'angular';
 import ConfigService from '../../services/peoplesearch-config.service';
@@ -43,7 +46,7 @@ module('app', [
         '$translateProvider',
         ($translateProvider: angular.translate.ITranslateProvider) => {
             $translateProvider
-                .translations('fallback', require('i18n/translations_en.json'))
+                .translations('fallback', require('../../i18n/translations_en.json'))
                 .useLoader('translationsLoader')
                 .useSanitizeValueStrategy('escapeParameters')
                 .preferredLanguage('en')

+ 2 - 2
client/src/modules/peoplesearch/orgchart-search.component.ts

@@ -31,8 +31,8 @@ import IOrgChartData from '../../models/orgchart-data.model';
 import { IPerson } from '../../models/person.model';
 
 @Component({
-    stylesheetUrl: require('modules/peoplesearch/orgchart-search.component.scss'),
-    templateUrl: require('modules/peoplesearch/orgchart-search.component.html')
+    stylesheetUrl: require('./orgchart-search.component.scss'),
+    templateUrl: require('./orgchart-search.component.html')
 })
 export default class OrgChartSearchComponent {
     directReports: IPerson[];

+ 2 - 2
client/src/modules/peoplesearch/orgchart.component.ts

@@ -41,8 +41,8 @@ export enum OrgChartSize {
         person: '<',
         showImages: '<'
     },
-    stylesheetUrl: require('modules/peoplesearch/orgchart.component.scss'),
-    templateUrl: require('modules/peoplesearch/orgchart.component.html')
+    stylesheetUrl: require('./orgchart.component.scss'),
+    templateUrl: require('./orgchart.component.html')
 })
 export default class OrgChartComponent {
     directReports: IPerson[];

+ 2 - 2
client/src/modules/peoplesearch/peoplesearch-cards.component.ts

@@ -40,8 +40,8 @@ export enum PeopleSearchCardsSize {
 }
 
 @Component({
-    stylesheetUrl: require('modules/peoplesearch/peoplesearch-cards.component.scss'),
-    templateUrl: require('modules/peoplesearch/peoplesearch-cards.component.html')
+    stylesheetUrl: require('./peoplesearch-cards.component.scss'),
+    templateUrl: require('./peoplesearch-cards.component.html')
 })
 export default class PeopleSearchCardsComponent extends PeopleSearchBaseComponent {
     photosEnabled: boolean;

+ 2 - 2
client/src/modules/peoplesearch/peoplesearch-table.component.ts

@@ -32,8 +32,8 @@ import PromiseService from '../../services/promise.service';
 import SearchResult from '../../models/search-result.model';
 
 @Component({
-    stylesheetUrl: require('modules/peoplesearch/peoplesearch-table.component.scss'),
-    templateUrl: require('modules/peoplesearch/peoplesearch-table.component.html')
+    stylesheetUrl: require('./peoplesearch-table.component.scss'),
+    templateUrl: require('./peoplesearch-table.component.html')
 })
 export default class PeopleSearchTableComponent extends PeopleSearchBaseComponent {
     columnConfiguration: any;

+ 1 - 0
client/src/modules/peoplesearch/peoplesearch.module.ts

@@ -20,6 +20,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
+import 'angular-aria';
 
 import {IComponentOptions, module} from 'angular';
 import { HighlightFilter } from './string.filters';

+ 1 - 1
client/src/modules/peoplesearch/person-card.component.ts

@@ -25,7 +25,7 @@ import {IAugmentedJQuery} from 'angular';
 import { IPerson } from '../../models/person.model';
 import { IPeopleService } from '../../services/people.service';
 
-const templateUrl = require('modules/peoplesearch/person-card.component.html');
+const templateUrl = require('./person-card.component.html');
 
 class PersonCardController {
     private details: any[]; // For large style cards

+ 2 - 2
client/src/modules/peoplesearch/person-details-dialog.component.ts

@@ -28,8 +28,8 @@ import { IAugmentedJQuery, ITimeoutService } from 'angular';
 import { IPerson } from '../../models/person.model';
 
 @Component({
-    stylesheetUrl: require('modules/peoplesearch/person-details-dialog.component.scss'),
-    templateUrl: require('modules/peoplesearch/person-details-dialog.component.html')
+    stylesheetUrl: require('./person-details-dialog.component.scss'),
+    templateUrl: require('./person-details-dialog.component.html')
 })
 export default class PersonDetailsDialogComponent {
     person: IPerson;

+ 1 - 1
client/src/services/helpdesk.service.dev.ts

@@ -28,7 +28,7 @@ import {IPromise, IQService, ITimeoutService, IWindowService} from 'angular';
 import {IPerson} from '../models/person.model';
 import SearchResult from '../models/search-result.model';
 
-const peopleData = require('./people.data');
+const peopleData = require('./people.data.json');
 
 const MAX_RESULTS = 10;
 const SIMULATED_RESPONSE_TIME = 300;

+ 1 - 1
client/src/services/people.service.dev.ts

@@ -27,7 +27,7 @@ import {IPeopleService, IQuery} from './people.service';
 import IOrgChartData from '../models/orgchart-data.model';
 import SearchResult from '../models/search-result.model';
 
-const peopleData = require('./people.data');
+const peopleData = require('./people.data.json');
 
 const MAX_RESULTS = 10;
 const SIMULATED_RESPONSE_TIME = 0;

+ 3 - 3
client/karma.conf.js → client/test/karma.conf.js

@@ -21,14 +21,14 @@
  */
 
 var webpack = require('webpack');
-var webpackConfig = require('./webpack.test.js');
+var webpackConfig = require('../webpack.test.js');
 var path = require("path");
 var os = require('os');
 
 module.exports = function (config) {
     config.set({
         // base path that will be used to resolve all patterns (eg. files, exclude)
-        basePath: '',
+        basePath: '..',
 
         // frameworks to use
         // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
@@ -36,7 +36,7 @@ module.exports = function (config) {
 
         // list of files / patterns to load in the browser
         files: [
-            'test/karma-test-suite.ts'
+            'karma-test-suite.ts'
         ],
 
         exclude: [],

+ 0 - 53
client/webpack.build.js

@@ -1,53 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2018 The PWM Project
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- */
-
-
-var commonConfig = require('./webpack.common.js');
-var CopyWebpackPlugin = require('copy-webpack-plugin');
-var UglifyJsPlugin = require('uglifyjs-webpack-plugin');
-var webpack = require('webpack');
-var webpackMerge = require('webpack-merge');
-
-module.exports = webpackMerge(commonConfig, {
-    devtool: 'source-map',
-    entry: {
-        'peoplesearch.ng': './src/modules/peoplesearch/main',
-        'changepassword.ng': './src/modules/changepassword/changepassword.module',
-        'configeditor.ng': './src/modules/configeditor/configeditor.module',
-        'helpdesk.ng': './src/modules/helpdesk/main'
-    },
-    plugins: [
-        new CopyWebpackPlugin([
-            { from: 'node_modules/@microfocus/ux-ias/dist/ux-ias.css', to: 'vendor/' },
-            { from: 'node_modules/@microfocus/ng-ias/dist/ng-ias.js', to: 'vendor/' },
-            { from: 'node_modules/@microfocus/ias-icons/dist/ias-icons.css', to: 'vendor/' },
-            { from: 'node_modules/@microfocus/ias-icons/dist/fonts', to: 'vendor/fonts' }
-        ]),
-        new UglifyJsPlugin({
-            sourceMap: true,
-            uglifyOptions: {
-                compress: {warnings: false},
-                comments: false
-            }
-        })
-    ]
-});

+ 0 - 110
client/webpack.common.js

@@ -1,110 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2018 The PWM Project
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- */
-
-
-var HtmlWebpackPlugin = require('html-webpack-plugin');
-var autoPrefixer = require('autoprefixer');
-var path = require('path');
-var webpack = require('webpack');
-
-var outDir = path.resolve(__dirname, 'dist');
-
-module.exports = {
-    devServer: {
-        contentBase: outDir,
-        // outputPath: outDir,
-        port: 4000,
-        historyApiFallback: true
-    },
-    // Externals copied to /dist via CopyWebpackPlugin
-    externals:
-    {
-        'angular': true,
-        // Wrapped in window because of hyphens
-        'angular-ui-router': 'window["angular-ui-router"]',
-        'angular-translate': 'window["angular-translate"]'
-    },
-    module: {
-        rules: [
-            {
-                test: /\.ts$/,
-                enforce: 'pre',
-                loader: 'tslint-loader'
-            },
-            {
-                test: /\.ts$/,
-                loader: 'ts-loader',
-                exclude: /node_modules/
-            },
-            {
-                test: /index\.html$/,
-                loader: 'html-loader',
-                exclude: /node_modules/
-            },
-            {
-                test: /\.html$/,
-                loader: 'ngtemplate-loader?relativeTo=' + (path.resolve(__dirname, './src')) + '/!html-loader',
-                exclude: /index\.html$/
-            },
-            {
-                test: /\.scss$/,
-                loaders: [ 'style-loader', 'css-loader', 'sass-loader', {
-                    loader: 'postcss-loader',
-                    options: {
-                        plugins: function () {
-                            return [autoPrefixer('last 2 versions')]
-                        }
-                    }
-                }]
-            },
-            {
-                test: /\.(png|jpg|jpeg|gif|svg)$/,
-                loaders: [ 'url-loader?limit=25000' ]
-            }
-        ]
-    },
-    // [name] is replaced by entry point name
-    output: {
-        filename: '[name].js',
-        path: outDir
-    },
-    plugins: [
-        new HtmlWebpackPlugin({
-            chunks: ['peoplesearch.ng'],
-            filename: 'peoplesearch.html',
-            template: 'index.html',
-            // title: 'PeopleSearch Development',
-            inject: 'body'
-        }),
-        new HtmlWebpackPlugin({
-            chunks: ['helpdesk.ng'],
-            filename: 'helpdesk.html',
-            template: 'index.html',
-            // title: 'PeopleSearch Development',
-            inject: 'body'
-        })
-    ],
-    resolve: {
-        extensions: [ '.ts', '.js', '.json' ],
-        modules: ['./src', './vendor', 'node_modules']
-    }
-};

+ 141 - 0
client/webpack.config.js

@@ -0,0 +1,141 @@
+const path = require('path');
+const HtmlWebpackPlugin = require('html-webpack-plugin');
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
+const webpackMerge = require('webpack-merge');
+const webpack = require('webpack');
+const autoPrefixer = require('autoprefixer');
+
+const outDir = path.resolve(__dirname, 'dist');
+const srcDir = path.resolve(__dirname, 'src');
+
+module.exports = function (env, argv) {
+    const isProductionMode = (argv["mode"] === "production");
+
+    const commonConfig = {
+        devtool: 'source-map',
+        entry: {
+            'changepassword.ng': './src/modules/changepassword/changepassword.module',
+            'configeditor.ng': './src/modules/configeditor/configeditor.module'
+
+            // (see production and development specific sections below for more entries)
+        },
+        output: {
+            filename: "[name].js",
+            path: outDir
+        },
+        resolve: {
+            extensions: [".ts", ".js"]
+        },
+        module: {
+            rules: [
+                {
+                    test: /.ts$/,
+                    loader: "ts-loader"
+                },
+                {
+                    test: /\.ts$/,
+                    enforce: 'pre',
+                    loader: 'tslint-loader'
+                },
+                {
+                    test: /index-dev\.html$/,
+                    loader: 'html-loader',
+                    exclude: /node_modules/
+                },
+                {
+                    test: /\.html$/,
+                    loader: 'ngtemplate-loader!html-loader',
+                    exclude: /index-dev\.html$/
+                },
+                {
+                    test: /\.(scss)$/,
+                    loaders: [ 'style-loader', 'css-loader', 'sass-loader', {
+                        loader: 'postcss-loader',
+                        options: {
+                            plugins: function () {
+                                return [autoPrefixer('last 2 versions')]
+                            }
+                        }
+                    }]
+                },
+                {
+                    test: /\.(png|jpg|jpeg|gif|svg)$/,
+                    loaders: [ 'url-loader?limit=25000' ]
+                },
+                {
+                    test: [
+                        require.resolve("textangular"),
+                        require.resolve("textangular/dist/textAngular-sanitize")
+                    ],
+                    use: "imports-loader?angular"
+                }
+            ]
+        },
+        plugins: [
+            new CopyWebpackPlugin([
+                { from: 'node_modules/@microfocus/ux-ias/dist/ux-ias.css', to: 'vendor/ux-ias/' },
+                { from: 'node_modules/@microfocus/ias-icons/dist/ias-icons.css', to: 'vendor/ux-ias/' },
+                { from: 'node_modules/@microfocus/ias-icons/dist/fonts', to: 'vendor/ux-ias/fonts' },
+                { from: 'node_modules/textangular/dist/textAngular.css', to: 'vendor/textangular' }
+            ])
+        ],
+        optimization: {
+            splitChunks: {
+                cacheGroups: {
+                    vendor: {
+                        test: /[\\/]node_modules[\\/]/,
+                        name: "vendor",
+                        chunks: "all"
+                    }
+                }
+            }
+        }
+    };
+
+    if (isProductionMode) {
+        // Production-specific configuration
+        return webpackMerge(commonConfig, {
+            entry: {
+                'peoplesearch.ng': './src/modules/peoplesearch/main',
+                'helpdesk.ng': './src/modules/helpdesk/main'
+            },
+            plugins: [
+                new UglifyJsPlugin({
+                    sourceMap: true,
+                    uglifyOptions: {
+                        compress: {warnings: false},
+                        comments: false
+                    }
+                })
+            ]
+        });
+    }
+    else {
+        // Development-specific configuration
+        return webpackMerge(commonConfig, {
+            entry: {
+                'peoplesearch.ng': './src/modules/peoplesearch/main.dev',
+                'helpdesk.ng': './src/modules/helpdesk/main.dev'
+            },
+            plugins: [
+                new HtmlWebpackPlugin({
+                    chunks: ['peoplesearch.ng', 'vendor'],
+                    chunksSortMode: 'dependency',
+                    filename: 'peoplesearch.html',
+                    template: 'src/index-dev.html',
+                    inject: 'body',
+                    livereload: true
+                }),
+                new HtmlWebpackPlugin({
+                    chunks: ['helpdesk.ng', 'vendor'],
+                    chunksSortMode: 'dependency',
+                    filename: 'helpdesk.html',
+                    template: 'src/index-dev.html',
+                    inject: 'body',
+                    livereload: true
+                })
+            ],
+        });
+    }
+};

+ 0 - 49
client/webpack.dev.js

@@ -1,49 +0,0 @@
-/*
- * Password Management Servlets (PWM)
- * http://www.pwm-project.org
- *
- * Copyright (c) 2006-2009 Novell, Inc.
- * Copyright (c) 2009-2018 The PWM Project
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
- */
-
-
-var commonConfig = require('./webpack.common.js');
-var CopyWebpackPlugin = require('copy-webpack-plugin');
-var webpackMerge = require('webpack-merge');
-
-module.exports = webpackMerge(commonConfig, {
-    devtool: 'cheap-module-source-map',
-    entry: {
-        'peoplesearch.ng': './src/modules/peoplesearch/main.dev',
-        'changepassword.ng': './src/modules/changepassword/changepassword.module',
-        'configeditor.ng': './src/modules/configeditor/configeditor.module',
-        'helpdesk.ng': './src/modules/helpdesk/main.dev'
-    },
-    plugins: [
-        // Don't forget to add this to karma.conf.js
-        new CopyWebpackPlugin([
-            { from: 'node_modules/@uirouter/angularjs/release/angular-ui-router.js', to: 'vendor/' },
-            { from: 'node_modules/angular/angular.js', to: 'vendor/' },
-            { from: 'node_modules/angular-aria/angular-aria.js', to: 'vendor/' },
-            { from: 'node_modules/angular-translate/dist/angular-translate.js', to: 'vendor/' },
-            { from: 'node_modules/@microfocus/ux-ias/dist/ux-ias.css', to: 'vendor/' },
-            { from: 'node_modules/@microfocus/ng-ias/dist/ng-ias.js', to: 'vendor/' },
-            { from: 'node_modules/@microfocus/ias-icons/dist/ias-icons.css', to: 'vendor/' },
-            { from: 'node_modules/@microfocus/ias-icons/dist/fonts', to: 'vendor/fonts' }
-        ])
-    ]
-});

+ 2 - 0
data-service/README.md

@@ -0,0 +1,2 @@
+# pwm-data-service
+Cloud service for PWM

+ 325 - 0
data-service/checkstyle.xml

@@ -0,0 +1,325 @@
+<?xml version="1.0"?>
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2016 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  -->
+
+<!DOCTYPE module PUBLIC
+        "-//Puppy Crawl//DTD Check Configuration 1.2//EN"
+        "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
+
+<!--
+  PWM Checkstyle definition
+-->
+
+<module name="Checker">
+
+    <!-- Checks that each Java package has a Javadoc file used for commenting. -->
+    <!-- See http://checkstyle.sf.net/config_javadoc.html#JavadocPackage       -->
+    <!--module name="JavadocPackage">
+      <property name="allowLegacy" value="true"/>
+    </module-->
+
+    <module name="FileLength"/>
+
+    <!-- Checks for Headers                              -->
+    <!-- See http://checkstyle.sf.net/config_header.html -->
+    <!--
+    <module name="RegexpHeader">
+        <property name="fileExtensions" value="java"/>
+        <property name="headerFile" value="${checkstyle.header.file}"/>
+    </module>
+    -->
+
+    <module name="FileTabCharacter">
+        <property name="eachLine" value="true"/>
+    </module>
+    <module name="NewlineAtEndOfFile"/>
+
+    <module name="TreeWalker">
+
+        <property name="cacheFile" value="target/checkstyle.cache"/>
+
+        <!-- required for SuppressWarningsFilter (and other Suppress* rules not used here) -->
+        <!-- see http://checkstyle.sourceforge.net/config_annotation.html#SuppressWarningsHolder -->
+        <module name="SuppressWarningsHolder"/>
+
+        <module name="OuterTypeFilename"/>
+        <module name="IllegalTokenText">
+            <property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
+            <property name="format" value="\\u00(08|09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
+            <property name="message" value="Avoid using corresponding octal or Unicode escape."/>
+        </module>
+        <module name="AvoidEscapedUnicodeCharacters">
+            <property name="allowEscapesForControlCharacters" value="true"/>
+            <property name="allowByTailComment" value="true"/>
+            <property name="allowNonPrintableEscapes" value="true"/>
+        </module>
+
+        <!--
+        <module name="LineLength">
+            <property name="max" value="200" />
+            <property name="ignorePattern" value="@version|@see|@todo|TODO"/>
+        </module>
+        -->
+        <!-- required for SuppressionCommentFilter -->
+        <!-- see http://checkstyle.sourceforge.net/config.html#SuppressionCommentFilter -->
+        <!--
+        <module name="FileContentsHolder"/>
+
+
+        -->
+
+        <module name="EmptyBlock">
+            <property name="option" value="TEXT"/>
+            <property name="tokens" value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
+        </module>
+        <!--
+        <module name="LeftCurly">
+            <property name="option" value="nl"/>
+            <property name="maxLineLength" value="100"/>
+        </module>
+        -->
+
+        <module name="RightCurly"/>
+        <module name="RightCurly">
+            <property name="option" value="alone"/>
+            <property name="tokens" value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT, INSTANCE_INIT"/>
+        </module>
+
+        <!--
+        <module name="MemberName" />
+        -->
+
+        <!-- Checks for Javadoc comments.                     -->
+        <!-- See http://checkstyle.sf.net/config_javadoc.html -->
+        <!--
+        <module name="JavadocMethod">
+            <property name="severity" value="warning"/>
+            <property name="scope" value="protected"/>
+        </module>
+        <module name="JavadocType">
+            <property name="scope" value="protected"/>
+            <property name="allowUnknownTags" value="true" />
+        </module>
+        <module name="JavadocVariable">
+            <property name="severity" value="info"/>
+            <property name="scope" value="protected"/>
+        </module>
+        -->
+
+        <module name="AnnotationLocation">
+            <property name="tokens" value="VARIABLE_DEF"/>
+            <property name="allowSamelineMultipleAnnotations" value="true"/>
+        </module>
+
+        <!-- Checks for Naming Conventions.                  -->
+        <!-- See http://checkstyle.sf.net/config_naming.html -->
+        <!--
+        <module name="MemberName">
+            <property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
+        </module>
+        <module name="TypeName">
+        -->
+        <module name="ConstantName"/>
+        <module name="PackageName">
+            <property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
+        </module>
+        <module name="LocalVariableName">
+            <property name="tokens" value="VARIABLE_DEF"/>
+            <property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
+            <property name="allowOneCharVarInForLoop" value="true"/>
+        </module>
+        <!--
+        <module name="ClassTypeParameterName">
+            <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
+        </module>
+        -->
+        <module name="MethodTypeParameterName">
+            <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
+        </module>
+        <module name="InterfaceTypeParameterName">
+            <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
+        </module>
+        <!--
+        <module name="LocalFinalVariableName"/>
+        <module name="LocalVariableName"/>
+        <module name="MethodName"/>
+        <module name="PackageName"/>
+        <module name="ParameterName"/>
+        <module name="StaticVariableName"/>
+        <module name="TypeName"/>
+        -->
+
+        <!-- Checks for imports                              -->
+        <!-- See http://checkstyle.sf.net/config_import.html -->
+        <module name="AvoidStarImport"/>
+        <module name="AvoidStaticImport"/>
+        <module name="IllegalImport"/>
+        <module name="RedundantImport"/>
+        <module name="UnusedImports"/>
+
+
+        <!-- Checks for Size Violations.                    -->
+        <!-- See http://checkstyle.sf.net/config_sizes.html -->
+        <!--
+        <module name="MethodLength"/>
+        <module name="ParameterNumber"/>
+        -->
+
+
+        <!-- Checks for whitespace                               -->
+        <!-- See http://checkstyle.sf.net/config_whitespace.html -->
+        <module name="EmptyForIteratorPad">
+            <property name="option" value="space"/>
+        </module>
+        <module name="EmptyForInitializerPad"/>
+        <module name="NeedBraces"/>
+        <!--
+        -->
+        <!-- module name="NoWhitespaceAfter"/ -->
+        <!-- module name="NoWhitespaceBefore"/ -->
+        <!--
+        <module name="OperatorWrap"/>
+        <module name="ParenPad">
+            <property name="option" value="space" />
+        </module>
+        <module name="WhitespaceAfter"/>
+        <module name="WhitespaceAround"/>
+        -->
+        <!-- module name="MethodParamPad"/ -->
+        <module name="GenericWhitespace"/>
+        <module name="EmptyLineSeparator">
+            <property name="allowNoEmptyLineBetweenFields" value="true"/>
+        </module>
+
+
+
+        <!-- Modifier Checks                                    -->
+        <!-- See http://checkstyle.sf.net/config_modifiers.html -->
+        <module name="ModifierOrder"/>
+        <module name="RedundantModifier"/>
+        <!--
+        -->
+
+
+        <!-- Checks for blocks. You know, those {}'s         -->
+        <!-- See http://checkstyle.sf.net/config_blocks.html -->
+        <!--
+        <module name="AvoidNestedBlocks"/>
+        -->
+
+
+        <!-- Checks for common coding problems               -->
+        <!-- See http://checkstyle.sf.net/config_coding.html -->
+        <!-- module name="AvoidInlineConditionals"/ -->
+        <!--
+        <module name="EmptyStatement"/>
+        <module name="EqualsHashCode"/>
+        <module name="HiddenField">
+            <property name="severity" value="warning"/>
+            <property name="ignoreSetter" value="true"/>
+            <property name="ignoreConstructorParameter" value="true"/>
+        </module>
+        <module name="IllegalInstantiation"/>
+        <module name="InnerAssignment"/>
+        -->
+        <!--
+        <module name="MagicNumber">
+            <property name="ignoreNumbers" value="-4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 31, 32, 37, 64, 100, 128, 256, 512, 1000, 1024"/>
+        </module>
+        -->
+
+        <!-- Checks for class design                         -->
+        <!-- See http://checkstyle.sf.net/config_design.html -->
+        <!-- module name="DesignForExtension"/ -->
+        <!-- module name="FinalClass"/ -->
+        <!-- module name="HideUtilityClassConstructor"/ -->
+        <!--
+        <module name="InterfaceIsType"/>
+        <module name="VisibilityModifier">
+            <property name="protectedAllowed" value="true"/>
+            <property name="packageAllowed" value="true"/>
+        </module>
+        -->
+
+
+        <!-- future enabled checks -->
+        <!--
+        <module name="TrailingComment"/>
+        <module name="NPathComplexity"/>
+        <module name="EnumTrailingCommaCheck"/> //doesnt yet exist as of checkstyle 2.17
+        <module name="MultipleStringLiterals"/>
+        <module name="InnerAssignment"/>
+        <module name="MagicNumber">
+            <property name="ignoreNumbers" value="-4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 31, 32, 37, 64, 100, 128, 256, 512, 1000, 1024"/>
+        </module>
+        <module name="SimplifyBooleanExpression"/>
+        -->
+
+        <!-- coding -->
+        <module name="FallThrough"/>
+        <module name="EqualsHashCode"/>
+        <module name="ArrayTrailingCommaCheck"/>
+        <module name="FinalLocalVariable"/>
+        <module name="MissingSwitchDefault"/>
+        <module name="ModifiedControlVariable"/>
+        <module name="MultipleVariableDeclarations"/>
+        <module name="OneStatementPerLine"/>
+        <module name="FinalParameters"/>
+        <module name="ParameterAssignment"/>
+        <module name="SimplifyBooleanReturn"/>
+        <module name="StringLiteralEquality"/>
+        <module name="CovariantEquals"/>
+        <module name="DefaultComesLast"/>
+        <module name="EmptyStatement"/>
+        <module name="EqualsHashCode"/>
+        <module name="EqualsAvoidNull"/>
+
+        <module name="MutableException"/>
+        <module name="TodoComment"/>
+        <module name="NoLineWrap"/>
+        <module name="OneTopLevelClass"/>
+        <module name="NoFinalizer"/>
+        <module name="ArrayTypeStyle"/>
+        <module name="UpperEll"/>
+        <module name="PackageDeclaration"/>
+        <module name="NoClone"/>
+    </module>
+
+    <!-- Support @SuppressWarnings (added in Checkstyle 5.7) -->
+    <!-- see http://checkstyle.sourceforge.net/config.html#SuppressWarningsFilter -->
+    <module name="SuppressWarningsFilter"/>
+
+    <!-- Checks properties file for a duplicated properties. -->
+    <!-- See http://checkstyle.sourceforge.net/config_misc.html#UniqueProperties -->
+    <module name="UniqueProperties"/>
+
+    <!-- Support CHECKSTYLE_OFF: regexp and CHECKSTYLE_ON: regexp comments to disable/enable some checks -->
+    <!-- see http://checkstyle.sourceforge.net/config.html#SuppressionCommentFilter -->
+    <!--
+    <module name="SuppressionCommentFilter">
+        <property name="offCommentFormat" value="CHECKSTYLE_OFF\: (.+)"/>
+        <property name="onCommentFormat" value="CHECKSTYLE_ON\: (.+)"/>
+        <property name="checkFormat" value="$1"/>
+    </module>
+    -->
+
+</module>

+ 335 - 0
data-service/pom.xml

@@ -0,0 +1,335 @@
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <parent>
+        <groupId>org.pwm-project</groupId>
+        <artifactId>pwm-parent</artifactId>
+        <version>1.8.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>pwm-data-service</artifactId>
+
+    <packaging>war</packaging>
+
+    <name>PWM Password Self Service: Data Service</name>
+
+    <licenses>
+        <license>
+            <name>The GNU General Public License (GPL) Version 2</name>
+            <url>http://www.gnu.org/licenses/gpl-2.0.html</url>
+            <distribution>repo</distribution>
+        </license>
+    </licenses>
+
+    <organization>
+        <name>PWM Project</name>
+        <url>http://www.pwm-project.org</url>
+    </organization>
+
+    <properties>
+        <maven.compiler.source>1.8</maven.compiler.source>
+        <maven.compiler.target>1.8</maven.compiler.target>
+        <skipTests>false</skipTests>
+        <timestamp.iso>${maven.build.timestamp}</timestamp.iso>
+        <maven.build.timestamp.format>yyyy-MM-dd'T'HH:mm:ss'Z'</maven.build.timestamp.format>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <build.number>0</build.number>  <!-- default in case not set on command line -->
+        <build.revision>0</build.revision>  <!-- default in case not set on command line -->
+    </properties>
+
+    <profiles>
+        <profile>
+            <id>skip-all</id>
+            <properties>
+                <maven.javadoc.skip>true</maven.javadoc.skip>
+                <source.skip>true</source.skip>
+                <skipTests>true</skipTests>
+                <checkstyle.skip>true</checkstyle.skip>
+                <skip.npm>true</skip.npm>
+            </properties>
+        </profile>
+        <profile>
+            <id>skip-tests</id>
+            <properties>
+                <skipTests>true</skipTests>
+            </properties>
+        </profile>
+        <profile>
+            <id>skip-checkstyle</id>
+            <properties>
+                <checkstyle.skip>true</checkstyle.skip>
+            </properties>
+        </profile>
+    </profiles>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-enforcer-plugin</artifactId>
+                <version>3.0.0-M2</version>
+                <executions>
+                    <execution>
+                        <id>enforce-maven</id>
+                        <goals>
+                            <goal>enforce</goal>
+                        </goals>
+                        <configuration>
+                            <rules>
+                                <requireMavenVersion>
+                                    <version>3.3</version>
+                                </requireMavenVersion>
+                            </rules>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <!-- This plugin will set properties values using dependency information -->
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>properties</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.3</version>
+                <configuration>
+                    <source>${maven.compiler.source}</source>
+                    <target>${maven.compiler.target}</target>
+
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>2.19.1</version>
+                <configuration>
+                    <excludes>
+                        <exclude>password.pwm.manual.*</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+                <version>2.4</version>
+                <executions>
+                    <execution>
+                        <id>attach-sources</id>
+                        <goals>
+                            <goal>jar-no-fork</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <version>2.10.3</version>
+                <executions>
+                    <execution>
+                        <id>attach-javadocs</id>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <version>2.6</version>
+                <configuration>
+                    <archiveClasses>true</archiveClasses>
+                    <packagingExcludes>WEB-INF/classes</packagingExcludes>
+                    <archive>
+                        <manifestEntries>
+                            <Implementation-Title>${project.name}</Implementation-Title>
+                            <Implementation-Version>${project.version}</Implementation-Version>
+                            <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
+                            <Implementation-URL>${project.organization.url}</Implementation-URL>
+                            <Implementation-Build>${build.number}</Implementation-Build>
+                            <Implementation-Revision>${build.revision}</Implementation-Revision>
+                            <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
+                        </manifestEntries>
+                    </archive>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-checkstyle-plugin</artifactId>
+                <version>3.0.0</version>
+                <dependencies>
+                    <dependency>
+                        <groupId>com.puppycrawl.tools</groupId>
+                        <artifactId>checkstyle</artifactId>
+                        <version>8.10.1</version>
+                    </dependency>
+                </dependencies>
+                <executions>
+                    <execution>
+                        <id>validate</id>
+                        <phase>validate</phase>
+                        <configuration>
+                            <encoding>UTF-8</encoding>
+                            <consoleOutput>true</consoleOutput>
+                            <includeTestResources>false</includeTestResources>
+                            <failsOnError>true</failsOnError>
+                            <configLocation>checkstyle.xml</configLocation>
+                        </configuration>
+                        <goals>
+                            <goal>check</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <artifactId>maven-resources-plugin</artifactId>
+                <version>2.7</version>
+                <executions>
+                    <execution>
+                        <id>copy-resources</id>
+                        <phase>validate</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.outputDirectory}/src</outputDirectory>
+                            <resources>
+                                <resource><directory>src/main/java</directory></resource>
+                                <resource><directory>src/main/resources</directory></resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <artifactId>maven-clean-plugin</artifactId>
+                <version>3.0.0</version>
+            </plugin>
+        </plugins>
+    </build>
+
+    <reporting>
+    </reporting>
+
+    <dependencies>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>pwm</artifactId>
+            <version>${project.version}</version>
+            <type>jar</type>
+        </dependency>
+
+        <!-- dev tool -->
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>1.18.0</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- container dependencies -->
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <version>4.0.1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet.jsp</groupId>
+            <artifactId>jsp-api</artifactId>
+            <version>2.2.1-b03</version>
+            <scope>provided</scope>
+        </dependency>
+        <!-- / container dependencies -->
+
+        <dependency>
+            <groupId>commons-net</groupId>
+            <artifactId>commons-net</artifactId>
+            <version>3.6</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-csv</artifactId>
+            <version>1.5</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.7</version>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.mail</groupId>
+            <artifactId>javax.mail</artifactId>
+            <version>1.6.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.5.5</version>
+        </dependency>
+        <dependency>
+            <groupId>log4j</groupId>
+            <artifactId>log4j</artifactId>
+            <version>1.2.17</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.axis</groupId>
+            <artifactId>axis</artifactId>
+            <version>1.4</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jdom</groupId>
+            <artifactId>jdom2</artifactId>
+            <version>2.0.6</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+            <version>2.8.5</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jetbrains.xodus</groupId>
+            <artifactId>xodus-environment</artifactId>
+            <version>1.2.3</version>
+        </dependency>
+        <dependency>
+            <groupId>org.webjars</groupId>
+            <artifactId>webjars-locator-core</artifactId>
+            <version>0.35</version>
+        </dependency>
+
+
+
+    </dependencies>
+
+    <repositories>
+        <repository>
+            <id>central</id>
+            <url>https://repo1.maven.org/maven2</url>
+            <snapshots>
+                <enabled>false</enabled>
+            </snapshots>
+        </repository>
+    </repositories>
+    <pluginRepositories>
+        <pluginRepository>
+            <id>central</id>
+            <url>https://repo1.maven.org/maven2</url>
+            <snapshots>
+                <enabled>false</enabled>
+            </snapshots>
+        </pluginRepository>
+    </pluginRepositories>
+</project>

+ 55 - 0
data-service/src/main/java/password/pwm/receiver/ContextManager.java

@@ -0,0 +1,55 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+package password.pwm.receiver;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+import javax.servlet.annotation.WebListener;
+
+@WebListener
+public class ContextManager implements ServletContextListener {
+    private static final String CONTEXT_ATTR = "contextManager";
+    private PwmReceiverApp app;
+
+    @Override
+    public void contextInitialized(final ServletContextEvent sce) {
+        app = new PwmReceiverApp();
+        sce.getServletContext().setAttribute(CONTEXT_ATTR, this);
+    }
+
+    @Override
+    public void contextDestroyed(final ServletContextEvent sce) {
+        app.close();
+        app = null;
+    }
+
+    public PwmReceiverApp getApp() {
+        return app;
+    }
+
+    public static ContextManager getContextManager(final ServletContext serverContext) {
+        return (ContextManager)serverContext.getAttribute(CONTEXT_ATTR);
+    }
+}

+ 35 - 0
data-service/src/main/java/password/pwm/receiver/CsvDownloadServlet.java

@@ -0,0 +1,35 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+package password.pwm.receiver;
+
+import javax.servlet.annotation.WebServlet;
+
+@WebServlet(
+        name="TelemetryViewer",
+        urlPatterns={
+                "/csv",
+        }
+)
+public class CsvDownloadServlet {
+}

+ 162 - 0
data-service/src/main/java/password/pwm/receiver/FtpDataIngestor.java

@@ -0,0 +1,162 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+package password.pwm.receiver;
+
+import org.apache.commons.net.ftp.FTPClient;
+import org.apache.commons.net.ftp.FTPFile;
+import org.apache.commons.net.ftp.FTPSClient;
+import password.pwm.PwmConstants;
+import password.pwm.bean.TelemetryPublishBean;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+class FtpDataIngestor {
+
+    private static final PwmReceiverLogger LOGGER = PwmReceiverLogger.forClass(FtpDataIngestor.class);
+
+    private final Settings settings;
+    private final PwmReceiverApp app;
+
+    FtpDataIngestor(final PwmReceiverApp app, final Settings telemetrySettings) {
+        this.app = app;
+        this.settings = telemetrySettings;
+    }
+
+    void readData(final Storage storage) {
+        app.getStatus().setLastFtpStatus("beginning ftp ingestion");
+        LOGGER.debug( "beginning ftp ingestion" );
+        app.getStatus().setLastFtpIngest(Instant.now());
+        try {
+            final FTPClient ftpClient = getFtpClient();
+            final List<String> files = getFiles(ftpClient);
+            LOGGER.debug( "beginning ftp ingestion, listed " + files.size() + " files from server" );
+            for (final String fileName : files) {
+                if (fileName != null && fileName.endsWith(".zip")) {
+                    app.getStatus().setLastFtpIngest(Instant.now());
+                    app.getStatus().setLastFtpStatus("reading file " + fileName);
+                    LOGGER.debug( "read file " + fileName );
+                    try {
+                        readFile( ftpClient, fileName, storage );
+                    } catch (Exception e) {
+                        app.getStatus().setLastFtpIngest(Instant.now());
+                        final String msg = "error while reading ftp file '" + fileName + "': " + e.getMessage();
+                        app.getStatus().setLastFtpStatus(msg);
+                        LOGGER.error( msg );
+                    }
+                } else {
+                    LOGGER.info("skipping ftp file " + fileName);
+                }
+            }
+            ftpClient.disconnect();
+            LOGGER.info("completed ftp ingestion");
+            app.getStatus().setLastFtpStatus("completed successfully");
+            app.getStatus().setLastFtpIngest(Instant.now());
+            app.getStatus().setLastFtpFilesRead( files.size() );
+        } catch (Exception e) {
+            app.getStatus().setLastFtpIngest(Instant.now());
+            app.getStatus().setLastFtpStatus("error during ftp scan: " + e.getMessage());
+        }
+    }
+
+    private void readFile(final FTPClient ftpClient, final String fileName, final Storage storage) throws Exception {
+        final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+        ftpClient.retrieveFile(fileName, byteArrayOutputStream);
+        final ByteArrayInputStream inputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
+        readZippedByteStream(inputStream, fileName, storage);
+    }
+
+    private void readZippedByteStream(final InputStream inputStream, final String fileName, final Storage storage) throws Exception {
+        try {
+            final ZipInputStream zipInputStream = new ZipInputStream(inputStream);
+            final ZipEntry zipEntry = zipInputStream.getNextEntry();
+            final String zipEntryName = zipEntry.getName();
+            if (zipEntryName != null && zipEntryName.endsWith(".json")) {
+                LOGGER.info("reading ftp file " + fileName + ":" + zipEntryName);
+                final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+                final byte[] buffer = new byte[1024];
+                int len;
+                while ((len = zipInputStream.read(buffer)) > 0) {
+                    byteArrayOutputStream.write(buffer, 0, len);
+                }
+                final String resultsStr = byteArrayOutputStream.toString(PwmConstants.DEFAULT_CHARSET.name());
+                final TelemetryPublishBean bean = JsonUtil.deserialize(resultsStr, TelemetryPublishBean.class);
+                storage.store(bean);
+            }
+        } catch (Exception e) {
+            final String msg = "error reading ftp file '" + fileName + "', error: " + e.getMessage();
+            LOGGER.info(msg);
+            throw new Exception(e);
+        }
+    }
+
+    private List<String> getFiles(final FTPClient ftpClient) throws IOException {
+        final String pathname = settings.getSetting( Settings.Setting.ftpReadPath );
+        final FTPFile[] files = ftpClient.listFiles(pathname);
+        final List<String> returnFiles = new ArrayList<>();
+        for (final FTPFile ftpFile : files) {
+            final String name = ftpFile.getName();
+            final String fullPath = pathname + "/" + name;
+            returnFiles.add(fullPath);
+        }
+
+        return Collections.unmodifiableList(returnFiles);
+    }
+
+    private FTPClient getFtpClient() throws IOException {
+        final FTPClient ftpClient;
+        final Settings.FtpMode ftpMode = Settings.FtpMode.valueOf( settings.getSetting( Settings.Setting.ftpMode ) );
+        switch ( ftpMode ) {
+            case ftp:
+                ftpClient = new FTPClient();
+                break;
+
+            case ftps:
+                ftpClient = new FTPSClient();
+                break;
+
+            default:
+                throw new IllegalArgumentException("unexpected ftp mode");
+        }
+
+        ftpClient.connect( settings.getSetting( Settings.Setting.ftpSite ));
+        LOGGER.info("ftp connect complete");
+        if (!StringUtil.isEmpty(settings.getSetting(Settings.Setting.ftpUser)) && !StringUtil.isEmpty(settings.getSetting( Settings.Setting.ftpPassword ))) {
+            final boolean loggedInSuccess = ftpClient.login( settings.getSetting(Settings.Setting.ftpUser), settings.getSetting( Settings.Setting.ftpPassword ));
+            LOGGER.info("ftp login complete, success=" + loggedInSuccess);
+        }
+        ftpClient.enterLocalPassiveMode();
+        return ftpClient;
+    }
+}

+ 41 - 0
data-service/src/main/java/password/pwm/receiver/Logger.java

@@ -0,0 +1,41 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+package password.pwm.receiver;
+
+public class Logger {
+
+    private final String name;
+
+    private Logger(final String name) {
+        this.name = name;
+    }
+
+    public static Logger createLogger(final String name) {
+        return new Logger(name);
+    }
+
+    public void info(final CharSequence input) {
+        System.out.println(input);
+    }
+}

+ 96 - 0
data-service/src/main/java/password/pwm/receiver/PwmReceiverApp.java

@@ -0,0 +1,96 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+package password.pwm.receiver;
+
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
+
+import java.io.IOException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class PwmReceiverApp {
+    private static final PwmReceiverLogger LOGGER = PwmReceiverLogger.forClass( PwmReceiverApp.class );
+    private static final String ENV_NAME = "DATA_SERVICE_PROPS";
+
+    private Storage storage;
+    private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
+    private Settings settings;
+    private Status status = new Status();
+
+    public PwmReceiverApp() {
+        final String propsFile = System.getenv(ENV_NAME);
+        if (StringUtil.isEmpty(propsFile)) {
+            final String errorMsg = "Missing environment variable '" + ENV_NAME + "', can't load configuration";
+            status.setErrorState( errorMsg );
+            LOGGER.error( errorMsg );
+            return;
+        }
+
+        try {
+            settings = Settings.readFromFile(propsFile);
+        } catch (IOException e) {
+            final String errorMsg = "can't read configuration: " + JavaHelper.readHostileExceptionMessage(e);
+            status.setErrorState( errorMsg );
+            LOGGER.error( errorMsg, e );
+            return;
+        }
+
+        try {
+            storage = new Storage(settings);
+        } catch (Exception e) {
+            final String errorMsg = "can't start storage system: " + JavaHelper.readHostileExceptionMessage(e);
+            status.setErrorState( errorMsg );
+            LOGGER.error( errorMsg, e );
+            return;
+        }
+
+        if (settings.getSetting( Settings.Setting.ftpSite ) != null && !settings.getSetting( Settings.Setting.ftpSite ).isEmpty()) {
+            final Runnable ftpThread = () -> {
+                final FtpDataIngestor ftpDataIngestor = new FtpDataIngestor(this, settings);
+                ftpDataIngestor.readData(storage);
+            };
+            scheduledExecutorService.scheduleAtFixedRate(ftpThread, 0, 1, TimeUnit.HOURS);
+        }
+    }
+
+    public Settings getSettings() {
+        return settings;
+    }
+
+    public Storage getStorage() {
+        return storage;
+    }
+
+    void close() {
+        storage.close();
+        scheduledExecutorService.shutdown();
+    }
+
+    public Status getStatus() {
+        return status;
+    }
+
+}

+ 40 - 0
data-service/src/main/java/password/pwm/receiver/PwmReceiverLogger.java

@@ -0,0 +1,40 @@
+package password.pwm.receiver;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class PwmReceiverLogger
+{
+    private final Class clazz;
+
+    private PwmReceiverLogger( final Class clazz )
+    {
+        this.clazz = clazz;
+    }
+
+    public static PwmReceiverLogger forClass( final Class clazz )
+    {
+        return new PwmReceiverLogger( clazz );
+    }
+
+    public void debug(final CharSequence logMsg) {
+        log( Level.FINE, logMsg, null );
+    }
+
+    public void info(final CharSequence logMsg) {
+        log( Level.INFO, logMsg, null );
+    }
+
+    public void error(final CharSequence logMsg ) {
+        log( Level.SEVERE, logMsg, null );
+    }
+
+    public void error(final CharSequence logMsg, final Throwable throwable ) {
+        log( Level.SEVERE, logMsg, throwable );
+    }
+
+    private void log( final Level level, final CharSequence logMsg, final Throwable throwable ) {
+        final Logger logger = Logger.getLogger(clazz.getName());
+        logger.log( level, logMsg.toString(), throwable );
+    }
+}

+ 95 - 0
data-service/src/main/java/password/pwm/receiver/Settings.java

@@ -0,0 +1,95 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+package password.pwm.receiver;
+
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.TimeDuration;
+
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+
+public class Settings {
+    enum Setting {
+        ftpMode(FtpMode.ftp.name()),
+        ftpSite(null),
+        ftpUser(null),
+        ftpPassword(null),
+        ftpReadPath(null),
+        storagePath(null),
+        maxInstanceSeconds(Long.toString( new TimeDuration(14, TimeUnit.DAYS).getTotalSeconds() ) ),
+
+        ;
+
+        private final String defaultValue;
+
+        Setting( final String defaultValue )
+        {
+            this.defaultValue = defaultValue == null ? "" : defaultValue;
+        }
+
+        private String getDefaultValue( )
+        {
+            return defaultValue;
+        }
+    }
+
+    enum FtpMode {
+        ftp,
+        ftps,
+    }
+
+    private final Map<Setting,String> settings;
+
+    private Settings( final Map<Setting, String> settings )
+    {
+        this.settings = settings;
+    }
+
+    static Settings readFromFile( final String filename) throws IOException {
+        final Properties properties = new Properties();
+        properties.load(new FileReader(filename));
+        final Map<Setting,String> returnMap = new HashMap<>(  );
+        for (final Setting setting : Setting.values() )
+        {
+            final String value = properties.getProperty( setting.name(), setting.getDefaultValue() );
+            returnMap.put( setting, value );
+        }
+        return new Settings( Collections.unmodifiableMap( returnMap ) );
+    }
+
+    public String getSetting( final Setting setting )
+    {
+        return settings.get( setting );
+    }
+
+    public boolean isFtpEnabled() {
+        final String value = settings.get( Setting.ftpSite );
+        return !StringUtil.isEmpty( value );
+    }
+}

+ 38 - 0
data-service/src/main/java/password/pwm/receiver/Status.java

@@ -0,0 +1,38 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+package password.pwm.receiver;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.time.Instant;
+
+@Getter
+@Setter
+public class Status {
+    private String errorState;
+    private String lastFtpStatus;
+    private Instant lastFtpIngest;
+    private int lastFtpFilesRead;
+}

+ 188 - 0
data-service/src/main/java/password/pwm/receiver/Storage.java

@@ -0,0 +1,188 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+package password.pwm.receiver;
+
+import jetbrains.exodus.ArrayByteIterable;
+import jetbrains.exodus.ByteIterable;
+import jetbrains.exodus.bindings.StringBinding;
+import jetbrains.exodus.env.Cursor;
+import jetbrains.exodus.env.Environment;
+import jetbrains.exodus.env.EnvironmentConfig;
+import jetbrains.exodus.env.Environments;
+import jetbrains.exodus.env.Store;
+import jetbrains.exodus.env.StoreConfig;
+import jetbrains.exodus.env.Transaction;
+import password.pwm.bean.TelemetryPublishBean;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Iterator;
+
+public class Storage {
+    private final Environment environment;
+    private Store store;
+
+    public Storage(final Settings settings) throws IOException {
+        final String path = settings.getSetting( Settings.Setting.storagePath );
+        if (path == null) {
+            throw new IOException("data path is not specified!");
+        }
+
+        final File dataPath = new File(path);
+        if (!dataPath.exists()) {
+            throw new IOException("data path '" + dataPath + "' does not exist");
+        }
+
+        final File stoagePath = new File(dataPath.getAbsolutePath() + File.separator + "storage");
+        stoagePath.mkdir();
+
+        final EnvironmentConfig environmentConfig = new EnvironmentConfig();
+        environment = Environments.newInstance(stoagePath.getAbsolutePath(), environmentConfig);
+
+        environment.executeInTransaction(txn -> store
+                = environment.openStore("store1", StoreConfig.WITHOUT_DUPLICATES, txn));
+    }
+
+    public void store(final TelemetryPublishBean bean) {
+        if (bean == null) {
+            return;
+        }
+
+        final String instanceHash = bean.getInstanceHash();
+        if (instanceHash != null) {
+            final TelemetryPublishBean existingBean = get(instanceHash);
+            Instant existingTimestamp = null;
+            if (existingBean != null) {
+                existingTimestamp = existingBean.getTimestamp();
+            }
+            if (existingTimestamp == null || existingTimestamp.isBefore(bean.getTimestamp())) {
+                put(bean);
+            }
+        }
+    }
+
+    public Iterator<TelemetryPublishBean> iterator() {
+        return new InnerIterator();
+    }
+
+    private boolean put(final TelemetryPublishBean value) {
+        return environment.computeInTransaction(transaction -> {
+            final ByteIterable k = StringBinding.stringToEntry(value.getInstanceHash());
+            final ByteIterable v = StringBinding.stringToEntry(JsonUtil.serialize(value));
+            return store.put(transaction,k,v);
+        });
+    }
+
+    private TelemetryPublishBean get(final String hash) {
+        return environment.computeInTransaction(transaction -> {
+            final ByteIterable k = StringBinding.stringToEntry(hash);
+            final ByteIterable v = store.get(transaction,k);
+            if (v != null) {
+                final String string = StringBinding.entryToString(new ArrayByteIterable(v));
+                if (!StringUtil.isEmpty(string)) {
+                    return JsonUtil.deserialize(string, TelemetryPublishBean.class);
+                }
+            }
+            return null;
+        });
+    }
+
+    public void close() {
+        store.getEnvironment().close();
+    }
+
+    public long count() {
+        return environment.computeInTransaction( transaction -> store.count( transaction ) );
+    }
+
+    private class InnerIterator implements AutoCloseable,Iterator {
+        private final Transaction transaction;
+        private final Cursor cursor;
+
+        private boolean closed;
+        private String nextValue = "";
+
+        InnerIterator() {
+            this.transaction = environment.beginReadonlyTransaction();
+            this.cursor = store.openCursor(transaction);
+            doNext();
+        }
+
+        private void doNext() {
+            try {
+                if (closed) {
+                    return;
+                }
+
+                if (!cursor.getNext()) {
+                    close();
+                    return;
+                }
+                final ByteIterable nextKey = cursor.getKey();
+                final String string = StringBinding.entryToString(new ArrayByteIterable(nextKey));
+
+                if (string == null || string.isEmpty()) {
+                    close();
+                    return;
+                }
+                nextValue = string;
+            } catch (Exception e) {
+                e.printStackTrace();
+                throw e;
+            }
+        }
+
+        @Override
+        public void close() {
+            if (closed) {
+                return;
+            }
+            cursor.close();
+            transaction.abort();
+            nextValue = null;
+            closed = true;
+        }
+
+        @Override
+        public boolean hasNext() {
+            return !closed && nextValue != null;
+        }
+
+        @Override
+        public TelemetryPublishBean next() {
+            final String value = nextValue;
+            doNext();
+            return get(value);
+        }
+
+        @Override
+        public void remove() {
+            throw new UnsupportedOperationException("remove not supported");
+        }
+    }
+
+}

+ 194 - 0
data-service/src/main/java/password/pwm/receiver/SummaryBean.java

@@ -0,0 +1,194 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+package password.pwm.receiver;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Value;
+import password.pwm.PwmAboutProperty;
+import password.pwm.bean.TelemetryPublishBean;
+import password.pwm.config.PwmSetting;
+import password.pwm.svc.stats.Statistic;
+import password.pwm.util.java.TimeDuration;
+
+import java.time.Duration;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+
+@Getter
+@Builder
+public class SummaryBean {
+    private int serverCount;
+    private Map<String,SiteSummary> siteSummary;
+    private Map<String,Integer> ldapVendorCount;
+    private Map<String,Integer> appServerCount;
+    private Map<String,Integer> settingCount;
+    private Map<String,Integer> statCount;
+    private Map<String,Integer> osCount;
+    private Map<String,Integer> dbCount;
+    private Map<String,Integer> javaCount;
+    private Map<String,Integer> ssprVersionCount;
+
+    static SummaryBean fromStorage(final Storage storage, final TimeDuration maxAge ) {
+
+        final String naText = "n/a";
+
+        int serverCount = 0;
+        final Map<String,SiteSummary> siteSummaryMap = new TreeMap<>();
+        final Map<String,Integer> ldapVendorCount = new TreeMap<>();
+        final Map<String,Integer> appServerCount = new TreeMap<>();
+        final Map<String,Integer> settingCount = new TreeMap<>();
+        final Map<String,Integer> statCount = new TreeMap<>();
+        final Map<String,Integer> osCount = new TreeMap<>();
+        final Map<String,Integer> dbCount = new TreeMap<>();
+        final Map<String,Integer> javaCount = new TreeMap<>();
+        final Map<String,Integer> ssprVersionCount = new TreeMap<>();
+
+        for (Iterator<TelemetryPublishBean> iterator = storage.iterator(); iterator.hasNext(); ) {
+            final TelemetryPublishBean bean = iterator.next();
+            final TimeDuration age = TimeDuration.fromCurrent( bean.getTimestamp() );
+
+            if (bean.getAbout() != null && age.isShorterThan( maxAge ) ) {
+                serverCount++;
+                final String hashID = bean.getInstanceHash();
+                final String ldapVendor = bean.getLdapVendorName() == null
+                        ? naText
+                        : bean.getLdapVendorName();
+
+                final String dbVendor = dbVendorName(bean);
+
+                final SiteSummary siteSummary = SiteSummary.builder()
+                        .description(bean.getSiteDescription())
+                        .version(bean.getVersionVersion())
+                        .installAge(TimeDuration.fromCurrent(bean.getInstallTime()).asDuration())
+                        .updateAge(TimeDuration.fromCurrent(bean.getTimestamp()).asDuration())
+                        .ldapVendor(ldapVendor)
+                        .osName(bean.getAbout().get(PwmAboutProperty.java_osName.name()))
+                        .osVersion(bean.getAbout().get(PwmAboutProperty.java_osVersion.name()))
+                        .servletName(bean.getAbout().get(PwmAboutProperty.java_appServerInfo.name()))
+                        .dbVendor(dbVendor)
+                        .appliance(Boolean.parseBoolean(bean.getAbout().get(PwmAboutProperty.app_mode_appliance.name())))
+                        .javaVm(javaVmInfo( bean, "n/a" ))
+                        .build();
+
+                siteSummaryMap.put(hashID, siteSummary);
+
+                incrementCounterMap(dbCount, dbVendor);
+
+                incrementCounterMap(ldapVendorCount, ldapVendor);
+
+                incrementCounterMap(appServerCount, siteSummary.getServletName());
+
+                incrementCounterMap(osCount, bean.getAbout().get(PwmAboutProperty.java_osName.name()));
+
+                incrementCounterMap(javaCount, siteSummary.getJavaVm());
+
+                incrementCounterMap(ssprVersionCount, siteSummary.getVersion());
+
+                for (final String settingKey : bean.getConfiguredSettings()) {
+                    final PwmSetting setting = PwmSetting.forKey(settingKey);
+                    if (setting != null) {
+                        final String description = setting.toMenuLocationDebug(null, null);
+                        incrementCounterMap(settingCount, description);
+                    }
+                }
+
+                for (final String statKey : bean.getStatistics().keySet()) {
+                    final Statistic statistic = Statistic.forKey(statKey);
+                    if (statistic != null) {
+                        if (statistic.getType() == Statistic.Type.INCREMENTOR) {
+                            final int count = Integer.parseInt(bean.getStatistics().get(statKey));
+                            incrementCounterMap(statCount, statistic.getLabel(null), count);
+                        }
+                    }
+                }
+            }
+        }
+
+
+        return SummaryBean.builder()
+                .serverCount(serverCount)
+                .siteSummary(siteSummaryMap)
+                .ldapVendorCount(ldapVendorCount)
+                .settingCount(settingCount)
+                .statCount(statCount)
+                .appServerCount(appServerCount)
+                .osCount(osCount)
+                .dbCount(dbCount)
+                .javaCount(javaCount)
+                .ssprVersionCount(ssprVersionCount)
+                .build();
+
+    }
+
+    private static void incrementCounterMap(final Map<String,Integer> map, final String key) {
+        incrementCounterMap(map, key, 1);
+    }
+
+    private static void incrementCounterMap(final Map<String,Integer> map, final String key, final int count) {
+        if (map.containsKey(key)) {
+            map.put(key, map.get(key) + count);
+        } else {
+            map.put(key, count);
+        }
+    }
+
+    private static String dbVendorName(final TelemetryPublishBean bean) {
+        String dbVendor = "n/a";
+        final Map<String,String> aboutMap = bean.getAbout();
+        if (aboutMap.get(PwmAboutProperty.database_databaseProductName.name()) != null) {
+            dbVendor = aboutMap.get(PwmAboutProperty.database_databaseProductName.name());
+
+            if (aboutMap.get(PwmAboutProperty.database_databaseProductVersion.name()) != null) {
+                dbVendor += "/" + aboutMap.get(PwmAboutProperty.database_databaseProductVersion.name());
+            }
+        }
+        return dbVendor;
+    }
+
+    private static String javaVmInfo(final TelemetryPublishBean bean, final String naText ) {
+        return bean.getAbout().getOrDefault( PwmAboutProperty.java_vmName.name(), naText )
+                + " ("
+                + bean.getAbout().getOrDefault( PwmAboutProperty.java_vmVendor.name(), naText )
+                + " ) "
+                + bean.getAbout().getOrDefault( PwmAboutProperty.java_vmVersion.name(), naText );
+    }
+
+    @Value
+    @Builder
+    public static class SiteSummary {
+        private String description;
+        private String version;
+        private Duration installAge;
+        private Duration updateAge;
+        private String ldapVendor;
+        private String osName;
+        private String osVersion;
+        private String servletName;
+        private String dbVendor;
+        private String javaVm;
+        private boolean appliance;
+    }
+}

+ 97 - 0
data-service/src/main/java/password/pwm/receiver/TelemetryRestReceiver.java

@@ -0,0 +1,97 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+package password.pwm.receiver;
+
+import org.apache.commons.io.IOUtils;
+import password.pwm.PwmConstants;
+import password.pwm.bean.TelemetryPublishBean;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.i18n.Message;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.ws.server.RestResultBean;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringWriter;
+
+@WebServlet(
+        name="TelemetryRestReceiver",
+        urlPatterns={
+                "/telemetry",
+        }
+)
+
+public class TelemetryRestReceiver extends HttpServlet {
+    @Override
+    protected void doPost(final HttpServletRequest req, final HttpServletResponse resp)
+            throws ServletException, IOException
+    {
+        try {
+            resp.setHeader("Content","application/json");
+            final String input = readRequestBodyAsString(req, 1024 * 1024);
+            final TelemetryPublishBean telemetryPublishBean = JsonUtil.deserialize(input, TelemetryPublishBean.class);
+            final Storage stoage = ContextManager.getContextManager(this.getServletContext()).getApp().getStorage();
+            stoage.store(telemetryPublishBean);
+            resp.getWriter().print(RestResultBean.forSuccessMessage(null, null, null, Message.Success_Unknown).toJson());
+        } catch (PwmUnrecoverableException e) {
+            resp.getWriter().print(RestResultBean.fromError(e.getErrorInformation()).toJson());
+        } catch (Exception e) {
+            final RestResultBean restResultBean = RestResultBean.fromError(new ErrorInformation(PwmError.ERROR_UNKNOWN, e.getMessage()));
+            resp.getWriter().print(restResultBean.toJson());
+        }
+    }
+
+    private static String readRequestBodyAsString(final HttpServletRequest req, final int maxChars)
+            throws IOException, PwmUnrecoverableException
+    {
+        final StringWriter stringWriter = new StringWriter();
+        final Reader readerStream = new InputStreamReader(
+                req.getInputStream(),
+                PwmConstants.DEFAULT_CHARSET
+        );
+
+        try {
+            IOUtils.copy(readerStream, stringWriter);
+        } catch (Exception e) {
+            final String errorMsg = "error reading request body stream: " + e.getMessage();
+            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNKNOWN,errorMsg));
+        } finally {
+            IOUtils.closeQuietly(readerStream);
+        }
+
+        final String stringValue = stringWriter.toString();
+        if (stringValue.length() > maxChars) {
+            throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_UNKNOWN,"input request body is to big, size=" + stringValue.length() + ", max=" + maxChars));
+        }
+        return stringValue;
+    }
+}

+ 71 - 0
data-service/src/main/java/password/pwm/receiver/TelemetryViewerServlet.java

@@ -0,0 +1,71 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2017 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+package password.pwm.receiver;
+
+import password.pwm.util.java.StringUtil;
+import password.pwm.util.java.TimeDuration;
+
+import javax.servlet.ServletException;
+import javax.servlet.annotation.WebServlet;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+@WebServlet(
+        name="TelemetryViewer",
+        urlPatterns={
+                "/viewer",
+        }
+)
+public class TelemetryViewerServlet extends HttpServlet {
+    private static final String PARAM_DAYS = "days";
+
+    public static String SUMMARY_ATTR = "SummaryBean";
+
+    @Override
+    protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException
+    {
+        final String daysString = req.getParameter( PARAM_DAYS );
+        final int days = StringUtil.isEmpty( daysString ) ? 30 : Integer.parseInt( daysString );
+        final ContextManager contextManager = ContextManager.getContextManager(req.getServletContext());
+        final PwmReceiverApp app = contextManager.getApp();
+
+        {
+            final String errorState = app.getStatus().getErrorState();
+            if (!StringUtil.isEmpty(errorState)) {
+                resp.sendError(500, errorState);
+                final String htmlBody = "<html>Error: " + errorState + "</html>";
+                resp.getWriter().print(htmlBody);
+                return;
+            }
+        }
+
+        final Storage storage = app.getStorage();
+        final SummaryBean summaryBean = SummaryBean.fromStorage(storage, new TimeDuration(days, TimeUnit.DAYS ) );
+        req.setAttribute(SUMMARY_ATTR, summaryBean);
+        req.getServletContext().getRequestDispatcher("/WEB-INF/jsp/telemetry-viewer.jsp").forward(req,resp);
+    }
+}

+ 0 - 0
data-service/src/main/resources/password/pwm/receiver/package-info.java


+ 28 - 0
data-service/src/main/webapp/META-INF/context.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  ~
+  -->
+
+<Context tldValidation="false" unloadDelay="30000" useHttpOnly="true">
+
+</Context>

+ 207 - 0
data-service/src/main/webapp/WEB-INF/jsp/telemetry-viewer.jsp

@@ -0,0 +1,207 @@
+<%@ page import="password.pwm.config.PwmSetting" %>
+<%@ page import="password.pwm.receiver.SummaryBean" %>
+<%@ page import="password.pwm.receiver.TelemetryViewerServlet" %>
+<%@ page import="org.joda.time.DateTime" %>
+<%@ page import="java.time.format.DateTimeFormatter" %>
+<%@ page import="java.time.Instant" %>
+<%@ page import="java.time.LocalDateTime" %>
+<%@ page import="java.time.format.FormatStyle" %>
+<%@ page import="password.pwm.receiver.PwmReceiverApp" %>
+<%@ page import="password.pwm.receiver.ContextManager" %>
+<%--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  ~
+  --%>
+
+<!DOCTYPE html>
+<%@ page contentType="text/html" %>
+<% SummaryBean summaryBean = (SummaryBean)request.getAttribute(TelemetryViewerServlet.SUMMARY_ATTR); %>
+<% PwmReceiverApp app = ContextManager.getContextManager(request.getServletContext()).getApp(); %>
+<html>
+<head>
+    <title>Telemetry Data</title>
+</head>
+<body>
+<div>
+    Current Time: <%=Instant.now().toString()%>
+    <br/>
+    <% if (app.getSettings().isFtpEnabled()) {%>
+    <% Instant lastIngest = app.getStatus().getLastFtpIngest(); %>
+    Last FTP Ingestion: <%= lastIngest == null ? "n/a" : lastIngest.toString()%>
+    <br/>
+    Last FTP Status: <%= app.getStatus().getLastFtpStatus()%>
+    <br/>
+    FTP Files On Server: <%= app.getStatus().getLastFtpFilesRead()%>
+    <br/>
+    <% } %>
+    Servers Registered: <%= app.getStorage().count() %>
+    <br/>
+    Servers Shown: <%= summaryBean.getServerCount() %>
+    <br/>
+    <br/>
+
+    <form method="get">
+        <label>Servers that have sent data in last number of days
+            <input type="number" name="days" id="days" value="30" max="3650" min="1">
+        </label>
+        <button type="submit">Update</button>
+    </form>
+
+    <h2>Versions</h2>
+    <table border="1">
+        <tr>
+            <td><b>Version</b></td>
+            <td><b>Count</b></td>
+        </tr>
+        <% for (final String version : summaryBean.getSsprVersionCount().keySet()) { %>
+        <tr>
+            <td><%=version%></td>
+            <td><%=summaryBean.getSsprVersionCount().get(version)%></td>
+        </tr>
+        <% } %>
+    </table>
+    <h2>LDAP Vendors</h2>
+    <table border="1">
+        <tr>
+            <td><b>Ldap</b></td>
+            <td><b>Count</b></td>
+        </tr>
+        <% for (final String ldapVendor : summaryBean.getLdapVendorCount().keySet()) { %>
+        <tr>
+            <td><%=ldapVendor%></td>
+            <td><%=summaryBean.getLdapVendorCount().get(ldapVendor)%></td>
+        </tr>
+        <% } %>
+    </table>
+    <h2>App Servers</h2>
+    <table border="1">
+        <tr>
+            <td><b>App Server Info</b></td>
+            <td><b>Count</b></td>
+        </tr>
+        <% for (final String appServerInfo : summaryBean.getAppServerCount().keySet()) { %>
+        <tr>
+            <td><%=appServerInfo%></td>
+            <td><%=summaryBean.getAppServerCount().get(appServerInfo)%></td>
+        </tr>
+        <% } %>
+    </table>
+    <h2>OS Vendors</h2>
+    <table border="1">
+        <tr>
+            <td><b>OS Vendor</b></td>
+            <td><b>Count</b></td>
+        </tr>
+        <% for (final String osName : summaryBean.getOsCount().keySet()) { %>
+        <tr>
+            <td><%=osName%></td>
+            <td><%=summaryBean.getOsCount().get(osName)%></td>
+        </tr>
+        <% } %>
+    </table>
+    <h2>DB Vendors</h2>
+    <table border="1">
+        <tr>
+            <td><b>DB Vendor</b></td>
+            <td><b>Count</b></td>
+        </tr>
+        <% for (final String dbName : summaryBean.getDbCount().keySet()) { %>
+        <tr>
+            <td><%=dbName%></td>
+            <td><%=summaryBean.getDbCount().get(dbName)%></td>
+        </tr>
+        <% } %>
+    </table>
+    <h2>Java VMs</h2>
+    <table border="1">
+        <tr>
+            <td><b>Java VM</b></td>
+            <td><b>Count</b></td>
+        </tr>
+        <% for (final String javaName : summaryBean.getJavaCount().keySet()) { %>
+        <tr>
+            <td><%=javaName%></td>
+            <td><%=summaryBean.getJavaCount().get(javaName)%></td>
+        </tr>
+        <% } %>
+    </table>
+    <h2>Settings</h2>
+    <table border="1">
+        <tr>
+            <td><b>Setting</b></td>
+            <td><b>Count</b></td>
+        </tr>
+        <% for (final String setting: summaryBean.getSettingCount().keySet()) { %>
+        <tr>
+            <td><%=setting%></td>
+            <td><%=summaryBean.getSettingCount().get(setting)%></td>
+        </tr>
+        <% } %>
+    </table>
+    <h2>Statistics</h2>
+    <table border="1">
+        <tr>
+            <td><b>Statistic</b></td>
+            <td><b>Count</b></td>
+        </tr>
+        <% for (final String statistic: summaryBean.getStatCount().keySet()) { %>
+        <tr>
+            <td><%=statistic%></td>
+            <td><%=summaryBean.getStatCount().get(statistic)%></td>
+        </tr>
+        <% } %>
+    </table>
+    <br/>
+    <h2>Summary Data</h2>
+    <table border="1">
+        <tr>
+            <td><b>SiteHash</b></td>
+            <td><b>Description</b></td>
+            <td><b>Version</b></td>
+            <td><b>Installed</b></td>
+            <td><b>Last Updated</b></td>
+            <td><b>Ldap</b></td>
+            <td><b>OS Name</b></td>
+            <td><b>OS Version</b></td>
+            <td><b>Servlet Name</b></td>
+            <td><b>DB Vendor</b></td>
+            <td><b>Appliance</b></td>
+        </tr>
+        <% for (final String hashID : summaryBean.getSiteSummary().keySet()) { %>
+        <% SummaryBean.SiteSummary siteSummary = summaryBean.getSiteSummary().get(hashID); %>
+        <tr>
+            <td style="max-width: 500px; overflow: auto"><%=hashID%></td>
+            <td><%=siteSummary.getDescription()%></td>
+            <td><%=siteSummary.getVersion()%></td>
+            <td><%=siteSummary.getInstallAge()%></td>
+            <td><%=siteSummary.getUpdateAge()%></td>
+            <td><%=siteSummary.getLdapVendor()%></td>
+            <td><%=siteSummary.getOsName()%></td>
+            <td><%=siteSummary.getOsVersion()%></td>
+            <td><%=siteSummary.getServletName()%></td>
+            <td><%=siteSummary.getDbVendor()%></td>
+            <td><%=siteSummary.isAppliance()%></td>
+        </tr>
+        <% } %>
+    </table>
+</div>
+</body>
+</html>

+ 58 - 0
data-service/src/main/webapp/WEB-INF/web.xml

@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  ~
+  -->
+
+<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://java.sun.com/xml/ns/javaee"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+         id="PWM" version="3.0">
+    <display-name>PWM Receiver</display-name>
+    <!-- <distributable/> Clustering/Session replication is not supported -->
+    <description>Password Management Servlet</description>
+    <context-param>
+        <description>
+            Explicit location of application path working directory or the literal value "unspecified".  See the environment documentation at /public/reference/environment.jsp for more information.
+        </description>
+        <param-name>applicationPath</param-name>
+        <param-value>unspecified</param-value>
+    </context-param>
+    <welcome-file-list>
+        <welcome-file>index.jsp</welcome-file>
+    </welcome-file-list>
+    <session-config>
+        <session-timeout>5</session-timeout>
+        <cookie-config>
+            <http-only>true</http-only>
+        </cookie-config>
+    </session-config>
+    <jsp-config>
+        <jsp-property-group>
+            <url-pattern>*.jsp</url-pattern>
+            <trim-directive-whitespaces>true</trim-directive-whitespaces>
+        </jsp-property-group>
+        <jsp-property-group>
+            <url-pattern>*.jsp</url-pattern>
+            <page-encoding>UTF-8</page-encoding>
+        </jsp-property-group>
+    </jsp-config>
+</web-app>

+ 32 - 0
data-service/src/main/webapp/index.jsp

@@ -0,0 +1,32 @@
+<%--
+  ~ Password Management Servlets (PWM)
+  ~ http://www.pwm-project.org
+  ~
+  ~ Copyright (c) 2006-2009 Novell, Inc.
+  ~ Copyright (c) 2009-2017 The PWM Project
+  ~
+  ~ This program is free software; you can redistribute it and/or modify
+  ~ it under the terms of the GNU General Public License as published by
+  ~ the Free Software Foundation; either version 2 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  ~ GNU General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU General Public License
+  ~ along with this program; if not, write to the Free Software
+  ~ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+  ~
+  --%>
+
+<!DOCTYPE html>
+<%@ page language="java" session="true" isThreadSafe="true"
+         contentType="text/html" %>
+<html>
+<body>
+<div>html-pwm-receiver
+</div>
+</body>
+</html>

+ 94 - 0
docker/pom.xml

@@ -0,0 +1,94 @@
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <parent>
+        <groupId>org.pwm-project</groupId>
+        <artifactId>pwm-parent</artifactId>
+        <version>1.8.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <packaging>pom</packaging>
+
+    <artifactId>pwm-docker</artifactId>
+
+    <name>PWM Password Self Service: Docker Image</name>
+
+    <properties>
+        <skipDocker>false</skipDocker>
+    </properties>
+
+    <profiles>
+        <profile>
+            <id>skip-docker</id>
+            <properties>
+                <skipDocker>true</skipDocker>
+            </properties>
+            <activation>
+                <activeByDefault>true</activeByDefault>
+            </activation>
+        </profile>
+    </profiles>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>io.fabric8</groupId>
+                <artifactId>docker-maven-plugin</artifactId>
+                <version>0.26.0</version>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>build</goal>
+                            <goal>save</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <skip>${skipDocker}</skip>
+                    <saveFile>${project.build.directory}/pwm-server-docker-${project.version}.tar.gz</saveFile>
+                    <images>
+                        <image>
+                            <name>pwm-server:${project.version}</name>
+                            <build>
+                                <tags>
+                                    <tag>pwm-server</tag>
+                                </tags>
+                                <dockerFileDir>${project.basedir}/src/main/docker</dockerFileDir>
+                                <assembly>
+                                    <name>onejar</name>
+                                    <inline>
+                                        <files>
+                                            <file>
+                                                <source>..${file.separator}onejar${file.separator}target${file.separator}pwm-onejar-${project.version}.jar</source>
+                                                <destName>pwm-onejar.jar</destName>
+                                            </file>
+                                        </files>
+                                    </inline>
+                                </assembly>
+                            </build>
+                        </image>
+                    </images>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+
+        <!--
+        This is included via the assembly plugin descriptor, so its not really required here but keeps
+        the module build order correct.
+        -->
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>pwm-onejar</artifactId>
+            <version>${project.version}</version>
+            <type>jar</type>
+            <scope>provided</scope>
+        </dependency>
+
+    </dependencies>
+</project>

+ 11 - 0
docker/readme.txt

@@ -0,0 +1,11 @@
+Docker image usage notes:
+
+--Load docker image from file
+docker load --input=pwm-docker-image.tar.gz
+
+--Create docker container and run--
+docker run -d --name <container name> -p 8443:8443 pwm
+
+This will expose the https port to 8443.  You can also manage the exposed configuration volume of /config if you want to preserve
+the /config directory when you destroy/create the container in the future.  The docker image will place all of it's configuration
+and runtime data in the /config volume.

+ 23 - 0
docker/src/main/docker/Dockerfile

@@ -0,0 +1,23 @@
+FROM adoptopenjdk/openjdk10
+MAINTAINER Jason Rivard
+
+# setup environment
+RUN mkdir /config ; mkdir /appliance
+ENV PWM_APPLICATIONPATH /config
+ENV PWM_APPLICATIONFLAGS Appliance,ManageHttps
+
+VOLUME /config
+EXPOSE 8443
+
+# copy war
+COPY onejar/pwm-onejar.jar /appliance/pwm-onejar.jar
+
+# copy scripts
+COPY startup.sh /appliance/startup.sh
+RUN chmod a+x /appliance/startup.sh;
+
+VOLUME /config
+EXPOSE 8443
+
+WORKDIR /appliance
+CMD ./startup.sh

+ 3 - 0
docker/src/main/docker/java.vmoptions

@@ -0,0 +1,3 @@
+-server
+-Xmx1g
+-Xms1g

+ 9 - 0
docker/src/main/docker/startup.sh

@@ -0,0 +1,9 @@
+#!/usr/bin/env bash
+#Docker startup script
+
+#start onejar
+cd /appliance
+
+JAVA_OPTS="$(sed 's/./&/' java.vmoptions | tr '\n' ' ')"
+
+java $JAVA_OPTS -jar pwm-onejar.jar -applicationPath /config

+ 0 - 18
onejar/.gitignore

@@ -1,18 +0,0 @@
-# PWM Project gitignore files
-#
-# Maven Output
-/target
-
-# Eclipse Project Files
-/.project
-/.settings
-/.classpath
-
-# IntelliJ Project Files
-.idea/
-/*.iml
-
-# OS folder info
-.directory
-.DS_Store
-.DS_Store?

+ 3 - 3
onejar/pom.xml

@@ -16,10 +16,9 @@
     <name>PWM Password Self Service: Executable Jar</name>
 
     <properties>
-        <tomcat.version>9.0.7</tomcat.version>
+        <tomcat.version>9.0.10</tomcat.version>
         <maven.compiler.source>1.8</maven.compiler.source>
         <maven.compiler.target>1.8</maven.compiler.target>
-        <warArtifactID>pwm-${project.version}.war</warArtifactID>
     </properties>
 
     <build>
@@ -82,12 +81,13 @@
                     </descriptors>
                     <archive>
                         <manifestEntries>
-                            <Main-Class>password.pwm.TomcatOneJarMain</Main-Class>
+                            <Main-Class>password.pwm.onejar.TomcatOneJarMain</Main-Class>
                             <Implementation-Title>${project.name}</Implementation-Title>
                             <Implementation-Version>${project.version}</Implementation-Version>
                             <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
                             <Implementation-URL>${project.organization.url}</Implementation-URL>
                             <Implementation-Build>${build.number}</Implementation-Build>
+                            <Implementation-Build-Timestamp>${timestamp.iso}</Implementation-Build-Timestamp>
                             <Implementation-Revision>${build.revision}</Implementation-Revision>
                             <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
                         </manifestEntries>

+ 1 - 1
onejar/src/main/java/password/pwm/Argument.java → onejar/src/main/java/password/pwm/onejar/Argument.java

@@ -20,7 +20,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm;
+package password.pwm.onejar;
 
 import org.apache.commons.cli.Option;
 import org.apache.commons.cli.Options;

+ 1 - 1
onejar/src/main/java/password/pwm/ArgumentParser.java → onejar/src/main/java/password/pwm/onejar/ArgumentParser.java

@@ -20,7 +20,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm;
+package password.pwm.onejar;
 
 import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.DefaultParser;

+ 1 - 1
onejar/src/main/java/password/pwm/ArgumentParserException.java → onejar/src/main/java/password/pwm/onejar/ArgumentParserException.java

@@ -20,7 +20,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm;
+package password.pwm.onejar;
 
 public class ArgumentParserException extends Exception
 {

+ 1 - 1
onejar/src/main/java/password/pwm/Resource.java → onejar/src/main/java/password/pwm/onejar/Resource.java

@@ -20,7 +20,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm;
+package password.pwm.onejar;
 
 import java.util.ResourceBundle;
 

+ 1 - 1
onejar/src/main/java/password/pwm/TomcatConfig.java → onejar/src/main/java/password/pwm/onejar/TomcatConfig.java

@@ -20,7 +20,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm;
+package password.pwm.onejar;
 
 import java.io.File;
 import java.io.InputStream;

+ 1 - 1
onejar/src/main/java/password/pwm/TomcatOneJarException.java → onejar/src/main/java/password/pwm/onejar/TomcatOneJarException.java

@@ -20,7 +20,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm;
+package password.pwm.onejar;
 
 public class TomcatOneJarException extends Exception
 {

+ 1 - 1
onejar/src/main/java/password/pwm/TomcatOneJarMain.java → onejar/src/main/java/password/pwm/onejar/TomcatOneJarMain.java

@@ -20,7 +20,7 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
-package password.pwm;
+package password.pwm.onejar;
 
 import org.apache.catalina.LifecycleException;
 import org.apache.catalina.connector.Connector;

+ 4 - 4
client/webpack.test.js → onejar/src/main/java/password/pwm/onejar/WebServer.java

@@ -20,8 +20,8 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
+package password.pwm.onejar;
 
-var commonConfig = require('./webpack.common.js');
-var webpackMerge = require('webpack-merge');
-
-module.exports = webpackMerge(commonConfig, {});
+public interface WebServer
+{
+}

+ 0 - 0
onejar/src/main/resources/password/pwm/Resource.properties → onejar/src/main/resources/password/pwm/onejar/Resource.properties


+ 3 - 0
pom.xml

@@ -23,16 +23,19 @@
     </organization>
 
     <properties>
+        <warArtifactID>pwm-${project.version}.war</warArtifactID>
         <build.number>0</build.number>  <!-- default in case not set on command line -->
         <build.revision>0</build.revision>  <!-- default in case not set on command line -->
         <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
         <pwm.minimum.maven.version>3.2</pwm.minimum.maven.version>
+        <timestamp.iso>${maven.build.timestamp}</timestamp.iso>
     </properties>
 
     <modules>
         <module>client</module>
         <module>server</module>
         <module>onejar</module>
+        <module>docker</module>
     </modules>
 
     <build>

+ 0 - 18
server/.gitignore

@@ -1,21 +1,3 @@
-# PWM Project gitignore files
-#
-# Maven Output
-/target
-
-# Eclipse Project Files
-/.project
-/.settings
-/.classpath
-
-# IntelliJ Project Files
-.idea/
-/*.iml
-
-# OS folder info
-.directory
-.DS_Store
-.DS_Store?
 
 
 # Older PWM versions defaulted the applicationPath dir to WEB-INF.  This section ignores them if the applicationPath is set to WEB-INF.

+ 63 - 57
server/pom.xml

@@ -19,7 +19,6 @@
         <maven.compiler.target>1.8</maven.compiler.target>
         <skipTests>false</skipTests>
         <skipSpotbugs>false</skipSpotbugs>
-        <timestamp.iso>${maven.build.timestamp}</timestamp.iso>
     </properties>
 
     <profiles>
@@ -79,15 +78,49 @@
 
     <build>
         <plugins>
+            <!--
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-pmd-plugin</artifactId>
+                <version>3.9.0</version>
+                <executions>
+                    <execution>
+                        <phase>test</phase>
+                        <goals>
+                            <goal>check</goal>
+                            <goal>cpd-check</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <language>jsp</language>
+                    <rulesets>
+                        <ruleset>/category/jsp/bestpractices.xml</ruleset>
+                        <ruleset>/category/jsp/codestyle.xml</ruleset>
+                        <ruleset>/category/jsp/design.xml</ruleset>
+                        <ruleset>/category/jsp/errorprone.xml</ruleset>
+                        <ruleset>/category/jsp/security.xml</ruleset>
+                    </rulesets>
+                    <includes>
+                        <include>**/*.jsp</include>
+                    </includes>
+                    <compileSourceRoots>
+                        <compileSourceRoot>${basedir}/src/main/webapp</compileSourceRoot>
+                    </compileSourceRoots>
+                    <skip>false</skip>
+                </configuration>
+            </plugin>
+            -->
+
             <plugin>
                 <groupId>com.github.spotbugs</groupId>
                 <artifactId>spotbugs-maven-plugin</artifactId>
-                <version>3.1.2</version>
+                <version>3.1.3.1</version>
                 <dependencies>
                     <dependency>
                         <groupId>com.github.spotbugs</groupId>
                         <artifactId>spotbugs</artifactId>
-                        <version>3.1.2</version>
+                        <version>3.1.5</version>
                     </dependency>
                 </dependencies>
                 <configuration>
@@ -167,24 +200,6 @@
                 <configuration>
                     <source>${maven.compiler.source}</source>
                     <target>${maven.compiler.target}</target>
-
-                    <!-- following allows lombok processor to execute on jdk9+ -->
-                    <showDeprecation>true</showDeprecation>
-                    <showWarnings>true</showWarnings>
-                    <fork>true</fork>
-                    <compilerargs>
-                        <arg>-Werror</arg>
-                        <arg>-Xlint:all</arg>
-                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
-                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
-                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
-                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
-                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
-                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
-                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
-                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
-                        <arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
-                    </compilerargs>
                 </configuration>
             </plugin>
             <plugin>
@@ -206,11 +221,15 @@
                     <includePom>true</includePom>
                     <archive>
                         <manifestEntries>
+                            <Implementation-Archive-Name>pwm.source</Implementation-Archive-Name>
                             <Implementation-Title>${project.name}</Implementation-Title>
                             <Implementation-Version>${project.version}</Implementation-Version>
                             <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
                             <Implementation-URL>${project.organization.url}</Implementation-URL>
+                            <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
+                            <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
                             <Implementation-Build>${build.number}</Implementation-Build>
+                            <Implementation-Build-Timestamp>${timestamp.iso}</Implementation-Build-Timestamp>
                             <Implementation-Revision>${build.revision}</Implementation-Revision>
                             <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
                         </manifestEntries>
@@ -224,11 +243,15 @@
                 <configuration>
                     <archive>
                         <manifestEntries>
+                            <Implementation-Archive-Name>pwm.javadoc</Implementation-Archive-Name>
                             <Implementation-Title>${project.name}</Implementation-Title>
                             <Implementation-Version>${project.version}</Implementation-Version>
                             <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
                             <Implementation-URL>${project.organization.url}</Implementation-URL>
+                            <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
+                            <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
                             <Implementation-Build>${build.number}</Implementation-Build>
+                            <Implementation-Build-Timestamp>${timestamp.iso}</Implementation-Build-Timestamp>
                             <Implementation-Revision>${build.revision}</Implementation-Revision>
                             <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
                         </manifestEntries>
@@ -244,11 +267,15 @@
                     <packagingExcludes>WEB-INF/classes</packagingExcludes>
                     <archive>
                         <manifestEntries>
+                            <Implementation-Archive-Name>pwm.war</Implementation-Archive-Name>
                             <Implementation-Title>${project.name}</Implementation-Title>
                             <Implementation-Version>${project.version}</Implementation-Version>
                             <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
                             <Implementation-URL>${project.organization.url}</Implementation-URL>
+                            <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
+                            <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
                             <Implementation-Build>${build.number}</Implementation-Build>
+                            <Implementation-Build-Timestamp>${timestamp.iso}</Implementation-Build-Timestamp>
                             <Implementation-Revision>${build.revision}</Implementation-Revision>
                             <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
                         </manifestEntries>
@@ -263,7 +290,7 @@
                     <dependency>
                         <groupId>com.puppycrawl.tools</groupId>
                         <artifactId>checkstyle</artifactId>
-                        <version>8.9</version>
+                        <version>8.11</version>
                     </dependency>
                 </dependencies>
                 <executions>
@@ -456,11 +483,15 @@
                 <configuration>
                     <archive>
                         <manifestEntries>
+                            <Implementation-Archive-Name>pwm.jar</Implementation-Archive-Name>
                             <Implementation-Title>${project.name}</Implementation-Title>
                             <Implementation-Version>${project.version}</Implementation-Version>
                             <Implementation-Vendor>${project.organization.name}</Implementation-Vendor>
                             <Implementation-URL>${project.organization.url}</Implementation-URL>
+                            <Implementation-Build-Java-Vendor>${java.vendor}</Implementation-Build-Java-Vendor>
+                            <Implementation-Build-Java-Version>${java.version}</Implementation-Build-Java-Version>
                             <Implementation-Build>${build.number}</Implementation-Build>
+                            <Implementation-Build-Timestamp>${timestamp.iso}</Implementation-Build-Timestamp>
                             <Implementation-Revision>${build.revision}</Implementation-Revision>
                             <Implementation-Version-Display>v${project.version} b${build.number} r${build.revision}</Implementation-Version-Display>
                         </manifestEntries>
@@ -529,7 +560,7 @@
             <plugin> <!-- checks owsp vulnerability database -->
                 <groupId>org.owasp</groupId>
                 <artifactId>dependency-check-maven</artifactId>
-                <version>3.1.1</version>
+                <version>3.1.2</version>
                 <reportSets>
                     <reportSet>
                         <reports>
@@ -546,13 +577,13 @@
         <dependency>
             <groupId>org.projectlombok</groupId>
             <artifactId>lombok</artifactId>
-            <version>1.16.20</version>
+            <version>1.18.0</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>com.github.spotbugs</groupId>
             <artifactId>spotbugs-annotations</artifactId>
-            <version>3.1.2</version>
+            <version>3.1.5</version>
             <scope>provided</scope>
         </dependency>
 
@@ -578,7 +609,7 @@
         <dependency>
             <groupId>com.github.tomakehurst</groupId>
             <artifactId>wiremock</artifactId>
-            <version>2.15.0</version>
+            <version>2.18.0</version>
             <scope>test</scope>
         </dependency>
         <dependency>
@@ -592,7 +623,7 @@
         <dependency>
             <groupId>javax.servlet</groupId>
             <artifactId>javax.servlet-api</artifactId>
-            <version>4.0.0</version>
+            <version>4.0.1</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
@@ -629,7 +660,7 @@
         <dependency>
             <groupId>com.github.ldapchai</groupId>
             <artifactId>ldapchai</artifactId>
-            <version>0.7.1</version>
+            <version>0.7.3</version>
         </dependency>
         <dependency>
             <groupId>commons-net</groupId>
@@ -639,7 +670,7 @@
         <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-text</artifactId>
-            <version>1.3</version>
+            <version>1.4</version>
         </dependency>
         <dependency>
             <groupId>org.apache.commons</groupId>
@@ -734,17 +765,17 @@
         <dependency>
             <groupId>com.google.code.gson</groupId>
             <artifactId>gson</artifactId>
-            <version>2.8.2</version>
+            <version>2.8.5</version>
         </dependency>
         <dependency>
             <groupId>com.blueconic</groupId>
             <artifactId>browscap-java</artifactId>
-            <version>1.2.2</version>
+            <version>1.2.3</version>
         </dependency>
         <dependency>
             <groupId>org.jetbrains.xodus</groupId>
             <artifactId>xodus-environment</artifactId>
-            <version>1.2.2</version>
+            <version>1.2.3</version>
         </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
@@ -822,31 +853,6 @@
             <artifactId>famfamfam-flags</artifactId>
             <version>1.0.0</version>
         </dependency>
-        <dependency>
-            <groupId>org.webjars.bower</groupId>
-            <artifactId>angular</artifactId>
-            <version>1.6.9</version>
-        </dependency>
-        <dependency>
-            <groupId>org.webjars.bower</groupId>
-            <artifactId>angular-aria</artifactId>
-            <version>1.6.9</version>
-        </dependency>
-        <dependency>
-            <groupId>org.webjars.bower</groupId>
-            <artifactId>angular-ui-router</artifactId>
-            <version>1.0.14</version>
-        </dependency>
-        <dependency>
-            <groupId>org.webjars.bower</groupId>
-            <artifactId>angular-translate</artifactId>
-            <version>2.17.0</version>
-        </dependency>
-        <dependency>
-            <groupId>org.webjars.bower</groupId>
-            <artifactId>textAngular</artifactId>
-            <version>1.5.16</version>
-        </dependency>
     </dependencies>
 
     <repositories>

+ 1 - 0
server/src/build/checkstyle-import.xml

@@ -74,6 +74,7 @@
     <allow pkg="javax.net"/>
     <allow pkg="javax.crypto"/>
     <allow pkg="javax.mail"/>
+    <allow class="com.sun.mail.smtp.SMTPSendFailedException"/>
     <allow pkg="org.xeustechnologies"/>
     <allow pkg="net.glxn"/>
     <allow pkg="org.webjars"/>

+ 4 - 0
server/src/build/spotbugs-exclude.xml

@@ -3,4 +3,8 @@
     <Match>
         <Bug pattern="SE_NO_SERIALVERSIONID,IC_INIT_CIRCULARITY,RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE,DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED,SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING"/>
     </Match>
+    <Match>
+        <!-- due to bug https://github.com/spotbugs/spotbugs/issues/493 in spotbugs 3.1.3 -->
+        <Bug pattern="OBL_UNSATISFIED_OBLIGATION"/>
+    </Match>
 </FindBugsFilter>

+ 3 - 0
server/src/main/java/password/pwm/AppProperty.java

@@ -88,6 +88,7 @@ public enum AppProperty
     DB_INIT_HALT_ON_INDEX_CREATE_ERROR              ( "db.init.haltOnIndexCreateError" ),
     DB_SCHEMA_KEY_LENGTH                            ( "db.schema.keyLength" ),
     DOWNLOAD_FILENAME_STATISTICS_CSV                ( "download.filename.statistics.csv" ),
+    DOWNLOAD_FILENAME_SESSIONS_CSV                  ( "download.filename.sessions.csv" ),
     DOWNLOAD_FILENAME_USER_REPORT_SUMMARY_CSV       ( "download.filename.reportSummary.csv" ),
     DOWNLOAD_FILENAME_USER_REPORT_RECORDS_CSV       ( "download.filename.reportRecords.csv" ),
     DOWNLOAD_FILENAME_AUDIT_RECORDS_CSV             ( "download.filename.auditRecords.csv" ),
@@ -196,6 +197,7 @@ public enum AppProperty
     LDAP_CHAI_SETTINGS                              ( "ldap.chaiSettings" ),
     LDAP_PROXY_CONNECTION_PER_PROFILE               ( "ldap.proxy.connectionsPerProfile" ),
     LDAP_PROXY_MAX_CONNECTIONS                      ( "ldap.proxy.maxConnections" ),
+    LDAP_PROXY_USE_THREAD_LOCAL                     ( "ldap.proxy.useThreadLocal" ),
     LDAP_EXTENSIONS_NMAS_ENABLE                     ( "ldap.extensions.nmas.enable" ),
     LDAP_CONNECTION_TIMEOUT                         ( "ldap.connection.timeoutMS" ),
     LDAP_PROFILE_RETRY_DELAY                        ( "ldap.profile.retryDelayMS" ),
@@ -305,6 +307,7 @@ public enum AppProperty
     SECURITY_DEFAULT_EPHEMERAL_HASH_ALG             ( "security.defaultEphemeralHashAlg" ),
     SEEDLIST_BUILTIN_PATH                           ( "seedlist.builtin.path" ),
     SMTP_SUBJECT_ENCODING_CHARSET                   ( "smtp.subjectEncodingCharset" ),
+    SMTP_RETRYABLE_SEND_RESPONSE_STATUSES           ( "smtp.retryableSendResponseStatus" ),
     TOKEN_CLEANER_INTERVAL_SECONDS                  ( "token.cleaner.intervalSeconds" ),
     TOKEN_MASK_EMAIL_REGEX                          ( "token.mask.email.regex" ),
     TOKEN_MASK_EMAIL_REPLACE                        ( "token.mask.email.replace" ),

+ 93 - 180
server/src/main/java/password/pwm/PwmAboutProperty.java

@@ -31,84 +31,94 @@ import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.secure.PwmRandom;
 
+import java.lang.management.ManagementFactory;
 import java.nio.charset.Charset;
 import java.time.Instant;
 import java.util.Collections;
 import java.util.Date;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.TreeMap;
 
 public enum PwmAboutProperty
 {
 
-    app_version( null ),
-    app_chaiApiVersion( null ),
-    app_currentTime( null ),
-    app_startTime( null ),
-    app_installTime( null ),
-    app_currentPublishedVersion( null ),
-    app_currentPublishedVersionCheckTime( null ),
-    app_siteUrl( null ),
-    app_instanceID( null ),
-    app_trialMode( null ),
-    app_mode_appliance( null ),
-    app_mode_docker( null ),
-    app_mode_manageHttps( null ),
-    app_applicationPath( null ),
-    app_environmentFlags( null ),
-    app_wordlistSize( null ),
-    app_seedlistSize( null ),
-    app_sharedHistorySize( null ),
-    app_sharedHistoryOldestTime( null ),
-    app_emailQueueSize( null ),
-    app_emailQueueOldestTime( null ),
-    app_smsQueueSize( null ),
-    app_smsQueueOldestTime( null ),
-    app_syslogQueueSize( null ),
-    app_localDbLogSize( null ),
-    app_localDbLogOldestTime( null ),
-    app_localDbStorageSize( null ),
-    app_localDbFreeSpace( null ),
-    app_configurationRestartCounter( null ),
-    app_secureBlockAlgorithm( null ),
-    app_secureHashAlgorithm( null ),
-    app_ldapProfileCount( null ),
-
-    build_Time( null ),
-    build_Number( null ),
-    build_Type( null ),
-    build_User( null ),
-    build_Revision( null ),
-    build_JavaVendor( null ),
-    build_JavaVersion( null ),
-    build_Version( null ),
-
-    java_memoryFree( "Java Memory Free" ),
-    java_memoryAllocated( "Java Memory Allocated" ),
-    java_memoryMax( "Java Memory Max" ),
-    java_threadCount( "Java Thread Count" ),
-    java_vmVendor( "Java Vendor" ),
-    java_vmLocation( "Java VM Location" ),
-    java_vmVersion( "Java VM Version" ),
-    java_runtimeVersion( "Java Runtime Version" ),
-    java_vmName( "Java VM Name" ),
-    java_osName( "Java OS Name" ),
-    java_osVersion( "Java OS Version" ),
-    java_osArch( "Java OS Architecture" ),
-    java_randomAlgorithm( null ),
-    java_defaultCharset( null ),
-    java_appServerInfo( "Java AppServer Info" ),
-
-    database_driverName( null ),
-    database_driverVersion( null ),
-    database_databaseProductName( null ),
-    database_databaseProductVersion( null ),;
+    app_version( null, pwmApplication -> PwmConstants.SERVLET_VERSION ),
+    app_chaiApiVersion( null, pwmApplication -> PwmConstants.CHAI_API_VERSION ),
+    app_currentTime( null, pwmApplication -> format( Instant.now() ) ),
+    app_startTime( null, pwmApplication -> format( pwmApplication.getStartupTime() ) ),
+    app_installTime( null, pwmApplication -> format( pwmApplication.getInstallTime() ) ),
+    app_siteUrl( null, pwmApplication -> pwmApplication.getConfig().readSettingAsString( PwmSetting.PWM_SITE_URL ) ),
+    app_instanceID( null, PwmApplication::getInstanceID ),
+    app_trialMode( null, pwmApplication -> Boolean.toString( PwmConstants.TRIAL_MODE ) ),
+    app_mode_appliance( null, pwmApplication -> Boolean.toString( pwmApplication.getPwmEnvironment().getFlags().contains( PwmEnvironment.ApplicationFlag.Appliance ) ) ),
+    app_mode_docker( null, pwmApplication -> Boolean.toString( pwmApplication.getPwmEnvironment().getFlags().contains( PwmEnvironment.ApplicationFlag.Docker ) ) ),
+    app_mode_manageHttps( null, pwmApplication -> Boolean.toString( pwmApplication.getPwmEnvironment().getFlags().contains( PwmEnvironment.ApplicationFlag.ManageHttps ) ) ),
+    app_applicationPath( null, pwmApplication -> pwmApplication.getPwmEnvironment().getApplicationPath().getAbsolutePath() ),
+    app_environmentFlags( null, pwmApplication -> StringUtil.collectionToString( pwmApplication.getPwmEnvironment().getFlags() ) ),
+    app_wordlistSize( null, pwmApplication -> Integer.toString( pwmApplication.getWordlistManager().size() ) ),
+    app_seedlistSize( null, pwmApplication -> Integer.toString( pwmApplication.getSeedlistManager().size() ) ),
+    app_sharedHistorySize( null, pwmApplication -> Integer.toString( pwmApplication.getSharedHistoryManager().size() ) ),
+    app_sharedHistoryOldestTime( null, pwmApplication -> format( pwmApplication.getSharedHistoryManager().getOldestEntryTime() ) ),
+    app_emailQueueSize( null, pwmApplication -> Integer.toString( pwmApplication.getEmailQueue().queueSize() ) ),
+    app_emailQueueOldestTime( null, pwmApplication -> format( Date.from( pwmApplication.getEmailQueue().eldestItem() ) ) ),
+    app_smsQueueSize( null, pwmApplication -> Integer.toString( pwmApplication.getSmsQueue().queueSize() ) ),
+    app_smsQueueOldestTime( null, pwmApplication -> format( Date.from( pwmApplication.getSmsQueue().eldestItem() ) ) ),
+    app_syslogQueueSize( null, pwmApplication -> Integer.toString( pwmApplication.getAuditManager().syslogQueueSize() ) ),
+    app_localDbLogSize( null, pwmApplication -> Integer.toString( pwmApplication.getLocalDBLogger().getStoredEventCount() ) ),
+    app_localDbLogOldestTime( null, pwmApplication -> format( pwmApplication.getLocalDBLogger().getTailDate() ) ),
+    app_localDbStorageSize( null, pwmApplication -> StringUtil.formatDiskSize( FileSystemUtility.getFileDirectorySize( pwmApplication.getLocalDB().getFileLocation() ) ) ),
+    app_localDbFreeSpace( null, pwmApplication -> StringUtil.formatDiskSize( FileSystemUtility.diskSpaceRemaining( pwmApplication.getLocalDB().getFileLocation() ) ) ),
+    app_configurationRestartCounter( null, pwmApplication -> Integer.toString( pwmApplication.getPwmEnvironment().getContextManager().getRestartCount() ) ),
+    app_secureBlockAlgorithm( null, pwmApplication -> pwmApplication.getSecureService().getDefaultBlockAlgorithm().getLabel() ),
+    app_secureHashAlgorithm( null, pwmApplication -> pwmApplication.getSecureService().getDefaultHashAlgorithm().toString() ),
+    app_ldapProfileCount( null, pwmApplication -> Integer.toString( pwmApplication.getConfig().getLdapProfiles().size() ) ),
+
+    build_Time( "Build Time", pwmApplication -> PwmConstants.BUILD_TIME ),
+    build_Number( "Build Number", pwmApplication -> PwmConstants.BUILD_NUMBER ),
+    build_Revision( "Build Revision", pwmApplication -> PwmConstants.BUILD_REVISION ),
+    build_JavaVendor( "Build Java Vendor", pwmApplication -> PwmConstants.BUILD_JAVA_VENDOR ),
+    build_JavaVersion( "Build Java Version", pwmApplication -> PwmConstants.BUILD_JAVA_VERSION ),
+    build_Version( "Build Version", pwmApplication -> PwmConstants.BUILD_VERSION ),
+
+    java_memoryFree( "Java Memory Free", pwmApplication -> Long.toString( Runtime.getRuntime().freeMemory() ) ),
+    java_memoryAllocated( "Java Memory Allocated", pwmApplication -> Long.toString( Runtime.getRuntime().totalMemory() ) ),
+    java_memoryMax( "Java Memory Max", pwmApplication -> Long.toString( Runtime.getRuntime().maxMemory() ) ),
+    java_threadCount( "Java Thread Count", pwmApplication -> Integer.toString( Thread.activeCount() ) ),
+    java_runtimeVersion( "Java Runtime Version", pwmApplication -> System.getProperty( "java.runtime.version" ) ),
+    java_vmName( "Java VM Name", pwmApplication -> System.getProperty( "java.vm.name" ) ),
+    java_vmVendor( "Java VM Vendor", pwmApplication -> System.getProperty( "java.vm.vendor" ) ),
+    java_vmLocation( "Java VM Location", pwmApplication -> System.getProperty( "java.home" ) ),
+    java_vmVersion( "Java VM Version", pwmApplication -> System.getProperty( "java.vm.version" ) ),
+    java_vmCommandLine( "Java VM Command Line", pwmApplication -> StringUtil.collectionToString( ManagementFactory.getRuntimeMXBean().getInputArguments() ) ),
+    java_osName( "Java OS Name", pwmApplication -> System.getProperty( "os.name" ) ),
+    java_osVersion( "Java OS Version", pwmApplication -> System.getProperty( "os.version" ) ),
+    java_osArch( "Java OS Architecture", pwmApplication -> System.getProperty( "os.arch" ) ),
+    java_randomAlgorithm( null, pwmApplication -> PwmRandom.getInstance().getAlgorithm() ),
+    java_defaultCharset( null, pwmApplication -> Charset.defaultCharset().name() ),
+    java_appServerInfo( "Java AppServer Info", pwmApplication -> pwmApplication.getPwmEnvironment().getContextManager().getServerInfo() ),
+
+    database_driverName( null,
+            pwmApplication -> pwmApplication.getDatabaseService().getConnectionDebugProperties().get( DatabaseService.DatabaseAboutProperty.driverName ) ),
+    database_driverVersion( null,
+            pwmApplication -> pwmApplication.getDatabaseService().getConnectionDebugProperties().get( DatabaseService.DatabaseAboutProperty.driverVersion ) ),
+    database_databaseProductName( null,
+            pwmApplication -> pwmApplication.getDatabaseService().getConnectionDebugProperties().get( DatabaseService.DatabaseAboutProperty.databaseProductName ) ),
+    database_databaseProductVersion( null,
+            pwmApplication -> pwmApplication.getDatabaseService().getConnectionDebugProperties().get( DatabaseService.DatabaseAboutProperty.databaseProductVersion ) ),;
 
     private final String label;
+    private final ValueProvider valueProvider;
 
-    PwmAboutProperty( final String label )
+    PwmAboutProperty( final String label, final ValueProvider valueProvider )
     {
         this.label = label;
+        this.valueProvider = valueProvider;
+    }
+
+    private interface ValueProvider
+    {
+        String value( PwmApplication pwmApplication );
     }
 
     private static final PwmLogger LOGGER = PwmLogger.forClass( PwmAboutProperty.class );
@@ -117,139 +127,42 @@ public enum PwmAboutProperty
             final PwmApplication pwmApplication
     )
     {
-        final Map<PwmAboutProperty, String> aboutMap = new TreeMap<>();
-
-        // about page
-        aboutMap.put( app_version, PwmConstants.SERVLET_VERSION );
-        aboutMap.put( app_currentTime, dateFormatForInfoBean( new Date() ) );
-        aboutMap.put( app_startTime, dateFormatForInfoBean( pwmApplication.getStartupTime() ) );
-        aboutMap.put( app_installTime, dateFormatForInfoBean( pwmApplication.getInstallTime() ) );
-        aboutMap.put( app_siteUrl, pwmApplication.getConfig().readSettingAsString( PwmSetting.PWM_SITE_URL ) );
-        aboutMap.put( app_ldapProfileCount, Integer.toString( pwmApplication.getConfig().getLdapProfiles().size() ) );
-        aboutMap.put( app_instanceID, pwmApplication.getInstanceID() );
-        aboutMap.put( app_trialMode, Boolean.toString( PwmConstants.TRIAL_MODE ) );
-        if ( pwmApplication.getPwmEnvironment() != null )
-        {
-            aboutMap.put( app_mode_appliance, Boolean.toString( pwmApplication.getPwmEnvironment().getFlags().contains( PwmEnvironment.ApplicationFlag.Appliance ) ) );
-            aboutMap.put( app_mode_docker, Boolean.toString( pwmApplication.getPwmEnvironment().getFlags().contains( PwmEnvironment.ApplicationFlag.Docker ) ) );
-            aboutMap.put( app_mode_manageHttps, Boolean.toString( pwmApplication.getPwmEnvironment().getFlags().contains( PwmEnvironment.ApplicationFlag.ManageHttps ) ) );
-            aboutMap.put( app_applicationPath, pwmApplication.getPwmEnvironment().getApplicationPath().getAbsolutePath() );
-            aboutMap.put( app_environmentFlags, StringUtil.collectionToString( pwmApplication.getPwmEnvironment().getFlags(), "," ) );
-        }
-        aboutMap.put( app_chaiApiVersion, PwmConstants.CHAI_API_VERSION );
-
-        aboutMap.put( app_secureBlockAlgorithm, pwmApplication.getSecureService().getDefaultBlockAlgorithm().getLabel() );
-        aboutMap.put( app_secureHashAlgorithm, pwmApplication.getSecureService().getDefaultHashAlgorithm().toString() );
-
-        aboutMap.put( app_wordlistSize, Integer.toString( pwmApplication.getWordlistManager().size() ) );
-        aboutMap.put( app_seedlistSize, Integer.toString( pwmApplication.getSeedlistManager().size() ) );
-        if ( pwmApplication.getSharedHistoryManager() != null )
-        {
-            aboutMap.put( app_sharedHistorySize, Integer.toString( pwmApplication.getSharedHistoryManager().size() ) );
-            aboutMap.put( app_sharedHistoryOldestTime, dateFormatForInfoBean( pwmApplication.getSharedHistoryManager().getOldestEntryTime() ) );
-        }
-
-
-        if ( pwmApplication.getEmailQueue() != null )
-        {
-            aboutMap.put( app_emailQueueSize, Integer.toString( pwmApplication.getEmailQueue().queueSize() ) );
-            if ( pwmApplication.getEmailQueue().eldestItem() != null )
-            {
-                aboutMap.put( app_emailQueueOldestTime, dateFormatForInfoBean( Date.from( pwmApplication.getEmailQueue().eldestItem() ) ) );
-            }
-        }
+        final Map<String, String> aboutMap = new TreeMap<>();
 
-        if ( pwmApplication.getSmsQueue() != null )
+        for ( final PwmAboutProperty pwmAboutProperty : PwmAboutProperty.values() )
         {
-            aboutMap.put( app_smsQueueSize, Integer.toString( pwmApplication.getSmsQueue().queueSize() ) );
-            if ( pwmApplication.getSmsQueue().eldestItem() != null )
+            final ValueProvider valueProvider = pwmAboutProperty.valueProvider;
+            if ( valueProvider != null )
             {
-                aboutMap.put( app_smsQueueOldestTime, dateFormatForInfoBean( Date.from( pwmApplication.getSmsQueue().eldestItem() ) ) );
-            }
-        }
-
-        if ( pwmApplication.getAuditManager() != null )
-        {
-            aboutMap.put( app_syslogQueueSize, Integer.toString( pwmApplication.getAuditManager().syslogQueueSize() ) );
-        }
-
-        if ( pwmApplication.getLocalDB() != null )
-        {
-            aboutMap.put( app_localDbLogSize, Integer.toString( pwmApplication.getLocalDBLogger().getStoredEventCount() ) );
-            aboutMap.put( app_localDbLogOldestTime, dateFormatForInfoBean( pwmApplication.getLocalDBLogger().getTailDate() ) );
-
-            aboutMap.put( app_localDbStorageSize, StringUtil.formatDiskSize( FileSystemUtility.getFileDirectorySize( pwmApplication.getLocalDB().getFileLocation() ) ) );
-            aboutMap.put( app_localDbFreeSpace, StringUtil.formatDiskSize( FileSystemUtility.diskSpaceRemaining( pwmApplication.getLocalDB().getFileLocation() ) ) );
-        }
-
-
-        {
-            // java info
-            final Runtime runtime = Runtime.getRuntime();
-            aboutMap.put( java_memoryFree, Long.toString( runtime.freeMemory() ) );
-            aboutMap.put( java_memoryAllocated, Long.toString( runtime.totalMemory() ) );
-            aboutMap.put( java_memoryMax, Long.toString( runtime.maxMemory() ) );
-            aboutMap.put( java_threadCount, Integer.toString( Thread.activeCount() ) );
-
-            aboutMap.put( java_vmVendor, System.getProperty( "java.vm.vendor" ) );
-
-            aboutMap.put( java_runtimeVersion, System.getProperty( "java.runtime.version" ) );
-            aboutMap.put( java_vmVersion, System.getProperty( "java.vm.version" ) );
-            aboutMap.put( java_vmName, System.getProperty( "java.vm.name" ) );
-            aboutMap.put( java_vmLocation, System.getProperty( "java.home" ) );
-
-            aboutMap.put( java_osName, System.getProperty( "os.name" ) );
-            aboutMap.put( java_osVersion, System.getProperty( "os.version" ) );
-            aboutMap.put( java_osArch, System.getProperty( "os.arch" ) );
-
-            aboutMap.put( java_randomAlgorithm, PwmRandom.getInstance().getAlgorithm() );
-            aboutMap.put( java_defaultCharset, Charset.defaultCharset().name() );
-        }
-
-        {
-            // build info
-            aboutMap.put( build_Time, PwmConstants.BUILD_TIME );
-            aboutMap.put( build_Number, PwmConstants.BUILD_NUMBER );
-            aboutMap.put( build_Type, PwmConstants.BUILD_TYPE );
-            aboutMap.put( build_User, PwmConstants.BUILD_USER );
-            aboutMap.put( build_Revision, PwmConstants.BUILD_REVISION );
-            aboutMap.put( build_JavaVendor, PwmConstants.BUILD_JAVA_VENDOR );
-            aboutMap.put( build_JavaVersion, PwmConstants.BUILD_JAVA_VERSION );
-            aboutMap.put( build_Version, PwmConstants.BUILD_VERSION );
-        }
-
-        {
-            // database info
-            try
-            {
-                final DatabaseService databaseService = pwmApplication.getDatabaseService();
-                if ( databaseService != null )
+                try
                 {
-                    final Map<PwmAboutProperty, String> debugData = databaseService.getConnectionDebugProperties();
-                    aboutMap.putAll( debugData );
+                    final String value = valueProvider.value( pwmApplication );
+                    aboutMap.put( pwmAboutProperty.name(), value == null ? "" : value );
+                }
+                catch ( Throwable t )
+                {
+                    aboutMap.put( pwmAboutProperty.name(), LocaleHelper.getLocalizedMessage( null, Display.Value_NotApplicable, null ) );
+                    LOGGER.trace( "error generating about value for '" + pwmAboutProperty.name() + "', error: " + t.getMessage() );
                 }
-            }
-            catch ( Throwable t )
-            {
-                LOGGER.error( "error reading database debug properties" );
             }
         }
 
-        if ( pwmApplication.getPwmEnvironment().getContextManager() != null
-                && pwmApplication.getPwmEnvironment().getContextManager().getServerInfo() != null )
+        final Map<PwmAboutProperty, String> returnMap = new LinkedHashMap<>();
+        for ( final Map.Entry<String, String> entry : aboutMap.entrySet() )
         {
-            aboutMap.put( java_appServerInfo, pwmApplication.getPwmEnvironment().getContextManager().getServerInfo() );
+            returnMap.put( PwmAboutProperty.valueOf( entry.getKey() ), entry.getValue() );
         }
 
-        return Collections.unmodifiableMap( aboutMap );
+        return Collections.unmodifiableMap( returnMap );
     }
 
-    private static String dateFormatForInfoBean( final Date date )
+    private static String format( final Date date )
     {
-        return dateFormatForInfoBean( date == null ? null : date.toInstant() );
+        return format( date == null ? null : date.toInstant() );
     }
 
-    private static String dateFormatForInfoBean( final Instant date )
+
+    private static String format( final Instant date )
     {
         if ( date != null )
         {

+ 68 - 14
server/src/main/java/password/pwm/PwmConstants.java

@@ -24,18 +24,22 @@ package password.pwm;
 
 import org.apache.commons.csv.CSVFormat;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.java.StringUtil;
 import password.pwm.util.secure.PwmHashAlgorithm;
 
+import java.net.URL;
 import java.nio.charset.Charset;
-import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Enumeration;
 import java.util.List;
 import java.util.Locale;
 import java.util.ResourceBundle;
 import java.util.TimeZone;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
 
 /**
  * Constant values used throughout the servlet.
@@ -45,14 +49,12 @@ import java.util.TimeZone;
 public abstract class PwmConstants
 {
 
-    public static final String BUILD_TIME = readBuildInfoBundle( "build.time", Instant.now().toString() );
-    public static final String BUILD_NUMBER = readBuildInfoBundle( "build.number", "0" );
-    public static final String BUILD_TYPE = readBuildInfoBundle( "build.type", "" );
-    public static final String BUILD_USER = readBuildInfoBundle( "build.user", System.getProperty( "user.name" ) );
-    public static final String BUILD_REVISION = readBuildInfoBundle( "build.revision", "0" );
-    public static final String BUILD_JAVA_VENDOR = readBuildInfoBundle( "build.java.vendor" );
-    public static final String BUILD_JAVA_VERSION = readBuildInfoBundle( "build.java.version" );
-    public static final String BUILD_VERSION = readBuildInfoBundle( "build.version", "" );
+    public static final String BUILD_TIME = readBuildInfoBundle( "Implementation-Build-Timestamp", "n/a" );
+    public static final String BUILD_NUMBER = readBuildInfoBundle( "Implementation-Build", "0" );
+    public static final String BUILD_REVISION = readBuildInfoBundle( "Implementation-Revision", "0" );
+    public static final String BUILD_JAVA_VENDOR = readBuildInfoBundle( "Implementation-Build-Java-Vendor" );
+    public static final String BUILD_JAVA_VERSION = readBuildInfoBundle( "Implementation-Build-Java-Version" );
+    public static final String BUILD_VERSION = readBuildInfoBundle( "Implementation-Version", "" );
 
     private static final String MISSING_VERSION_STRING = readPwmConstantsBundle( "missingVersionString" );
     public static final String SERVLET_VERSION;
@@ -62,8 +64,7 @@ public abstract class PwmConstants
         final String servletVersion =
                 ( BUILD_VERSION.length() > 0 ? "v" + BUILD_VERSION : "" )
                         + ( BUILD_NUMBER.length() > 0 ? " b" + BUILD_NUMBER : "" )
-                        + ( BUILD_REVISION.length() > 0 ? " r" + BUILD_REVISION : "" )
-                        + ( BUILD_TYPE.length() > 0 ? " (" + BUILD_TYPE + ")" : "" ).trim();
+                        + ( BUILD_REVISION.length() > 0 ? " r" + BUILD_REVISION : "" ).trim();
 
         SERVLET_VERSION = servletVersion.isEmpty()
                 ? MISSING_VERSION_STRING
@@ -243,13 +244,66 @@ public abstract class PwmConstants
 
     private static String readBuildInfoBundle( final String key, final String defaultValue )
     {
-        final ResourceBundle resourceBundle = ResourceBundle.getBundle( "password.pwm.BuildInformation" );
-        if ( resourceBundle.containsKey( key ) )
+
+        try
+        {
+            final Enumeration<URL> resources = PwmConstants.class.getClassLoader().getResources( "META-INF/MANIFEST.MF" );
+            while ( resources.hasMoreElements() )
+            {
+                final Manifest manifest = new Manifest( resources.nextElement().openStream() );
+                final Attributes attributes = manifest.getMainAttributes();
+                final String archiveName = attributes.getValue( "Implementation-Archive-Name" );
+                try
+                {
+                    if ( "pwm.jar".equals( archiveName ) || "pwm.war".equals( archiveName ) )
+                    {
+                        final String value = attributes.getValue( key );
+                        if ( !StringUtil.isEmpty( value ) )
+                        {
+                            return value;
+                        }
+                    }
+                }
+                catch ( Throwable t )
+                {
+                    System.out.println( t );
+                }
+            }
+        }
+        catch ( Throwable t )
+        {
+            System.out.println( t );
+        }
+
+        return defaultValue;
+
+        /*
+        try
+        {
+            final Class clazz = PwmConstants.class;
+            final String className = clazz.getSimpleName() + ".class";
+            final String classPath = clazz.getResource( className ).toString();
+            if ( !classPath.startsWith( "jar" ) )
+            {
+                // Class not from JAR
+                return defaultValue;
+            }
+            final String manifestPath = classPath.substring( 0, classPath.lastIndexOf( "!" ) + 1 ) + "/META-INF/MANIFEST.MF";
+            final Manifest manifest = new Manifest( new URL( manifestPath ).openStream() );
+            final Attributes attributes = manifest.getMainAttributes();
+            final String value = attributes.getValue( key );
+            if ( !StringUtil.isEmpty( value ) )
+            {
+                return value;
+            }
+        }
+        catch ( Throwable t )
         {
-            return resourceBundle.getString( key );
+            System.out.println( t );
         }
 
         return defaultValue;
+        */
     }
 
     public enum AcceptValue

+ 0 - 1
server/src/main/java/password/pwm/config/stored/StoredConfigurationImpl.java

@@ -1166,7 +1166,6 @@ public class StoredConfigurationImpl implements StoredConfiguration
 
             rootElement.setAttribute( "pwmVersion", PwmConstants.BUILD_VERSION );
             rootElement.setAttribute( "pwmBuild", PwmConstants.BUILD_NUMBER );
-            rootElement.setAttribute( "pwmBuildType", PwmConstants.BUILD_TYPE );
             rootElement.setAttribute( "xmlVersion", XML_FORMAT_VERSION );
 
             { // migrate old properties

+ 20 - 9
server/src/main/java/password/pwm/config/value/ActionValue.java

@@ -128,10 +128,17 @@ public class ActionValue extends AbstractValue implements StoredValue
                             final List<ActionConfiguration.WebAction> clonedWebActions = new ArrayList<>();
                             for ( final ActionConfiguration.WebAction webAction : value.getWebActions() )
                             {
+                                // add success status if empty list
+                                final List<Integer> successStatus = JavaHelper.isEmpty( webAction.getSuccessStatus() )
+                                        ? Collections.singletonList( 200 )
+                                        : webAction.getSuccessStatus();
+
+                                // decrypt pw
                                 try
                                 {
                                     clonedWebActions.add( webAction.toBuilder()
                                             .password( decryptPwValue( webAction.getPassword(), pwmSecurityKey ) )
+                                            .successStatus( successStatus )
                                             .build() );
                                 }
                                 catch ( PwmOperationalException e )
@@ -265,27 +272,31 @@ public class ActionValue extends AbstractValue implements StoredValue
             for ( final ActionConfiguration.WebAction webAction : actionConfiguration.getWebActions() )
             {
                 sb.append( "\n   WebServiceAction: " );
-                sb.append( "method=" ).append( webAction.getMethod() );
-                sb.append( " url=" ).append( webAction.getUrl() );
-                sb.append( " headers=" ).append( JsonUtil.serializeMap( webAction.getHeaders() ) );
-                sb.append( " username=" ).append( webAction.getUsername() );
-                sb.append( " password=" ).append(
+                sb.append( "\n    method=" ).append( webAction.getMethod() );
+                sb.append( "\n    url=" ).append( webAction.getUrl() );
+                sb.append( "\n    headers=" ).append( JsonUtil.serializeMap( webAction.getHeaders() ) );
+                sb.append( "\n    username=" ).append( webAction.getUsername() );
+                sb.append( "\n    password=" ).append(
                         StringUtil.isEmpty( webAction.getPassword() )
                                 ? ""
                                 : PwmConstants.LOG_REMOVED_VALUE_REPLACEMENT
                 );
+                if ( !JavaHelper.isEmpty( webAction.getSuccessStatus() ) )
+                {
+                    sb.append( "\n    successStatus=" ).append( StringUtil.collectionToString( webAction.getSuccessStatus() ) );
+                }
                 if ( StringUtil.isEmpty( webAction.getBody() ) )
                 {
-                    sb.append( " body=" ).append( webAction.getBody() );
+                    sb.append( "\n    body=" ).append( webAction.getBody() );
                 }
             }
 
             for ( final ActionConfiguration.LdapAction ldapAction : actionConfiguration.getLdapActions() )
             {
                 sb.append( "\n   LdapAction: " );
-                sb.append( "method=" ).append( ldapAction.getLdapMethod() );
-                sb.append( " attribute=" ).append( ldapAction.getAttributeName() );
-                sb.append( " value=" ).append( ldapAction.getAttributeValue() );
+                sb.append( "\n    method=" ).append( ldapAction.getLdapMethod() );
+                sb.append( "\n    attribute=" ).append( ldapAction.getAttributeName() );
+                sb.append( "\n    value=" ).append( ldapAction.getAttributeValue() );
             }
             counter++;
             if ( counter != values.size() )

+ 14 - 0
server/src/main/java/password/pwm/config/value/EmailValue.java

@@ -124,6 +124,8 @@ public class EmailValue extends AbstractValue implements StoredValue
 
     public List<String> validateValue( final PwmSetting pwmSetting )
     {
+        final int maxBodyChars = 500_000;
+
         if ( pwmSetting.isRequired() )
         {
             if ( values == null || values.isEmpty() || values.values().iterator().next() == null )
@@ -151,6 +153,18 @@ public class EmailValue extends AbstractValue implements StoredValue
             {
                 return Collections.singletonList( "plain body field is required" + ( loopLocale.length() > 0 ? " for locale " + loopLocale : "" ) );
             }
+
+            if ( emailItemBean.getBodyPlain() == null || emailItemBean.getBodyPlain().length() > maxBodyChars )
+            {
+                return Collections.singletonList( "plain body field is too large" + ( loopLocale.length() > 0 ? " for locale " + loopLocale : "" )
+                        + ", chars=" + emailItemBean.getBodyPlain().length() + ", max=" + maxBodyChars );
+            }
+
+            if ( emailItemBean.getBodyHtml() == null || emailItemBean.getBodyHtml().length() > maxBodyChars )
+            {
+                return Collections.singletonList( "html body field is too large" + ( loopLocale.length() > 0 ? " for locale " + loopLocale : "" )
+                        + ", chars=" + emailItemBean.getBodyHtml().length() + ", max=" + maxBodyChars );
+            }
         }
 
         return Collections.emptyList();

+ 3 - 0
server/src/main/java/password/pwm/config/value/data/ActionConfiguration.java

@@ -74,6 +74,9 @@ public class ActionConfiguration implements Serializable
 
         @Builder.Default
         private List<X509Certificate> certificates = Collections.emptyList();
+
+        @Builder.Default
+        private List<Integer> successStatus = Collections.singletonList( 200 );
     }
 
     @Value

+ 5 - 1
server/src/main/java/password/pwm/error/PwmError.java

@@ -343,7 +343,11 @@ public enum PwmError
             5300, "Error_HTTP_404", null ),
 
     ERROR_REST_INVOCATION_ERROR(
-            7000, "Error_RestInvocationError", null ),;
+            7000, "Error_RestInvocationError", null ),
+    ERROR_REST_PARAMETER_CONFLICT(
+            7001, "Error_RestParameterConflict", null ),
+
+    /* End of list*/;
 
     enum ErrorFlag
     {

+ 1 - 0
server/src/main/java/password/pwm/http/HttpContentType.java

@@ -32,6 +32,7 @@ public enum HttpContentType
 {
     json( "application/json", PwmConstants.DEFAULT_CHARSET ),
     zip( "application/zip" ),
+    gzip( "application/gzip" ),
     xml( "text/xml", PwmConstants.DEFAULT_CHARSET ),
     csv( "text/csv", PwmConstants.DEFAULT_CHARSET ),
     javascript( "text/javascript", PwmConstants.DEFAULT_CHARSET ),

+ 18 - 17
server/src/main/java/password/pwm/http/HttpHeader.java

@@ -28,33 +28,34 @@ import password.pwm.util.java.StringUtil;
 
 public enum HttpHeader
 {
+    Authorization( "Authorization", Property.Sensitive ),
     Accept( "Accept" ),
+    AcceptEncoding( "Accept-Encoding" ),
+    AcceptLanguage( "Accept-Language" ),
+    CacheControl( "Cache-Control" ),
     Connection( "Connection" ),
-    Content_Type( "Content-Type" ),
-    Content_Encoding( "Content-Encoding" ),
-    Location( "Location" ),
+    ContentEncoding( "Content-Encoding" ),
+    ContentDisposition( "content-disposition" ),
+    ContentLanguage( "Content-Language" ),
+    ContentLength( "Content-Length" ),
     ContentSecurityPolicy( "Content-Security-Policy" ),
+    ContentTransferEncoding( "Content-Transfer-Encoding" ),
+    ContentType( "Content-Type" ),
+    ETag( "ETag" ),
+    Expires( "Expires" ),
     If_None_Match( "If-None-Match" ),
+    Location( "Location" ),
+    Origin( "Origin" ),
+    Referer( "Referer" ),
     Server( "Server" ),
-    Cache_Control( "Cache-Control" ),
-    WWW_Authenticate( "WWW-Authenticate" ),
-    ContentDisposition( "content-disposition" ),
-    ContentTransferEncoding( "Content-Transfer-Encoding" ),
-    Content_Language( "Content-Language" ),
-    Accept_Encoding( "Accept-Encoding" ),
-    Accept_Language( "Accept-Language" ),
-    Authorization( "Authorization", Property.Sensitive ),
     UserAgent( "User-Agent" ),
-    Referer( "Referer" ),
-    Origin( "Origin" ),
+    WWW_Authenticate( "WWW-Authenticate" ),
+    XContentTypeOptions( "X-Content-Type-Options" ),
     XForwardedFor( "X-Forwarded-For" ),
-    ETag( "ETag" ),
-    Expires( "Expires" ),
-
     XFrameOptions( "X-Frame-Options" ),
-    XContentTypeOptions( "X-Content-Type-Options" ),
     XXSSProtection( "X-XSS-Protection" ),
 
+
     XAmb( "X-" + PwmConstants.PWM_APP_NAME + "-Amb" ),
     XVersion( "X-" + PwmConstants.PWM_APP_NAME + "-Version" ),
     XInstance( "X-" + PwmConstants.PWM_APP_NAME + "-Instance" ),

+ 11 - 3
server/src/main/java/password/pwm/http/JspUtility.java

@@ -37,6 +37,7 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.jsp.PageContext;
 import java.io.Serializable;
+import java.text.NumberFormat;
 import java.time.Instant;
 import java.util.Locale;
 
@@ -167,7 +168,7 @@ public abstract class JspUtility
         return forRequest( pageContext.getRequest() );
     }
 
-    public static String freindlyWrite( final PageContext pageContext, final boolean value )
+    public static String friendlyWrite( final PageContext pageContext, final boolean value )
     {
         final PwmRequest pwmRequest = forRequest( pageContext.getRequest() );
         return value
@@ -175,7 +176,14 @@ public abstract class JspUtility
                 : LocaleHelper.getLocalizedMessage( Display.Value_False, pwmRequest );
     }
 
-    public static String freindlyWrite( final PageContext pageContext, final String input )
+    public static String friendlyWrite( final PageContext pageContext, final long value )
+    {
+        final PwmRequest pwmRequest = forRequest( pageContext.getRequest() );
+        final NumberFormat numberFormat = NumberFormat.getInstance( pwmRequest.getLocale() );
+        return numberFormat.format( value );
+    }
+
+    public static String friendlyWrite( final PageContext pageContext, final String input )
     {
         final PwmRequest pwmRequest = forRequest( pageContext.getRequest() );
         if ( StringUtil.isEmpty( input ) )
@@ -185,7 +193,7 @@ public abstract class JspUtility
         return StringUtil.escapeHtml( input );
     }
 
-    public static String freindlyWrite( final PageContext pageContext, final Instant instant )
+    public static String friendlyWrite( final PageContext pageContext, final Instant instant )
     {
         final PwmRequest pwmRequest = forRequest( pageContext.getRequest() );
         if ( instant == null )

+ 22 - 0
server/src/main/java/password/pwm/http/PwmURL.java

@@ -331,6 +331,28 @@ public class PwmURL
         return output.toString();
     }
 
+    public static String encodeParametersToFormBody( final Map<String, String> parameters )
+    {
+        final StringBuilder output = new StringBuilder( );
+
+        for ( final Map.Entry<String, String> entry : parameters.entrySet() )
+        {
+            final String paramName = entry.getKey();
+            final String value = entry.getValue();
+            final String encodedValue = value == null
+                    ? ""
+                    : StringUtil.urlEncode( value );
+
+            output.append( output.length() > 0 ? "&" : "" );
+            output.append( paramName );
+            output.append( "=" );
+            output.append( encodedValue );
+        }
+
+        return output.toString();
+    }
+
+
     public static int portForUriSchema( final URI uri )
     {
         final int port = uri.getPort();

+ 2 - 0
server/src/main/java/password/pwm/http/bean/ConfigGuideBean.java

@@ -23,6 +23,7 @@
 package password.pwm.http.bean;
 
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 import password.pwm.config.option.SessionBeanMode;
 import password.pwm.config.value.FileValue;
 import password.pwm.http.servlet.configguide.ConfigGuideForm;
@@ -37,6 +38,7 @@ import java.util.Map;
 import java.util.Set;
 
 @Data
+@EqualsAndHashCode( callSuper = false )
 public class ConfigGuideBean extends PwmSessionBean
 {
 

+ 2 - 0
server/src/main/java/password/pwm/http/bean/ForgottenPasswordBean.java

@@ -26,6 +26,7 @@ import com.google.gson.annotations.SerializedName;
 import com.novell.ldapchai.cr.ChallengeSet;
 import lombok.AllArgsConstructor;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 import lombok.Value;
 import password.pwm.VerificationMethodSystem;
 import password.pwm.bean.TokenDestinationItem;
@@ -46,6 +47,7 @@ import java.util.Set;
  * @author Jason D. Rivard
  */
 @Data
+@EqualsAndHashCode( callSuper = false )
 public class ForgottenPasswordBean extends PwmSessionBean
 {
 

+ 2 - 0
server/src/main/java/password/pwm/http/bean/UpdateProfileBean.java

@@ -24,6 +24,7 @@ package password.pwm.http.bean;
 
 import com.google.gson.annotations.SerializedName;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 import password.pwm.config.option.SessionBeanMode;
 
 import java.util.Arrays;
@@ -34,6 +35,7 @@ import java.util.Map;
 import java.util.Set;
 
 @Data
+@EqualsAndHashCode( callSuper = false )
 public class UpdateProfileBean extends PwmSessionBean
 {
 

+ 2 - 2
server/src/main/java/password/pwm/http/filter/RequestInitializationFilter.java

@@ -369,7 +369,7 @@ public class RequestInitializationFilter implements Filter
         final boolean includeContentLanguage = Boolean.parseBoolean( config.readAppProperty( AppProperty.HTTP_HEADER_SEND_CONTENT_LANGUAGE ) );
         if ( includeContentLanguage )
         {
-            resp.setHeader( HttpHeader.Content_Language, pwmRequest.getLocale().toLanguageTag() );
+            resp.setHeader( HttpHeader.ContentLanguage, pwmRequest.getLocale().toLanguageTag() );
         }
 
         addStaticResponseHeaders( pwmApplication, resp.getHttpServletResponse() );
@@ -454,7 +454,7 @@ public class RequestInitializationFilter implements Filter
             ) );
         }
 
-        resp.setHeader( HttpHeader.Cache_Control.getHttpName(), "no-cache, no-store, must-revalidate, proxy-revalidate" );
+        resp.setHeader( HttpHeader.CacheControl.getHttpName(), "no-cache, no-store, must-revalidate, proxy-revalidate" );
     }
 
 

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java

@@ -162,7 +162,7 @@ public class ClientApiServlet extends ControlledPwmServlet
 
         pwmRequest.getPwmResponse().setHeader( HttpHeader.ETag, eTagValue );
         pwmRequest.getPwmResponse().setHeader( HttpHeader.Expires, String.valueOf( System.currentTimeMillis() + ( maxCacheAgeSeconds * 1000 ) ) );
-        pwmRequest.getPwmResponse().setHeader( HttpHeader.Cache_Control, "public, max-age=" + maxCacheAgeSeconds );
+        pwmRequest.getPwmResponse().setHeader( HttpHeader.CacheControl, "public, max-age=" + maxCacheAgeSeconds );
 
         final AppData appData = makeAppData(
                 pwmRequest.getPwmApplication(),
@@ -188,7 +188,7 @@ public class ClientApiServlet extends ControlledPwmServlet
 
         pwmRequest.getPwmResponse().setHeader( HttpHeader.ETag, eTagValue );
         pwmRequest.getPwmResponse().setHeader( HttpHeader.Expires, String.valueOf( System.currentTimeMillis() + ( maxCacheAgeSeconds * 1000 ) ) );
-        pwmRequest.getPwmResponse().setHeader( HttpHeader.Cache_Control, "public, max-age=" + maxCacheAgeSeconds );
+        pwmRequest.getPwmResponse().setHeader( HttpHeader.CacheControl, "public, max-age=" + maxCacheAgeSeconds );
 
         try
         {

+ 216 - 34
server/src/main/java/password/pwm/http/servlet/admin/AdminServlet.java

@@ -57,7 +57,6 @@ import password.pwm.svc.event.AuditRecord;
 import password.pwm.svc.intruder.RecordType;
 import password.pwm.svc.pwnotify.PwNotifyService;
 import password.pwm.svc.pwnotify.StoredJobState;
-import password.pwm.svc.report.ReportColumnFilter;
 import password.pwm.svc.report.ReportCsvUtility;
 import password.pwm.svc.report.ReportService;
 import password.pwm.svc.report.UserCacheRecord;
@@ -69,9 +68,12 @@ import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
-import password.pwm.util.localdb.LocalDBException;
+import password.pwm.util.logging.LocalDBLogger;
+import password.pwm.util.logging.LocalDBSearchQuery;
+import password.pwm.util.logging.LocalDBSearchResults;
+import password.pwm.util.logging.PwmLogEvent;
+import password.pwm.util.logging.PwmLogLevel;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.reports.ReportUtils;
 import password.pwm.ws.server.RestResultBean;
 import password.pwm.ws.server.rest.RestStatisticsServer;
 
@@ -79,6 +81,7 @@ import javax.servlet.ServletException;
 import javax.servlet.annotation.WebServlet;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.io.OutputStreamWriter;
 import java.io.Serializable;
 import java.io.Writer;
 import java.lang.management.ManagementFactory;
@@ -93,8 +96,12 @@ import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
+import java.util.zip.GZIPOutputStream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
 
 @WebServlet(
         name = "AdminServlet",
@@ -116,6 +123,7 @@ public class AdminServlet extends ControlledPwmServlet
         downloadUserReportCsv( HttpMethod.POST ),
         downloadUserSummaryCsv( HttpMethod.POST ),
         downloadStatisticsLogCsv( HttpMethod.POST ),
+        downloadSessionsCsv( HttpMethod.POST ),
         clearIntruderTable( HttpMethod.POST ),
         reportCommand( HttpMethod.POST ),
         reportStatus( HttpMethod.GET ),
@@ -128,7 +136,9 @@ public class AdminServlet extends ControlledPwmServlet
         statistics( HttpMethod.GET ),
         startPwNotifyJob( HttpMethod.POST ),
         readPwNotifyStatus( HttpMethod.POST ),
-        readPwNotifyLog( HttpMethod.POST ),;
+        readPwNotifyLog( HttpMethod.POST ),
+        readLogData( HttpMethod.POST ),
+        downloadLogData( HttpMethod.POST ),;
 
         private final Collection<HttpMethod> method;
 
@@ -230,11 +240,8 @@ public class AdminServlet extends ControlledPwmServlet
         final OutputStream outputStream = pwmRequest.getPwmResponse().getOutputStream();
         try
         {
-            final String selectedColumns = pwmRequest.readParameterAsString( "selectedColumns", "" );
-
-            final ReportColumnFilter columnFilter = ReportUtils.toReportColumnFilter( selectedColumns );
             final ReportCsvUtility reportCsvUtility = new ReportCsvUtility( pwmApplication );
-            reportCsvUtility.outputToCsv( outputStream, true, pwmRequest.getLocale(), columnFilter );
+            reportCsvUtility.outputToCsv( outputStream, true, pwmRequest.getLocale() );
         }
         catch ( Exception e )
         {
@@ -308,6 +315,34 @@ public class AdminServlet extends ControlledPwmServlet
         return ProcessStatus.Halt;
     }
 
+    @ActionHandler( action = "downloadSessionsCsv" )
+    private ProcessStatus downloadSessionsCsv( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException, IOException, ChaiUnavailableException, ServletException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+
+        pwmRequest.getPwmResponse().markAsDownload(
+                HttpContentType.csv,
+                pwmRequest.getPwmApplication().getConfig().readAppProperty( AppProperty.DOWNLOAD_FILENAME_SESSIONS_CSV )
+        );
+
+        final OutputStream outputStream = pwmRequest.getPwmResponse().getOutputStream();
+        try
+        {
+            pwmApplication.getSessionTrackService().outputToCsv( pwmRequest.getLocale(), pwmRequest.getConfig(), outputStream );
+        }
+        catch ( Exception e )
+        {
+            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_UNKNOWN, e.getMessage() );
+            pwmRequest.respondWithError( errorInformation );
+        }
+        finally
+        {
+            outputStream.close();
+        }
+        return ProcessStatus.Halt;
+    }
+
     @ActionHandler( action = "clearIntruderTable" )
     private ProcessStatus processClearIntruderTable(
             final PwmRequest pwmRequest
@@ -346,27 +381,20 @@ public class AdminServlet extends ControlledPwmServlet
 
     @ActionHandler( action = "reportStatus" )
     private ProcessStatus processReportStatus( final PwmRequest pwmRequest )
-            throws ChaiUnavailableException, PwmUnrecoverableException, IOException
+            throws IOException
     {
-        try
-        {
-            final ReportStatusBean returnMap = ReportStatusBean.makeReportStatusData(
-                    pwmRequest.getPwmApplication().getReportService(),
-                    pwmRequest.getPwmSession().getSessionStateBean().getLocale()
-            );
-            final RestResultBean restResultBean = RestResultBean.withData( returnMap );
-            pwmRequest.outputJsonResult( restResultBean );
-        }
-        catch ( LocalDBException e )
-        {
-            throw new PwmUnrecoverableException( e.getErrorInformation() );
-        }
+        final ReportStatusBean returnMap = ReportStatusBean.makeReportStatusData(
+                pwmRequest.getPwmApplication().getReportService(),
+                pwmRequest.getPwmSession().getSessionStateBean().getLocale()
+        );
+        final RestResultBean restResultBean = RestResultBean.withData( returnMap );
+        pwmRequest.outputJsonResult( restResultBean );
         return ProcessStatus.Halt;
     }
 
     @ActionHandler( action = "reportSummary" )
     private ProcessStatus processReportSummary( final PwmRequest pwmRequest )
-            throws ChaiUnavailableException, PwmUnrecoverableException, IOException
+            throws IOException
     {
         final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
         final LinkedHashMap<String, Object> returnMap = new LinkedHashMap<>();
@@ -389,10 +417,9 @@ public class AdminServlet extends ControlledPwmServlet
 
         final ReportService reportService = pwmRequest.getPwmApplication().getReportService();
         final ArrayList<UserCacheRecord> reportData = new ArrayList<>();
-        ClosableIterator<UserCacheRecord> cacheBeanIterator = null;
-        try
+
+        try ( ClosableIterator<UserCacheRecord> cacheBeanIterator = reportService.iterator() )
         {
-            cacheBeanIterator = reportService.iterator();
             while ( cacheBeanIterator.hasNext() && reportData.size() < maximum )
             {
                 final UserCacheRecord userCacheRecord = cacheBeanIterator.next();
@@ -402,13 +429,6 @@ public class AdminServlet extends ControlledPwmServlet
                 }
             }
         }
-        finally
-        {
-            if ( cacheBeanIterator != null )
-            {
-                cacheBeanIterator.close();
-            }
-        }
 
         final HashMap<String, Object> returnData = new HashMap<>();
         returnData.put( "users", reportData );
@@ -703,7 +723,7 @@ public class AdminServlet extends ControlledPwmServlet
         {
             final DisplayElement displayElement = new DisplayElement( String.valueOf( key++ ), DisplayElement.Type.string, "Status",
                     "Password Notification Feature is not enabled.  See setting: "
-                    + PwmSetting.PW_EXPY_NOTIFY_ENABLE.toMenuLocationDebug( null, pwmRequest.getLocale() ) );
+                            + PwmSetting.PW_EXPY_NOTIFY_ENABLE.toMenuLocationDebug( null, pwmRequest.getLocale() ) );
             pwmRequest.outputJsonResult( RestResultBean.withData( new PwNotifyStatusBean( Collections.singletonList( displayElement ), false ) ) );
             return ProcessStatus.Halt;
         }
@@ -799,6 +819,168 @@ public class AdminServlet extends ControlledPwmServlet
         return ProcessStatus.Halt;
     }
 
+    public enum LogDisplayType
+    {
+        grid,
+        lines,
+    }
+
+    @ActionHandler( action = "readLogData" )
+    public ProcessStatus readLogData( final PwmRequest pwmRequest ) throws IOException, PwmUnrecoverableException
+    {
+        final LocalDBLogger localDBLogger = pwmRequest.getPwmApplication().getLocalDBLogger();
+
+        final LogDisplayType logDisplayType;
+        final LocalDBSearchQuery searchParameters;
+        {
+            final Map<String, String> inputMap = pwmRequest.readBodyAsJsonStringMap( PwmHttpRequestWrapper.Flag.BypassValidation );
+            final int eventCount = Integer.parseInt( inputMap.getOrDefault( "count", "0" ) );
+            final TimeDuration maxTimeSeconds = new TimeDuration( Integer.parseInt( inputMap.getOrDefault( "maxTime", "5" ) ), TimeUnit.SECONDS );
+            final String username = inputMap.getOrDefault( "username", "" );
+            final String text = inputMap.getOrDefault( "text", "" );
+            final PwmLogLevel logLevel = JavaHelper.readEnumFromString( PwmLogLevel.class, PwmLogLevel.TRACE, inputMap.get( "level" ) );
+            final LocalDBLogger.EventType logType = JavaHelper.readEnumFromString( LocalDBLogger.EventType.class, LocalDBLogger.EventType.Both, inputMap.get( "type" ) );
+            logDisplayType = JavaHelper.readEnumFromString( LogDisplayType.class, LogDisplayType.grid, inputMap.get( "displayType" ) );
+
+            searchParameters = LocalDBSearchQuery.builder()
+                    .minimumLevel( logLevel )
+                    .maxEvents( eventCount )
+                    .username( username )
+                    .text( text )
+                    .maxQueryTime( maxTimeSeconds )
+                    .eventType( logType )
+                    .build();
+        }
+
+        final LocalDBSearchResults searchResults = localDBLogger.readStoredEvents( searchParameters );
+
+        final LinkedHashMap<String, Object> returnData = new LinkedHashMap<>(  );
+        if ( logDisplayType == LogDisplayType.grid )
+        {
+            final ArrayList<PwmLogEvent> pwmLogEvents = new ArrayList<>();
+            while ( searchResults.hasNext() )
+            {
+                pwmLogEvents.add( searchResults.next() );
+            }
+            returnData.put( "records", pwmLogEvents );
+        }
+        else if ( logDisplayType == LogDisplayType.lines )
+        {
+            final ArrayList<String> pwmLogEvents = new ArrayList<>();
+            while ( searchResults.hasNext() )
+            {
+                pwmLogEvents.add( searchResults.next().toLogString() );
+            }
+            returnData.put( "records", pwmLogEvents );
+        }
+        returnData.put( "display", logDisplayType );
+        returnData.put( "size", searchResults.getReturnedEvents() );
+        returnData.put( "duration", searchResults.getSearchTime() );
+        pwmRequest.outputJsonResult( RestResultBean.withData( returnData ) );
+
+        return ProcessStatus.Halt;
+    }
+
+    public enum LogDownloadType
+    {
+        plain,
+        csv,
+    }
+
+    public enum LogDownloadCompression
+    {
+        none,
+        zip,
+        gzip,
+    }
+
+    @ActionHandler( action = "downloadLogData" )
+    public ProcessStatus downloadLogData( final PwmRequest pwmRequest ) throws IOException, PwmUnrecoverableException
+    {
+        final LocalDBLogger localDBLogger = pwmRequest.getPwmApplication().getLocalDBLogger();
+
+        final LogDownloadType logDownloadType = JavaHelper.readEnumFromString( LogDownloadType.class, LogDownloadType.plain, pwmRequest.readParameterAsString( "downloadType" ) );
+
+        final LocalDBSearchQuery searchParameters = LocalDBSearchQuery.builder()
+                .minimumLevel( PwmLogLevel.TRACE )
+                .eventType( LocalDBLogger.EventType.Both )
+                .build();
+
+        final LocalDBSearchResults searchResults = localDBLogger.readStoredEvents( searchParameters );
+
+        final String compressFileNameSuffix;
+        final HttpContentType compressedContentType;
+        final Writer writer;
+        {
+            final LogDownloadCompression logDownloadCompression = JavaHelper.readEnumFromString(
+                    LogDownloadCompression.class,
+                    AdminServlet.LogDownloadCompression.none,
+                    pwmRequest.readParameterAsString( "compressionType" ) );
+
+            final OutputStream compressedStream;
+
+            switch ( logDownloadCompression )
+            {
+                case zip:
+                    final ZipOutputStream zipOutputStream = new ZipOutputStream( pwmRequest.getPwmResponse().getOutputStream() );
+                    zipOutputStream.putNextEntry( new ZipEntry( "debug.txt"  ) );
+                    compressedStream = zipOutputStream;
+                    compressFileNameSuffix = ".zip";
+                    compressedContentType = HttpContentType.zip;
+                    break;
+
+                case gzip:
+                    compressedStream = new GZIPOutputStream( pwmRequest.getPwmResponse().getOutputStream() );
+                    compressFileNameSuffix = ".gz";
+                    compressedContentType = HttpContentType.gzip;
+                    break;
+
+                default:
+                    compressedStream = pwmRequest.getPwmResponse().getOutputStream();
+                    compressFileNameSuffix = "";
+                    compressedContentType = null;
+            }
+            writer = new OutputStreamWriter( compressedStream, PwmConstants.DEFAULT_CHARSET );
+        }
+        
+        switch ( logDownloadType )
+        {
+            case plain:
+            {
+                while ( searchResults.hasNext() )
+                {
+                    writer.write( searchResults.next().toLogString( true ) );
+                    writer.write( "\n" );
+                    pwmRequest.getPwmResponse().markAsDownload(
+                            compressedContentType != null ? compressedContentType : HttpContentType.plain,
+                            "debug.txt" + compressFileNameSuffix );
+                }
+            }
+            break;
+
+            case csv:
+            {
+                pwmRequest.getPwmResponse().markAsDownload(
+                        compressedContentType != null ? compressedContentType : HttpContentType.csv,
+                        "debug.csv" + compressFileNameSuffix );
+                while ( searchResults.hasNext() )
+                {
+                    writer.write( searchResults.next().toCsvLine( ) );
+                    writer.write( "\n" );
+                }
+            }
+            break;
+
+            default:
+                JavaHelper.unhandledSwitchStatement( logDownloadType );
+
+        }
+
+        writer.close();
+
+        return ProcessStatus.Halt;
+    }
+
     @Value
     public static class PwNotifyStatusBean implements Serializable
     {

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/admin/AppDashboardData.java

@@ -450,7 +450,7 @@ public class AppDashboardData implements Serializable
             final PwmNumberFormat numberFormat = PwmNumberFormat.forLocale( locale );
 
             final String display = numberFormat.format( pwmApplication.getResourceServletService().itemsInCache() )
-                    + "items (" + numberFormat.format( pwmApplication.getResourceServletService().bytesInCache() ) + " bytes)";
+                    + " items (" + numberFormat.format( pwmApplication.getResourceServletService().bytesInCache() ) + " bytes)";
 
             javaInfo.add( new DisplayElement(
                     "resourceFileServletCacheSize",

+ 43 - 72
server/src/main/java/password/pwm/http/servlet/admin/ReportStatusBean.java

@@ -22,69 +22,78 @@
 
 package password.pwm.http.servlet.admin;
 
+import lombok.Builder;
+import lombok.Value;
+import password.pwm.http.bean.DisplayElement;
 import password.pwm.svc.report.ReportService;
 import password.pwm.svc.report.ReportStatusInfo;
+import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.PwmNumberFormat;
 import password.pwm.util.java.TimeDuration;
-import password.pwm.util.localdb.LocalDBException;
 
 import java.io.Serializable;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.util.ArrayList;
 import java.util.HashSet;
-import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Locale;
-import java.util.Map;
 import java.util.Set;
 
+@Value
+@Builder
 public class ReportStatusBean implements Serializable
 {
-    private Map<String, Object> presentable = new LinkedHashMap<>();
-    private ReportStatusInfo raw;
-    private boolean controllable;
-    private Set<ReportService.ReportCommand> availableCommands;
+    private final List<DisplayElement> presentable;
+    private final ReportStatusInfo raw;
+    private final boolean controllable;
+    private final Set<ReportService.ReportCommand> availableCommands;
 
-    public static ReportStatusBean makeReportStatusData( final ReportService reportService, final Locale locale )
-            throws LocalDBException
+    static ReportStatusBean makeReportStatusData( final ReportService reportService, final Locale locale )
     {
         final PwmNumberFormat numberFormat = PwmNumberFormat.forLocale( locale );
-
-        final ReportStatusBean returnMap = new ReportStatusBean();
         final ReportStatusInfo reportInfo = reportService.getReportStatusInfo();
-        final LinkedHashMap<String, Object> presentableMap = new LinkedHashMap<>();
+        final List<DisplayElement> presentableMap = new ArrayList<>();
         final Set<ReportService.ReportCommand> availableCommands = new HashSet<>();
 
-        presentableMap.put( "Job Engine", reportInfo.getCurrentProcess().getLabel() );
+        presentableMap.add( new DisplayElement( "jobEngine", DisplayElement.Type.string, "Job Engine", reportInfo.getCurrentProcess().getLabel() ) );
 
         switch ( reportInfo.getCurrentProcess() )
         {
             case RollOver:
             {
-                presentableMap.put( "Users Processed",
+                presentableMap.add( new DisplayElement( "usersProcessed", DisplayElement.Type.string, "Users Processed",
                         numberFormat.format( reportService.getSummaryData().getTotalUsers() )
-                                + " of " + numberFormat.format( reportService.getTotalRecords() ) );
+                                + " of " + numberFormat.format( reportService.getTotalRecords() ) ) );
                 availableCommands.add( ReportService.ReportCommand.Stop );
             }
             break;
 
             case ReadData:
             {
-                presentableMap.put( "Users Processed", numberFormat.format( reportInfo.getCount() ) );
-                presentableMap.put( "Users Remaining", numberFormat.format( reportService.getWorkQueueSize() ) );
+                presentableMap.add( new DisplayElement( "usersProcessed", DisplayElement.Type.string, "Users Processed",
+                        numberFormat.format( reportInfo.getCount() ) ) );
+                presentableMap.add( new DisplayElement( "usersRemaining", DisplayElement.Type.string, "Users Remaining",
+                        numberFormat.format( reportService.getWorkQueueSize() ) ) );
                 if ( reportInfo.getJobDuration() != null )
                 {
-                    presentableMap.put( "Job Time", reportInfo.getJobDuration().asLongString( locale ) );
+                    presentableMap.add( new DisplayElement( "jobTime", DisplayElement.Type.string, "Job Time",
+                            reportInfo.getJobDuration().asLongString( locale ) ) );
                 }
                 if ( reportInfo.getCount() > 0 )
                 {
                     final BigDecimal eventRate = reportService.getEventRate().setScale( 2, RoundingMode.UP );
-                    presentableMap.put( "Users/Second", eventRate );
-                    if ( !eventRate.equals( BigDecimal.ZERO ) )
+                    if ( eventRate != null )
+                    {
+                        presentableMap.add( new DisplayElement( "usersPerSecond", DisplayElement.Type.number, "Users/Second", eventRate.toString() ) );
+                    }
+                    if ( !BigDecimal.ZERO.equals( eventRate ) )
                     {
                         final int usersRemaining = reportService.getWorkQueueSize();
                         final float secondsRemaining = usersRemaining / eventRate.floatValue();
                         final TimeDuration remainingDuration = new TimeDuration( ( ( int ) secondsRemaining ) * 1000 );
-                        presentableMap.put( "Estimated Time Remaining", remainingDuration.asLongString( locale ) );
+                        presentableMap.add( new DisplayElement( "timeRemaining", DisplayElement.Type.string, "Estimated Time Remaining",
+                                remainingDuration.asLongString( locale ) ) );
                     }
                 }
                 availableCommands.add( ReportService.ReportCommand.Stop );
@@ -95,7 +104,8 @@ public class ReportStatusBean implements Serializable
             {
                 if ( reportInfo.getFinishDate() != null )
                 {
-                    presentableMap.put( "Last Job Completed", reportInfo.getFinishDate() );
+                    presentableMap.add( new DisplayElement( "lastCompleted", DisplayElement.Type.timestamp,  "Last Job Completed",
+                            JavaHelper.toIsoDate( reportInfo.getFinishDate() ) ) );
                 }
                 availableCommands.add( ReportService.ReportCommand.Start );
                 if ( reportService.getTotalRecords() > 0 )
@@ -114,65 +124,26 @@ public class ReportStatusBean implements Serializable
         {
             if ( reportInfo.getErrors() > 0 )
             {
-                presentableMap.put( "Error Count", numberFormat.format( reportInfo.getErrors() ) );
+                presentableMap.add( new DisplayElement( "errorCount", DisplayElement.Type.number, "Error Count", numberFormat.format( reportInfo.getErrors() ) ) );
             }
             if ( reportInfo.getLastError() != null )
             {
-                presentableMap.put( "Last Error", reportInfo.getLastError().toDebugStr() );
+                presentableMap.add( new DisplayElement( "lastError", DisplayElement.Type.string, "Last Error", reportInfo.getLastError().toDebugStr() ) );
             }
             final int totalRecords = reportService.getTotalRecords();
-            presentableMap.put( "Records in Cache", numberFormat.format( totalRecords ) );
+            presentableMap.add( new DisplayElement( "recordsInCache", DisplayElement.Type.string, "Records in Cache", numberFormat.format( totalRecords ) ) );
             if ( totalRecords > 0 )
             {
-                presentableMap.put( "Mean Record Cache Time", reportService.getSummaryData().getMeanCacheTime() );
+                presentableMap.add( new DisplayElement( "meanRecordCacheTime", DisplayElement.Type.timestamp, "Mean Record Cache Time",
+                        JavaHelper.toIsoDate( reportService.getSummaryData().getMeanCacheTime() ) ) );
             }
         }
 
-
-        returnMap.setControllable( true );
-        returnMap.setRaw( reportInfo );
-        returnMap.setPresentable( presentableMap );
-        returnMap.setAvailableCommands( availableCommands );
-        return returnMap;
-    }
-
-    public Map<String, Object> getPresentable( )
-    {
-        return presentable;
-    }
-
-    public void setPresentable( final Map<String, Object> presentable )
-    {
-        this.presentable = presentable;
-    }
-
-    public ReportStatusInfo getRaw( )
-    {
-        return raw;
-    }
-
-    public void setRaw( final ReportStatusInfo raw )
-    {
-        this.raw = raw;
-    }
-
-    public boolean isControllable( )
-    {
-        return controllable;
-    }
-
-    public void setControllable( final boolean controllable )
-    {
-        this.controllable = controllable;
-    }
-
-    public Set<ReportService.ReportCommand> getAvailableCommands( )
-    {
-        return availableCommands;
-    }
-
-    public void setAvailableCommands( final Set<ReportService.ReportCommand> availableCommands )
-    {
-        this.availableCommands = availableCommands;
+        return ReportStatusBean.builder()
+                .controllable( true )
+                .raw( reportInfo )
+                .presentable( presentableMap )
+                .availableCommands( availableCommands )
+                .build();
     }
 }

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/command/CommandServlet.java

@@ -125,7 +125,7 @@ public abstract class CommandServlet extends ControlledPwmServlet
         pwmRequest.validatePwmFormID();
         if ( !pwmRequest.getPwmResponse().isCommitted() )
         {
-            pwmRequest.getPwmResponse().setHeader( HttpHeader.Cache_Control, "no-cache, no-store, must-revalidate" );
+            pwmRequest.getPwmResponse().setHeader( HttpHeader.CacheControl, "no-cache, no-store, must-revalidate" );
             pwmRequest.getPwmResponse().setContentType( HttpContentType.plain );
         }
         return ProcessStatus.Halt;
@@ -173,7 +173,7 @@ public abstract class CommandServlet extends ControlledPwmServlet
         LOGGER.debug( "pageLeaveNotice indicated at " + pageLeaveNoticeTime.toString() + ", referer=" + referrer );
         if ( !pwmRequest.getPwmResponse().isCommitted() )
         {
-            pwmRequest.getPwmResponse().setHeader( HttpHeader.Cache_Control, "no-cache, no-store, must-revalidate" );
+            pwmRequest.getPwmResponse().setHeader( HttpHeader.CacheControl, "no-cache, no-store, must-revalidate" );
             pwmRequest.getPwmResponse().setContentType( HttpContentType.plain );
         }
         return ProcessStatus.Halt;

+ 71 - 50
server/src/main/java/password/pwm/http/servlet/configmanager/DebugItemGenerator.java

@@ -28,7 +28,6 @@ import password.pwm.PwmAboutProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.bean.UserIdentity;
-import password.pwm.bean.pub.SessionStateInfoBean;
 import password.pwm.config.Configuration;
 import password.pwm.config.stored.StoredConfigurationImpl;
 import password.pwm.error.PwmUnrecoverableException;
@@ -39,6 +38,8 @@ import password.pwm.http.servlet.admin.UserDebugDataBean;
 import password.pwm.http.servlet.admin.UserDebugDataReader;
 import password.pwm.ldap.LdapDebugDataGenerator;
 import password.pwm.svc.PwmService;
+import password.pwm.svc.cache.CacheService;
+import password.pwm.svc.cluster.ClusterService;
 import password.pwm.util.LDAPPermissionCalculator;
 import password.pwm.util.java.FileSystemUtility;
 import password.pwm.util.java.JavaHelper;
@@ -68,7 +69,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Enumeration;
-import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -76,6 +76,7 @@ import java.util.Properties;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
@@ -99,7 +100,9 @@ public class DebugItemGenerator
             LDAPPermissionItemGenerator.class,
             LocalDBDebugGenerator.class,
             SessionDataGenerator.class,
-            LdapRecentUserDebugGenerator.class
+            LdapRecentUserDebugGenerator.class,
+            ClusterInfoDebugGenerator.class,
+            CacheServiceDebugItemGenerator.class
     ) );
 
     static void outputZipDebugFile(
@@ -512,14 +515,12 @@ public class DebugItemGenerator
 
             final int maxCount = Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.CONFIG_MANAGER_ZIPDEBUG_MAXLOGLINES ) );
             final int maxSeconds = Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.CONFIG_MANAGER_ZIPDEBUG_MAXLOGSECONDS ) );
-            final LocalDBSearchQuery searchParameters = new LocalDBSearchQuery(
-                    PwmLogLevel.TRACE,
-                    maxCount,
-                    null,
-                    null,
-                    ( maxSeconds * 1000 ),
-                    null
-            );
+            final LocalDBSearchQuery searchParameters = LocalDBSearchQuery.builder()
+                    .minimumLevel( PwmLogLevel.TRACE )
+                    .maxEvents( maxCount )
+                    .maxQueryTime( new TimeDuration( maxSeconds, TimeUnit.SECONDS ) )
+                    .build();
+
             final LocalDBSearchResults searchResults = pwmApplication.getLocalDBLogger().readStoredEvents(
                     searchParameters );
             int counter = 0;
@@ -616,46 +617,10 @@ public class DebugItemGenerator
                 final PwmApplication pwmApplication,
                 final PwmRequest pwmRequest,
                 final OutputStream outputStream
-        ) throws Exception
+        )
+                throws Exception
         {
-
-
-            final CSVPrinter csvPrinter = JavaHelper.makeCsvPrinter( outputStream );
-            {
-                final List<String> headerRow = new ArrayList<>();
-                headerRow.add( "Label" );
-                headerRow.add( "Create Time" );
-                headerRow.add( "Last Time" );
-                headerRow.add( "Idle" );
-                headerRow.add( "Source Address" );
-                headerRow.add( "Source Host" );
-                headerRow.add( "LDAP Profile" );
-                headerRow.add( "UserID" );
-                headerRow.add( "UserDN" );
-                headerRow.add( "Locale" );
-                headerRow.add( "Last URL" );
-                csvPrinter.printComment( StringUtil.join( headerRow, "," ) );
-            }
-
-            final Iterator<SessionStateInfoBean> debugInfos = pwmApplication.getSessionTrackService().getSessionInfoIterator();
-            while ( debugInfos.hasNext() )
-            {
-                final SessionStateInfoBean info = debugInfos.next();
-                final List<String> dataRow = new ArrayList<>();
-                dataRow.add( info.getLabel() );
-                dataRow.add( JavaHelper.toIsoDate( info.getCreateTime() ) );
-                dataRow.add( JavaHelper.toIsoDate( info.getLastTime() ) );
-                dataRow.add( info.getIdle() );
-                dataRow.add( info.getSrcAddress() );
-                dataRow.add( info.getSrcHost() );
-                dataRow.add( info.getLdapProfile() );
-                dataRow.add( info.getUserID() );
-                dataRow.add( info.getUserDN() );
-                dataRow.add( info.getLocale() != null ? info.getLocale().toLanguageTag() : "" );
-                dataRow.add( info.getLastUrl() );
-                csvPrinter.printRecord( dataRow );
-            }
-            csvPrinter.flush();
+            pwmApplication.getSessionTrackService().outputToCsv( pwmRequest.getLocale(), pwmRequest.getConfig(), outputStream );
         }
     }
 
@@ -693,6 +658,62 @@ public class DebugItemGenerator
         }
     }
 
+    static class ClusterInfoDebugGenerator implements Generator
+    {
+        @Override
+        public String getFilename( )
+        {
+            return "cluster-info.json";
+        }
+
+        @Override
+        public void outputItem(
+                final PwmApplication pwmApplication,
+                final PwmRequest pwmRequest,
+                final OutputStream outputStream
+        )
+                throws Exception
+        {
+            final ClusterService clusterService = pwmApplication.getClusterService();
+
+            final Map<String, Serializable> debugOutput = new LinkedHashMap<>();
+            debugOutput.put( "status", clusterService.status() );
+
+            if ( clusterService.status() == PwmService.STATUS.OPEN )
+            {
+                debugOutput.put( "isMaster", clusterService.isMaster() );
+                debugOutput.put( "nodes", new ArrayList<>( clusterService.nodes() ) );
+            }
+
+            outputStream.write( JsonUtil.serializeMap( debugOutput, JsonUtil.Flag.PrettyPrint ).getBytes( PwmConstants.DEFAULT_CHARSET ) );
+        }
+    }
+
+    static class CacheServiceDebugItemGenerator implements Generator
+    {
+        @Override
+        public String getFilename( )
+        {
+            return "cache-service-info.json";
+        }
+
+        @Override
+        public void outputItem(
+                final PwmApplication pwmApplication,
+                final PwmRequest pwmRequest,
+                final OutputStream outputStream
+        )
+                throws Exception
+        {
+            final CacheService cacheService = pwmApplication.getCacheService();
+
+            final Map<String, Serializable> debugOutput = new LinkedHashMap<>( cacheService.debugInfo() );
+            outputStream.write( JsonUtil.serializeMap( debugOutput, JsonUtil.Flag.PrettyPrint ).getBytes( PwmConstants.DEFAULT_CHARSET ) );
+        }
+
+    }
+
+
     interface Generator
     {
 

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/forgottenpw/RemoteVerificationMethod.java

@@ -143,8 +143,8 @@ public class RemoteVerificationMethod implements VerificationMethodSystem
         lastResponse = null;
 
         final Map<String, String> headers = new LinkedHashMap<>();
-        headers.put( HttpHeader.Content_Type.getHttpName(), HttpContentType.json.getHeaderValue() );
-        headers.put( HttpHeader.Accept_Language.getHttpName(), locale.toLanguageTag() );
+        headers.put( HttpHeader.ContentType.getHttpName(), HttpContentType.json.getHeaderValue() );
+        headers.put( HttpHeader.AcceptLanguage.getHttpName(), locale.toLanguageTag() );
 
         final RemoteVerificationRequestBean remoteVerificationRequestBean = new RemoteVerificationRequestBean();
         remoteVerificationRequestBean.setResponseSessionID( this.remoteSessionID );

+ 2 - 2
server/src/main/java/password/pwm/http/servlet/oauth/OAuthMachine.java

@@ -245,7 +245,7 @@ public class OAuthMachine
     )
             throws PwmUnrecoverableException
     {
-        final String requestBody = PwmURL.appendAndEncodeUrlParameters( "", requestParams );
+        final String requestBody = PwmURL.encodeParametersToFormBody( requestParams );
         final List<X509Certificate> certs = settings.getCertificates();
 
         final PwmHttpClientRequest pwmHttpClientRequest;
@@ -253,7 +253,7 @@ public class OAuthMachine
             final Map<String, String> headers = new HashMap<>( );
             headers.put( HttpHeader.Authorization.getHttpName(),
                     new BasicAuthInfo( settings.getClientID(), settings.getSecret() ).toAuthHeader() );
-            headers.put( HttpHeader.Content_Type.getHttpName(), HttpContentType.form.getHeaderValue() );
+            headers.put( HttpHeader.ContentType.getHttpName(), HttpContentType.form.getHeaderValue() );
 
             pwmHttpClientRequest = new PwmHttpClientRequest( HttpMethod.POST, requestUrl, requestBody, headers );
         }

+ 6 - 6
server/src/main/java/password/pwm/http/servlet/resource/ResourceFileServlet.java

@@ -226,7 +226,7 @@ public class ResourceFileServlet extends HttpServlet implements PwmServlet
         {
             if ( contentType.startsWith( "text" ) || contentType.contains( "javascript" ) )
             {
-                final String acceptEncoding = pwmRequest.readHeaderValueAsString( HttpHeader.Accept_Encoding );
+                final String acceptEncoding = pwmRequest.readHeaderValueAsString( HttpHeader.AcceptEncoding );
                 acceptsGzip = acceptEncoding != null && accepts( acceptEncoding, "gzip" );
                 contentType += ";charset=UTF-8";
             }
@@ -254,7 +254,7 @@ public class ResourceFileServlet extends HttpServlet implements PwmServlet
 
         // Initialize response.
         addExpirationHeaders( resourceConfiguration, response );
-        response.setHeader( "ETag", resourceConfiguration.getNonceValue() );
+        response.setHeader(  HttpHeader.ETag.getHttpName(), resourceConfiguration.getNonceValue() );
         response.setContentType( contentType );
 
         try
@@ -345,7 +345,7 @@ public class ResourceFileServlet extends HttpServlet implements PwmServlet
                 if ( acceptsGzip )
                 {
                     final GZIPOutputStream gzipOutputStream = new GZIPOutputStream( tempOutputStream );
-                    headers.put( "Content-Encoding", "gzip" );
+                    headers.put( HttpHeader.ContentEncoding.getHttpName(), "gzip" );
                     copy( input, gzipOutputStream );
                     close( gzipOutputStream );
                 }
@@ -361,7 +361,7 @@ public class ResourceFileServlet extends HttpServlet implements PwmServlet
             }
 
             final byte[] entity = tempOutputStream.toByteArray();
-            headers.put( "Content-Length", String.valueOf( entity.length ) );
+            headers.put( HttpHeader.ContentLength.getHttpName(), String.valueOf( entity.length ) );
             cacheEntry = new CacheEntry( entity, headers );
         }
         else
@@ -407,7 +407,7 @@ public class ResourceFileServlet extends HttpServlet implements PwmServlet
             if ( acceptsGzip )
             {
                 // The browser accepts GZIP, so GZIP the content.
-                response.setHeader( "Content-Encoding", "gzip" );
+                response.setHeader( HttpHeader.ContentEncoding.getHttpName(), "gzip" );
                 output = new GZIPOutputStream( output );
             }
             else
@@ -416,7 +416,7 @@ public class ResourceFileServlet extends HttpServlet implements PwmServlet
                 // So only add it if there is no means of GZIP, else browser will hang.
                 if ( file.length() > 0 )
                 {
-                    response.setHeader( "Content-Length", String.valueOf( file.length() ) );
+                    response.setHeader( HttpHeader.ContentLength.getHttpName(), String.valueOf( file.length() ) );
                 }
             }
 

+ 15 - 1
server/src/main/java/password/pwm/i18n/Admin.java

@@ -63,7 +63,21 @@ public enum Admin implements PwmDisplayBundle
     EventLog_Narrative_HelpdeskVerifyToken,
     EventLog_Narrative_HelpdeskVerifyTokenIncorrect,
     EventLog_Narrative_HelpdeskVerifyAttributes,
-    EventLog_Narrative_HelpdeskVerifyAttributesIncorrect,;
+    EventLog_Narrative_HelpdeskVerifyAttributesIncorrect,
+
+    Field_Session_UserID,
+    Field_Session_LdapProfile,
+    Field_Session_UserDN,
+    Field_Session_CreateTime,
+    Field_Session_LastTime,
+    Field_Session_Label,
+    Field_Session_Idle,
+    Field_Session_SrcAddress,
+    Field_Session_Locale,
+    Field_Session_SrcHost,
+    Field_Session_LastURL,
+    Field_Session_IntruderAttempts,;
+
 
     public static final String STATISTICS_LABEL_PREFIX = "Statistic_Label.";
     public static final String STATISTICS_DESCRIPTION_PREFIX = "Statistic_Description.";

+ 16 - 10
server/src/main/java/password/pwm/ldap/LdapConnectionService.java

@@ -53,10 +53,12 @@ public class LdapConnectionService implements PwmService
 
     private final Map<LdapProfile, Map<Integer, ChaiProvider>> proxyChaiProviders = new ConcurrentHashMap<>();
     private final Map<LdapProfile, ErrorInformation> lastLdapErrors = new ConcurrentHashMap<>();
+
+    private boolean useThreadLocal;
     private PwmApplication pwmApplication;
     private STATUS status = STATUS.NEW;
     private AtomicLoopIntIncrementer slotIncrementer;
-    //private final ThreadLocal<Map<LdapProfile, ChaiProvider>> threadLocalProvider = new ThreadLocal<>();
+    private final ThreadLocal<Map<LdapProfile, ChaiProvider>> threadLocalProvider = new ThreadLocal<>();
     private ChaiProviderFactory chaiProviderFactory;
 
     public STATUS status( )
@@ -71,6 +73,8 @@ public class LdapConnectionService implements PwmService
 
         chaiProviderFactory = ChaiProviderFactory.newProviderFactory();
 
+        useThreadLocal = Boolean.parseBoolean( pwmApplication.getConfig().readAppProperty( AppProperty.LDAP_PROXY_USE_THREAD_LOCAL ) );
+
         // read the lastLoginTime
         this.lastLdapErrors.putAll( readLastLdapFailure( pwmApplication ) );
 
@@ -135,22 +139,24 @@ public class LdapConnectionService implements PwmService
                 ? pwmApplication.getConfig().getDefaultLdapProfile()
                 : ldapProfile;
 
-        /*
-        if ( threadLocalProvider.get() != null && threadLocalProvider.get().containsKey( effectiveProfile ) )
+        if ( useThreadLocal )
         {
-            return threadLocalProvider.get().get( effectiveProfile );
+            if ( threadLocalProvider.get() != null && threadLocalProvider.get().containsKey( effectiveProfile ) )
+            {
+                return threadLocalProvider.get().get( effectiveProfile );
+            }
         }
-        */
 
         final ChaiProvider chaiProvider = getNewProxyChaiProvider( effectiveProfile );
 
-        /*
-        if ( threadLocalProvider.get() == null )
+        if ( useThreadLocal )
         {
-            threadLocalProvider.set( new ConcurrentHashMap<>() );
+            if ( threadLocalProvider.get() == null )
+            {
+                threadLocalProvider.set( new ConcurrentHashMap<>() );
+            }
+            threadLocalProvider.get().put( effectiveProfile, chaiProvider );
         }
-        threadLocalProvider.get().put( effectiveProfile, chaiProvider );
-        */
 
         return chaiProvider;
     }

+ 0 - 9
server/src/main/java/password/pwm/ldap/ViewableUserInfoDisplayReader.java

@@ -216,15 +216,6 @@ public final class ViewableUserInfoDisplayReader
             }
         }
 
-        if ( userInfo.getResponseInfoBean() != null )
-        {
-            maker.add(
-                    ViewStatusFields.ResponsesTimestamp,
-                    Display.Field_ResponsesTimestamp,
-                    userInfo.getResponseInfoBean().getTimestamp()
-            );
-        }
-
         {
             maker.add(
                     ViewStatusFields.OTPStored,

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff