Ian Wright 8 年之前
父节点
当前提交
f57cc25a0e
共有 76 个文件被更改,包括 1073 次插入408 次删除
  1. 二进制
      local-maven-repo/com/novell/security/nmas/NMASToolkit/2013.04.26/NMASToolkit-2013.04.26.jar
  2. 二进制
      local-maven-repo/com/novell/security/nmas/ldap/2013.04.26/ldap-2013.04.26.jar
  3. 二进制
      local-maven-repo/com/novell/security/nmas/nmasclient/2013.04.26/nmasclient-2013.04.26.jar
  4. 0 8
      pom.xml
  5. 0 1
      src/main/angular/index.html
  6. 0 1
      src/main/angular/karma.conf.js
  7. 0 1
      src/main/angular/package.json
  8. 0 1
      src/main/angular/src/main.dev.ts
  9. 0 1
      src/main/angular/src/main.ts
  10. 2 0
      src/main/angular/src/models/person.model.ts
  11. 5 0
      src/main/angular/src/peoplesearch/orgchart-search.component.html
  12. 93 12
      src/main/angular/src/peoplesearch/orgchart.component.scss
  13. 35 38
      src/main/angular/src/peoplesearch/peoplesearch-base.component.ts
  14. 5 0
      src/main/angular/src/peoplesearch/peoplesearch-cards.component.html
  15. 18 1
      src/main/angular/src/peoplesearch/peoplesearch-cards.component.scss
  16. 40 4
      src/main/angular/src/peoplesearch/peoplesearch-cards.component.ts
  17. 5 0
      src/main/angular/src/peoplesearch/peoplesearch-table.component.html
  18. 19 5
      src/main/angular/src/peoplesearch/peoplesearch-table.component.ts
  19. 37 6
      src/main/angular/src/peoplesearch/person-card.component.scss
  20. 12 1
      src/main/angular/src/peoplesearch/person-details-dialog.component.html
  21. 8 3
      src/main/angular/src/peoplesearch/person-details-dialog.component.scss
  22. 0 4
      src/main/angular/src/services/people.service.dev.ts
  23. 1 22
      src/main/angular/src/services/people.service.ts
  24. 21 17
      src/main/angular/src/ux/app-bar.component.scss
  25. 1 1
      src/main/angular/src/ux/app-bar.component.ts
  26. 3 3
      src/main/angular/src/ux/auto-complete.component.scss
  27. 21 0
      src/main/angular/src/ux/dialog.component.scss
  28. 26 9
      src/main/angular/src/ux/icon-button.component.scss
  29. 5 3
      src/main/angular/src/ux/icon-button.component.ts
  30. 1 0
      src/main/angular/src/ux/search-bar.component.html
  31. 15 0
      src/main/angular/src/ux/search-bar.component.scss
  32. 16 7
      src/main/angular/src/ux/table.directive.scss
  33. 0 1
      src/main/angular/webpack.dev.js
  34. 3 1
      src/main/java/password/pwm/AppProperty.java
  35. 2 0
      src/main/java/password/pwm/PwmConstants.java
  36. 6 0
      src/main/java/password/pwm/config/PwmSetting.java
  37. 18 16
      src/main/java/password/pwm/config/option/IdentityVerificationMethod.java
  38. 107 0
      src/main/java/password/pwm/health/ApplianceStatusChecker.java
  39. 9 8
      src/main/java/password/pwm/health/HealthMonitor.java
  40. 1 0
      src/main/java/password/pwm/health/HealthTopic.java
  41. 19 0
      src/main/java/password/pwm/http/PwmHttpResponseWrapper.java
  42. 2 1
      src/main/java/password/pwm/http/PwmSession.java
  43. 11 0
      src/main/java/password/pwm/http/PwmURL.java
  44. 54 0
      src/main/java/password/pwm/http/bean/LoginServletBean.java
  45. 1 1
      src/main/java/password/pwm/http/filter/AuthenticationFilter.java
  46. 36 20
      src/main/java/password/pwm/http/servlet/LoginServlet.java
  47. 38 2
      src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java
  48. 36 0
      src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java
  49. 6 5
      src/main/java/password/pwm/http/servlet/oauth/OAuthConsumerServlet.java
  50. 57 0
      src/main/java/password/pwm/http/servlet/oauth/OAuthMachine.java
  51. 8 0
      src/main/java/password/pwm/http/servlet/oauth/OAuthSettings.java
  52. 46 0
      src/main/java/password/pwm/http/servlet/peoplesearch/LinkReferenceBean.java
  53. 31 3
      src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchDataReader.java
  54. 17 11
      src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchServlet.java
  55. 10 0
      src/main/java/password/pwm/http/servlet/peoplesearch/UserDetailBean.java
  56. 1 1
      src/main/java/password/pwm/http/state/CryptoCookieBeanImpl.java
  57. 9 0
      src/main/java/password/pwm/i18n/Display.java
  58. 41 0
      src/main/java/password/pwm/ldap/auth/SessionAuthenticator.java
  59. 4 0
      src/main/java/password/pwm/util/StringUtil.java
  60. 3 1
      src/main/resources/password/pwm/AppProperty.properties
  61. 21 3
      src/main/resources/password/pwm/config/PwmSetting.xml
  62. 8 0
      src/main/resources/password/pwm/i18n/Display.properties
  63. 1 0
      src/main/resources/password/pwm/i18n/Health.properties
  64. 18 127
      src/main/resources/password/pwm/i18n/PwmSetting.properties
  65. 1 0
      src/main/webapp/WEB-INF/jsp/forgottenpassword-method.jsp
  66. 5 5
      src/main/webapp/WEB-INF/jsp/fragment/header-body.jsp
  67. 1 1
      src/main/webapp/WEB-INF/jsp/fragment/header.jsp
  68. 0 3
      src/main/webapp/WEB-INF/jsp/login-passwordonly.jsp
  69. 0 2
      src/main/webapp/WEB-INF/jsp/login.jsp
  70. 0 1
      src/main/webapp/WEB-INF/jsp/peoplesearch.jsp
  71. 1 1
      src/main/webapp/public/index.jsp
  72. 46 39
      src/main/webapp/public/reference/environment.jsp
  73. 二进制
      src/main/webapp/public/resources/favicon.ico
  74. 二进制
      src/main/webapp/public/resources/favicon.png
  75. 3 3
      src/main/webapp/public/resources/js/configeditor.js
  76. 3 3
      src/test/resources/password/pwm/manual/TestHelper.properties

二进制
local-maven-repo/com/novell/security/nmas/NMASToolkit/2013.04.26/NMASToolkit-2013.04.26.jar


二进制
local-maven-repo/com/novell/security/nmas/ldap/2013.04.26/ldap-2013.04.26.jar


二进制
local-maven-repo/com/novell/security/nmas/nmasclient/2013.04.26/nmasclient-2013.04.26.jar


+ 0 - 8
pom.xml

@@ -289,9 +289,6 @@
                             <resources>
                                 <resource>
                                     <directory>src/main/angular/dist</directory>
-                                    <includes>
-                                        <include>*.ng.js*</include>
-                                    </includes>
                                 </resource>
                             </resources>
                         </configuration>
@@ -813,11 +810,6 @@
             <artifactId>angular-translate</artifactId>
             <version>2.13.0</version>
         </dependency>
-        <dependency>
-            <groupId>org.webjars.npm</groupId>
-            <artifactId>angular-sanitize</artifactId>
-            <version>1.5.8</version>
-        </dependency>
     </dependencies>
 
     <repositories>

+ 0 - 1
src/main/angular/index.html

@@ -18,7 +18,6 @@
 <ui-view>Loading...</ui-view>
 
 <script src="vendor/angular.js"></script>
-<script src="vendor/angular-sanitize.js"></script>
 <script src="vendor/angular-translate.js"></script>
 <script src="vendor/angular-ui-router.js"></script>
 </body>

+ 0 - 1
src/main/angular/karma.conf.js

@@ -38,7 +38,6 @@ module.exports = function (config) {
         files: [
             'node_modules/angular/angular.js',
             'node_modules/angular-mocks/angular-mocks.js',
-            'node_modules/angular-sanitize/angular-sanitize.js',
             'node_modules/angular-translate/dist/angular-translate.js',
             'vendor/angular-ui-router.js',
             'src/main.ts',

+ 0 - 1
src/main/angular/package.json

@@ -27,7 +27,6 @@
     "@types/node": "6.0.45",
     "angular": "1.5.8",
     "angular-mocks": "1.5.8",
-    "angular-sanitize": "1.5.8",
     "angular-translate": "2.13.0",
     "autoprefixer": "6.5.3",
     "copy-webpack-plugin": "3.0.1",

+ 0 - 1
src/main/angular/src/main.dev.ts

@@ -32,7 +32,6 @@ import uiRouter from 'angular-ui-router';
 require('./icons.json');
 
 module('app', [
-    'ngSanitize',
     uiRouter,
     peopleSearchModule,
     'pascalprecht.translate'

+ 0 - 1
src/main/angular/src/main.ts

@@ -34,7 +34,6 @@ import uiRouter from 'angular-ui-router';
 require('./icons.json');
 
 module('app', [
-    'ngSanitize',
     uiRouter,
     peopleSearchModule,
     'pascalprecht.translate'

+ 2 - 0
src/main/angular/src/models/person.model.ts

@@ -33,6 +33,7 @@ export default class Person {
     detail: any;
     displayNames: string[];
     photoURL: string;
+    links: any[];
 
     // Search properties (not available in details)
     givenName: string;
@@ -52,6 +53,7 @@ export default class Person {
         this.detail = options.detail;
         this.displayNames = options.displayNames;
         this.photoURL = options.photoURL;
+        this.links = options.links;
 
         // Search properties
         this.givenName = options.givenName;

+ 5 - 0
src/main/angular/src/peoplesearch/orgchart-search.component.html

@@ -18,6 +18,11 @@
             icon="view-list"
             ng-click="$ctrl.gotoSearchState('search.table')"
             ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"></mf-icon-button>
+    <div class="mf-divider vertical"></div>
+    <mf-icon-button
+            icon="orgchart"
+            disabled="true"
+            ng-attr-title="{{ 'Title_OrgChart' | translate }}"></mf-icon-button>
 </mf-app-bar>
 
 <org-chart person="$ctrl.person"

+ 93 - 12
src/main/angular/src/peoplesearch/orgchart.component.scss

@@ -32,15 +32,6 @@ org-chart {
   display: block;
   max-width: 100%;
 
-  > .org-chart-section {
-    width: 100%;
-
-    > person-card {
-      &[size="large"] {
-      }
-    }
-  }
-
   // (S) Too wide for full width person-card in direct reports
   &.small {
     > .org-chart-section {
@@ -67,7 +58,7 @@ org-chart {
   // (L) Wide enough to show main person offset to the right. Manager should now be locked in place (instead of centered)
   &.large {
     > .org-chart-section {
-      text-align: left;
+      text-align: start;
 
       > person-card {
         &[size="large"] {
@@ -88,7 +79,7 @@ org-chart {
         .manager {
           display: block;
           margin-left: 135px;
-          text-align: left;
+          text-align: start;
         }
       }
     }
@@ -169,6 +160,7 @@ org-chart {
   > .org-chart-section {
     position: relative;
     text-align: center;
+    width: 100%;
 
     &.direct-reports {
       > .org-chart-connector {
@@ -237,7 +229,7 @@ org-chart {
       line-height: 14px;
       margin: 0;
       padding: 15px 0 5px 0;
-      text-align: left;
+      text-align: start;
     }
 
     > person-card {
@@ -271,4 +263,93 @@ org-chart {
       width: 5px;
     }
   }
+}
+
+[dir="rtl"] {
+  // (XS) Default display
+  org-chart {
+    // (S) Too wide for full width person-card in direct reports
+    &.small {
+      > .org-chart-section {
+        > .person-card-list {
+          > person-card {
+            margin-left: 5px;
+            margin-right: auto;
+          }
+        }
+      }
+    }
+
+    // (L) Wide enough to show main person offset to the right. Manager should now be locked in place (instead of centered)
+    &.large {
+      > .org-chart-section {
+        > person-card {
+          &[size="large"] {
+            margin: 0 128px 0 0;
+          }
+        }
+
+        .org-chart-connector {
+          left: initial;
+          right: 172px;
+        }
+
+        &.managers {
+          .org-chart-connector {
+            left: initial;
+            right: 34px;
+          }
+
+          .manager {
+            margin-left: auto;
+            margin-right: 135px;
+          }
+        }
+      }
+    }
+
+    // (XL) Wide enough to display several managers horizontally
+    &.extra-large {
+      > .org-chart-section {
+        &.managers {
+          margin-left: auto;
+
+          > h3 {
+            left: initial;
+            right: 0;
+          }
+
+          .org-chart-connector {
+            left: initial;
+            right: 42px;
+          }
+
+          .manager {
+            margin-left: 5px;
+            margin-right: 0;
+
+            &:first-child {
+              margin-right: 115px;
+
+              > .org-chart-connector {
+                left: initial;
+                right: 57px;
+              }
+            }
+
+            &:not(:first-child) {
+              > .org-chart-connector {
+                left: initial;
+                right: -37px;
+              }
+            }
+
+            &:last-child {
+              margin-left: 0;
+            }
+          }
+        }
+      }
+    }
+  }
 }

+ 35 - 38
src/main/angular/src/peoplesearch/peoplesearch-base.component.ts

@@ -22,22 +22,18 @@
 
 
 import { IPeopleService } from '../services/people.service';
-import { isArray, isString, IPromise, IScope } from 'angular';
+import { isArray, isString, IPromise, IQService, IScope } from 'angular';
 import Person from '../models/person.model';
 import SearchResult from '../models/search-result.model';
 
-interface ISearchFunction {
-    (query: string): IPromise<SearchResult>;
-}
-
-export default class PeopleSearchBaseComponent {
+abstract class PeopleSearchBaseComponent {
     loading: boolean;
     query: string;
-    searchFunction: ISearchFunction;
     searchMessage: (string | IPromise<string>);
     searchResult: SearchResult;
 
-    protected constructor(protected $scope: IScope,
+    constructor(protected $q: IQService,
+                          protected $scope: IScope,
                           protected $state: angular.ui.IStateService,
                           protected $stateParams: angular.ui.IStateParamsService,
                           protected $translate: angular.translate.ITranslateService,
@@ -51,22 +47,6 @@ export default class PeopleSearchBaseComponent {
         this.$state.go(state, { query: this.query });
     }
 
-    initialize(searchFunction: ISearchFunction): void {
-        this.searchFunction = searchFunction;
-
-        // Read query from state parameters
-        var queryParameter = this.$stateParams['query'];
-        // If multiple query parameters are defined, use the first one
-        if (isArray(queryParameter)) {
-            this.query = queryParameter[0].trim();
-        }
-        else if (isString(queryParameter)) {
-            this.query = queryParameter.trim();
-        }
-
-        this.fetchData();
-    }
-
     onSearchBoxKeyDown(event: KeyboardEvent): void {
         switch (event.keyCode) {
             case 27: // ESC
@@ -99,29 +79,39 @@ export default class PeopleSearchBaseComponent {
             this.searchMessage = message;
         }
         else {
-            var self = this;
+            const self = this;
 
             message.then((translation: string) => {
                 self.searchMessage = translation;
-                // self.$scope.$apply();
             });
         }
     }
 
-    protected fetchData(): void {
+    protected clearSearch(): void {
+        this.query = null;
+        this.searchResult = null;
+        this.clearSearchMessage();
+    }
+
+    protected clearSearchMessage(): void  {
+        this.searchMessage = null;
+    }
+
+    abstract fetchData(): void;
+
+    protected fetchSearchData(): IPromise<SearchResult> {
         const self = this;
 
         if (!this.query) {
             this.clearSearch();
-            return;
+            return null;
         }
 
         this.loading = true;
 
-        this.searchFunction
-            .call(this.peopleService, this.query)
+        return this.peopleService
+            .search(this.query)
             .then((searchResult: SearchResult) => {
-                self.searchResult = searchResult;
                 self.clearSearchMessage();
 
                 // Too many results returned
@@ -132,19 +122,26 @@ export default class PeopleSearchBaseComponent {
                 if (!searchResult.people.length) {
                     self.setSearchMessage(self.$translate('Display_SearchResultsNone'));
                 }
+
+                return this.$q.resolve(searchResult);
             })
             .finally(() => {
                 self.loading = false;
             });
     }
 
-    private clearSearch(): void {
-        this.query = null;
-        this.searchResult = null;
-        this.clearSearchMessage();
-    }
+    protected initialize(): void {
+        // Read query from state parameters
+        const queryParameter = this.$stateParams['query'];
 
-    private clearSearchMessage(): void  {
-        this.searchMessage = null;
+        // If multiple query parameters are defined, use the first one
+        if (isArray(queryParameter)) {
+            this.query = queryParameter[0].trim();
+        }
+        else if (isString(queryParameter)) {
+            this.query = queryParameter.trim();
+        }
     }
 }
+
+export default PeopleSearchBaseComponent;

+ 5 - 0
src/main/angular/src/peoplesearch/peoplesearch-cards.component.html

@@ -5,10 +5,15 @@
                    on-key-down="$ctrl.onSearchBoxKeyDown($event)"
                    auto-focus></mf-search-bar>
     <span flex></span>
+    <mf-icon-button
+            icon="view-tile"
+            disabled="true"
+            ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"></mf-icon-button>
     <mf-icon-button
             icon="view-list"
             ng-click="$ctrl.gotoTableView()"
             ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"></mf-icon-button>
+    <div class="mf-divider vertical"></div>
     <mf-icon-button
             icon="orgchart"
             ng-click="$ctrl.gotoOrgchart()"

+ 18 - 1
src/main/angular/src/peoplesearch/peoplesearch-cards.component.scss

@@ -42,7 +42,9 @@ people-search-cards {
   &.large {
     > .people-search-component-content {
       > .person-card-list {
-        text-align: left;margin: 0;
+        text-align: start;
+        margin: 0;
+
         > person-card {
           display: inline-block;
           margin-right: 5px;
@@ -68,3 +70,18 @@ people-search-cards {
     }
   }
 }
+
+[dir="rtl"] {
+  people-search-cards {
+    &.large {
+      > .people-search-component-content {
+        .person-card-list {
+          > person-card {
+            margin-right: auto;
+            margin-left: 5px;
+          }
+        }
+      }
+    }
+  }
+}

+ 40 - 4
src/main/angular/src/peoplesearch/peoplesearch-cards.component.ts

@@ -22,10 +22,12 @@
 
 
 import { Component } from '../component';
+import ElementSizeService from '../ux/element-size.service';
+import { IAugmentedJQuery, IQService, IScope } from 'angular';
 import IPeopleService from '../services/people.service';
 import PeopleSearchBaseComponent from './peoplesearch-base.component';
-import { IAugmentedJQuery, IScope } from 'angular';
-import ElementSizeService from '../ux/element-size.service';
+import Person from '../models/person.model';
+import SearchResult from '../models/search-result.model';
 
 export enum PeopleSearchCardsSize {
     Small = 0,
@@ -40,6 +42,7 @@ export enum PeopleSearchCardsSize {
 export default class PeopleSearchCardsComponent extends PeopleSearchBaseComponent {
     static $inject = [
         '$element',
+        '$q',
         '$scope',
         '$state',
         '$stateParams',
@@ -48,13 +51,14 @@ export default class PeopleSearchCardsComponent extends PeopleSearchBaseComponen
         'PeopleService'
     ];
     constructor(private $element: IAugmentedJQuery,
+                $q: IQService,
                 $scope: IScope,
                 $state: angular.ui.IStateService,
                 $stateParams: angular.ui.IStateParamsService,
                 $translate: angular.translate.ITranslateService,
                 private elementSizeService: ElementSizeService,
                 peopleService: IPeopleService) {
-        super($scope, $state, $stateParams, $translate, peopleService);
+        super($q, $scope, $state, $stateParams, $translate, peopleService);
     }
 
     $onDestroy(): void {
@@ -62,11 +66,43 @@ export default class PeopleSearchCardsComponent extends PeopleSearchBaseComponen
     }
 
     $onInit(): void {
-        this.initialize(this.peopleService.cardSearch);
+        this.initialize();
+        this.fetchData();
+
         this.elementSizeService.watchWidth(this.$element, PeopleSearchCardsSize);
     }
 
     gotoTableView() {
         this.gotoState('search.table');
     }
+
+    fetchData() {
+        let searchResult = this.fetchSearchData();
+        if (searchResult) {
+            searchResult.then(this.onSearchResult.bind(this));
+        }
+    }
+
+    private onSearchResult(searchResult: SearchResult): void {
+        this.searchResult = new SearchResult({
+            sizeExceeded: searchResult.sizeExceeded,
+            searchResults: []
+        });
+
+        let self = this;
+
+        searchResult.people.forEach(
+            (person: Person) => {
+                this.peopleService
+                    .getPerson(person.userKey)
+                    .then((person: Person) => {
+                        // searchResult may be overwritten by ESC->[LETTER] typed in after a search
+                        // has started but before all calls to peopleService.getPerson have resolved
+                        if (self.searchResult) {
+                            self.searchResult.people.push(person);
+                        }
+                    });
+            },
+            this);
+    }
 }

+ 5 - 0
src/main/angular/src/peoplesearch/peoplesearch-table.component.html

@@ -9,6 +9,11 @@
             icon="view-tile"
             ng-click="$ctrl.gotoCardsView()"
             ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"></mf-icon-button>
+    <mf-icon-button
+            icon="view-list"
+            disabled="true"
+            ng-attr-title="{{ 'Title_PeopleSearch' | translate }}"></mf-icon-button>
+    <div class="mf-divider vertical"></div>
     <mf-icon-button
             icon="orgchart"
             ng-click="$ctrl.gotoOrgchart()"

+ 19 - 5
src/main/angular/src/peoplesearch/peoplesearch-table.component.ts

@@ -25,7 +25,8 @@ import { Component } from '../component';
 import { IConfigService } from '../services/config.service';
 import IPeopleService from '../services/people.service';
 import PeopleSearchBaseComponent from './peoplesearch-base.component';
-import { IScope } from 'angular';
+import { IQService, IScope } from 'angular';
+import SearchResult from '../models/search-result.model';
 
 @Component({
     stylesheetUrl: require('peoplesearch/peoplesearch-table.component.scss'),
@@ -34,18 +35,20 @@ import { IScope } from 'angular';
 export default class PeopleSearchTableComponent extends PeopleSearchBaseComponent {
     columnConfiguration: any;
 
-    static $inject = [ '$scope', '$state', '$stateParams', '$translate', 'ConfigService', 'PeopleService' ];
-    constructor($scope: IScope,
+    static $inject = [ '$q', '$scope', '$state', '$stateParams', '$translate', 'ConfigService', 'PeopleService' ];
+    constructor($q: IQService,
+                $scope: IScope,
                 $state: angular.ui.IStateService,
                 $stateParams: angular.ui.IStateParamsService,
                 $translate: angular.translate.ITranslateService,
                 private configService: IConfigService,
                 peopleService: IPeopleService) {
-        super($scope, $state, $stateParams, $translate, peopleService);
+        super($q, $scope, $state, $stateParams, $translate, peopleService);
     }
 
     $onInit(): void {
-        this.initialize(this.peopleService.search);
+        this.initialize();
+        this.fetchData();
 
         let self = this;
 
@@ -58,4 +61,15 @@ export default class PeopleSearchTableComponent extends PeopleSearchBaseComponen
     gotoCardsView() {
         this.gotoState('search.cards');
     }
+
+    fetchData() {
+        let searchResult = this.fetchSearchData();
+        if (searchResult) {
+            searchResult.then(this.onSearchResult.bind(this));
+        }
+    }
+
+    private onSearchResult(searchResult: SearchResult): void {
+        this.searchResult = searchResult;
+    }
 }

+ 37 - 6
src/main/angular/src/peoplesearch/person-card.component.scss

@@ -43,7 +43,7 @@ person-card {
   height: $person-card-height;
   padding: $person-card-spacing;
   position: relative;
-  text-align: left;
+  text-align: start;
   vertical-align: top;
   width: $person-card-width;
 
@@ -72,12 +72,7 @@ person-card {
       > .avatar {
         flex: 0 0 $person-card-large-avatar-size;
         height: $person-card-large-avatar-size;
-        margin-right: 15px;
         width: $person-card-large-avatar-size;
-
-        img {
-          width: 100%;
-        }
       }
     }
   }
@@ -201,3 +196,39 @@ person-card {
     }
   }
 }
+
+[dir="rtl"] {
+  person-card {
+    &[size="large"] {
+      > .person-card-content {
+        > .avatar {
+        }
+      }
+    }
+
+    &[size="small"] {
+      > .person-card-content {
+        > .avatar {
+          margin: 0 auto;
+        }
+
+        > .reports {
+          left: 20px;
+          right: auto;
+        }
+      }
+    }
+
+    > .person-card-content {
+      > .avatar {
+        margin-left: $person-card-spacing;
+        margin-right: 0;
+      }
+
+      > .reports {
+        left: 3px;
+        right: initial;
+      }
+    }
+  }
+}

+ 12 - 1
src/main/angular/src/peoplesearch/person-details-dialog.component.html

@@ -15,6 +15,13 @@
         <!-- Details -->
         <table>
             <tbody>
+                <tr ng-repeat="reference in $ctrl.person.links">
+                    <td colspan="2">
+                        <div class="detail-link">
+                            <a ng-href="{{reference.link}}"><span ng-bind="reference.name"></span></a>
+                        </div>
+                    </td>
+                </tr>
                 <tr ng-repeat="(key, detail) in $ctrl.person.detail">
                     <td ng-bind="detail.label"></td>
                     <td ng-switch="detail.type">
@@ -33,8 +40,12 @@
                                        ng-bind="value"
                                        ng-if="detail.type === 'email'"
                                        flex></a>
+                                    <a ng-href="tel:{{value}}"
+                                       ng-bind="value"
+                                       ng-if="detail.type === 'tel'"
+                                       flex></a>
                                     <span ng-bind="value"
-                                          ng-if="detail.type !== 'email'"
+                                          ng-if="detail.type !== 'email' && detail.type !== 'tel'"
                                           flex></span>
                                     <mf-icon-button icon="magnify"
                                                     ng-click="$ctrl.searchText(value)"

+ 8 - 3
src/main/angular/src/peoplesearch/person-details-dialog.component.scss

@@ -21,6 +21,11 @@
  */
 
 
+.detail-link {
+  text-align: center;
+  width:100%;
+}
+
 .person-details {
   mf-app-bar {
     padding: 0 5px;
@@ -41,15 +46,16 @@
           font-size: 12px;
           height: 19px;
           padding: 3px 5px;
-          text-align: left;
+          text-align: start;
 
           &:first-child {
             color: #949494;
-            text-align: right;
+            text-align: end;
             padding: 3px 15px;
           }
 
           &:last-child {
+
             > .detail-container {
               a {
                 cursor: pointer;
@@ -92,4 +98,3 @@
     }
   }
 }
-

+ 0 - 4
src/main/angular/src/services/people.service.dev.ts

@@ -75,10 +75,6 @@ export default class PeopleService implements IPeopleService {
             });
     }
 
-    cardSearch(query: string): angular.IPromise<SearchResult> {
-        return this.search(query);
-    }
-
     getDirectReports(id: string): angular.IPromise<Person[]> {
         const people = this.findDirectReports(id);
 

+ 1 - 22
src/main/angular/src/services/people.service.ts

@@ -29,7 +29,6 @@ import SearchResult from '../models/search-result.model';
 
 export interface IPeopleService {
     autoComplete(query: string): IPromise<Person[]>;
-    cardSearch(query: string): IPromise<SearchResult>;
     getDirectReports(personId: string): IPromise<Person[]>;
     getNumberOfDirectReports(personId: string): IPromise<number>;
     getManagementChain(personId: string): IPromise<Person[]>;
@@ -56,26 +55,6 @@ export default class PeopleService implements IPeopleService {
             });
     }
 
-    cardSearch(query: string): angular.IPromise<SearchResult> {
-        let self = this;
-
-        return this.search(query)
-            .then((searchResult: SearchResult) => {
-                let sizeExceeded = searchResult.sizeExceeded;
-
-                let peoplePromises: IPromise<Person>[] = searchResult.people.map((person: Person) => {
-                    return self.getPerson(person.userKey);
-                });
-
-                return this.$q
-                    .all(peoplePromises)
-                    .then((people: Person[]) => {
-                        let searchResult = new SearchResult({ sizeExceeded: sizeExceeded, searchResults: people });
-                        return this.$q.resolve(searchResult);
-                    });
-            });
-    }
-
     getDirectReports(id: string): IPromise<Person[]> {
         return this.getOrgChartData(id).then((orgChartData: OrgChartData) => {
             let people: Person[] = [];
@@ -144,7 +123,7 @@ export default class PeopleService implements IPeopleService {
     search(query: string, params?: any): IPromise<SearchResult> {
         return this.$http
             .get(
-                this.pwmService.getServerUrl('search', { 'includeDisplayName': true }),
+                this.pwmService.getServerUrl('search', params),
                 { cache: true, params: { username: query } }
             )
             .then((response) => {

+ 21 - 17
src/main/angular/src/ux/app-bar.component.scss

@@ -21,7 +21,8 @@
  */
 
 
-$mf-app-bar-height: 26px;
+$mf-app-bar-height: 29px;
+$mf-app-bar-icon-size: 24px;
 
 mf-app-bar {
   display: block;
@@ -32,45 +33,48 @@ mf-app-bar {
     > .mf-app-bar-content {
       > mf-auto-complete,
       > mf-search-bar {
-        margin: 2px 10px 2px 0;
+        margin: 0 10px;
         max-width: 320px;
-        flex: 1 1 255px;
+        flex: 1000 1;
         order: initial;
         width: auto;
       }
-
-      > mf-icon-button,
-      > span[flex],
-      > .page-content-title {
-        order: initial;
-      }
     }
   }
 
   > .mf-app-bar-content {
     display: flex;
     flex-flow: row wrap;
+    line-height: $mf-app-bar-height;
 
     > mf-auto-complete,
     > mf-search-bar {
-      order: 3;
+      height: $mf-app-bar-height;
+      margin-top: 5px;
+      order: 1;
       width: 100%;
     }
 
-    > mf-icon-button {
-      order: 2;
-    }
-
     > span[flex] {
       flex: 1 1;
-      order: 1;
     }
 
     > .page-content-title {
       height: $mf-app-bar-height;
       line-height: $mf-app-bar-height;
-      margin-right: 10px;
-      order: 0;
+    }
+
+    > mf-icon-button {
+      margin-top: ($mf-app-bar-height - $mf-app-bar-icon-size) / 2;
+    }
+  }
+
+  .mf-divider {
+    &.vertical {
+      background-color: transparentize(#808080, .50);
+      height: $mf-app-bar-height;
+      margin: 0 5px;
+      width: 1px;
     }
   }
 }

+ 1 - 1
src/main/angular/src/ux/app-bar.component.ts

@@ -26,7 +26,7 @@ import { IAugmentedJQuery } from 'angular';
 import ElementSizeService from './element-size.service';
 
 export enum AppBarSize {
-    Large = 415
+    Large = 413
 }
 
 @Component({

+ 3 - 3
src/main/angular/src/ux/auto-complete.component.scss

@@ -27,15 +27,15 @@ mf-auto-complete {
   position: relative;
 
   > mf-search-bar {
-    width: 100%;
+    height: 100%;
     max-width: 100%;
+    width: 100%;
   }
 
   > .results {
     background-color: white;
     bottom: 0;
     box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .26);
-    left: 0;
     list-style: none;
     margin: 0;
     max-height: 240px;
@@ -43,7 +43,6 @@ mf-auto-complete {
     overflow-y: auto;
     padding: 0;
     position: absolute;
-    text-align: left;
     transform: translateY(100%);
     width: 100%;
     z-index: 100;
@@ -56,6 +55,7 @@ mf-auto-complete {
       overflow: hidden;
       padding: 0 10px;
       text-overflow: ellipsis;
+      text-align: start;
       white-space: nowrap;
       width: 100%;
 

+ 21 - 0
src/main/angular/src/ux/dialog.component.scss

@@ -87,4 +87,25 @@ mf-dialog {
       max-width: 514px;
     }
   }
+}
+
+[dir="rtl"] {
+  mf-dialog {
+    > .mf-dialog-container {
+      > mf-icon-button {
+        right: auto;
+        left: 1px;
+      }
+    }
+  }
+
+  @media (min-width: 420px) {
+    mf-dialog {
+      > .mf-dialog-container {
+        left: auto;
+        right: 50%;
+        transform: translate(50%, -50%);
+      }
+    }
+  }
 }

+ 26 - 9
src/main/angular/src/ux/icon-button.component.scss

@@ -22,6 +22,7 @@
 
 
 $mf-icon-button-color: #808080;
+$mf-icon-button-disabled-color: transparentize($mf-icon-button-color, .50);
 $mf-icon-button-size: 24px;
 $mf-icon-button-bg-color: transparent;
 
@@ -39,26 +40,42 @@ mf-icon-button {
   height: $mf-icon-button-size;
   width: $mf-icon-button-size;
 
+  &:not([disabled]) {
+    > button {
+      cursor: inherit;
+
+      &:focus,
+      &:hover {
+        background-color: $mf-icon-button-hover-bg-color;
+        border-color: #dae1e1;
+        color: $mf-icon-button-hover-color;
+        outline: none;
+      }
+    }
+  }
+
+  &[disabled] {
+    color: $mf-icon-button-disabled-color;
+
+    > button {
+      &:focus,
+      &:hover {
+        outline: none;
+      }
+    }
+  }
+
   > button {
     background: transparent none;
     border: 1px solid transparent;
     border-radius: 2px;
     color: inherit;
-    cursor: inherit;
     display: block;
     height: 100%;
     padding: 0;
     margin: 0;
     width: 100%;
 
-    &:focus,
-    &:hover {
-      background-color: $mf-icon-button-hover-bg-color;
-      border-color: #dae1e1;
-      color: $mf-icon-button-hover-color;
-      outline: none;
-    }
-
     > mf-icon {
       height: 100%;
       width: 100%;

+ 5 - 3
src/main/angular/src/ux/icon-button.component.ts

@@ -25,9 +25,11 @@ import { Component } from '../component';
 
 @Component({
     bindings: {
-        icon: '@'
+        icon: '@',
+        disabled: '<'
     },
     stylesheetUrl: require('ux/icon-button.component.scss'),
-    template: `<button type="button"><mf-icon icon="{{$ctrl.icon}}"></mf-icon></button>`
+    template: `<button type="button" ng-disabled="$ctrl.disabled"><mf-icon icon="{{$ctrl.icon}}"></mf-icon></button>`
 })
-export default class IconButtonComponent {}
+export default class IconButtonComponent {
+}

+ 1 - 0
src/main/angular/src/ux/search-bar.component.html

@@ -4,6 +4,7 @@
        ng-focus="$ctrl.onFocus({ $event: $event })"
        ng-keydown="$ctrl.onKeyDown({ $event: $event })"
        ng-model="$ctrl.searchText"
+       ng-model-options="{ debounce: 300 }"
        ng-attr-placeholder="{{ ('Placeholder_Search' | translate) }}"
        title="Search Box"
        autocomplete="off" />

+ 15 - 0
src/main/angular/src/ux/search-bar.component.scss

@@ -32,6 +32,7 @@ mf-search-bar {
     border: 1px solid #dae1e1;
     border-radius: 2px;
     box-sizing: border-box;
+    display: block;
     flex: 1 1 100%;
     height: 100%;
     line-height: 100%;
@@ -76,4 +77,18 @@ mf-search-bar {
     transform: translateY(-50%);
     width: $mf-search-bar-height - 2px;
   }
+}
+
+[dir="rtl"] {
+  mf-search-bar {
+    > .clear-input {
+      right: auto;
+      left: 1px;
+    }
+
+    > .search-icon {
+      left: auto;
+      right: 0;
+    }
+  }
 }

+ 16 - 7
src/main/angular/src/ux/table.directive.scss

@@ -42,7 +42,7 @@ mf-table {
       margin: 0;
       padding: 10px;
       position: absolute;
-      text-align: left;
+      text-align: start;
       top: 0;
 
       > li {
@@ -81,7 +81,7 @@ mf-table {
       font-weight: normal;
       overflow: hidden;
       padding: 5px;
-      text-align: left;
+      text-align: start;
       vertical-align: top;
     }
 
@@ -110,11 +110,20 @@ mf-table {
     }
 
     tr {
-      &:not(:last-child) {
-        > th,
-        > td {
-          border-bottom: 1px solid #dae1e1;
-        }
+      > th,
+      > td {
+        border-bottom: 1px solid #dae1e1;
+      }
+    }
+  }
+}
+
+[dir="rtl"] {
+  mf-table {
+    > .table-configuration {
+      > ul {
+        left: auto;
+        right: 26px;
       }
     }
   }

+ 0 - 1
src/main/angular/webpack.dev.js

@@ -47,7 +47,6 @@ module.exports = webpackMerge(commonConfig, {
         new CopyWebpackPlugin([
             { from: 'vendor/angular-ui-router.js', to: 'vendor/' },
             { from: 'node_modules/angular/angular.js', to: 'vendor/' },
-            { from: 'node_modules/angular-sanitize/angular-sanitize.js', to: 'vendor/' },
             { from: 'node_modules/angular-translate/dist/angular-translate.js', to: 'vendor/' },
             { from: 'images/avatars', to: 'images/avatars' }
         ])

+ 3 - 1
src/main/java/password/pwm/AppProperty.java

@@ -29,7 +29,7 @@ import java.util.ResourceBundle;
  * by an associated {@code AppProperty.properties} file.  Properties can be overridden by the application administrator in
  * the configuration using the setting {@link password.pwm.config.PwmSetting#APP_PROPERTY_OVERRIDES}.
  */
-public enum AppProperty {
+public enum     AppProperty {
 
     APPLICATION_FILELOCK_FILENAME                   ("application.fileLock.filename"),
     APPLICATION_FILELOCK_WAIT_SECONDS               ("application.fileLock.waitSeconds"),
@@ -215,6 +215,7 @@ public enum AppProperty {
     PEOPLESEARCH_DISPLAYNAME_USEALLMACROS           ("peoplesearch.displayName.enableAllMacros"),
     PEOPLESEARCH_MAX_VALUE_VERIFYUSERDN             ("peoplesearch.values.verifyUserDN"),
     PEOPLESEARCH_VALUE_MAXCOUNT                     ("peoplesearch.values.maxCount"),
+    PEOPLESEARCH_VIEW_DETAIL_LINKS                  ("peoplesearch.view.detail.links"),
     QUEUE_EMAIL_RETRY_TIMEOUT_MS                    ("queue.email.retryTimeoutMs"),
     QUEUE_EMAIL_MAX_AGE_MS                          ("queue.email.maxAgeMs"),
     QUEUE_EMAIL_MAX_COUNT                           ("queue.email.maxCount"),
@@ -234,6 +235,7 @@ public enum AppProperty {
     SECURITY_HTTPSSERVER_SELF_FUTURESECONDS         ("security.httpsServer.selfCert.futureSeconds"),
     SECURITY_HTTPSSERVER_SELF_ALG                   ("security.httpsServer.selfCert.alg"),
     SECURITY_HTTPSSERVER_SELF_KEY_SIZE              ("security.httpsServer.selfCert.keySize"),
+    SECURITY_LOGIN_HIDDEN_ERROR_TYPES               ("security.login.hiddenErrorTypes"),
     SECURITY_RESPONSES_HASH_ITERATIONS              ("security.responses.hashIterations"),
     SECURITY_INPUT_TRIM                             ("security.input.trim"),
     SECURITY_INPUT_PASSWORD_TRIM                    ("security.input.password.trim"),

+ 2 - 0
src/main/java/password/pwm/PwmConstants.java

@@ -374,6 +374,8 @@ public abstract class PwmConstants {
         XSessionID("X-" + PwmConstants.PWM_APP_NAME + "-SessionID"),
         XNoise("X-" + PwmConstants.PWM_APP_NAME + "-Noise"),
 
+        SsprAuthorizationToken("sspr-authorization-token"),
+
         ;
 
         private final String httpName;

+ 6 - 0
src/main/java/password/pwm/config/PwmSetting.java

@@ -317,6 +317,10 @@ public enum PwmSetting {
             "email.intruderNotice", PwmSettingSyntax.EMAIL, PwmSettingCategory.EMAIL_TEMPLATES),
     EMAIL_DELETEACCOUNT(
             "email.deleteAccount", PwmSettingSyntax.EMAIL, PwmSettingCategory.EMAIL_TEMPLATES),
+    EMAIL_HELPDESK_UNLOCK(
+            "email.helpdesk.unlock", PwmSettingSyntax.EMAIL, PwmSettingCategory.EMAIL_TEMPLATES),
+    EMAIL_UNLOCK(
+            "email.unlock", PwmSettingSyntax.EMAIL, PwmSettingCategory.EMAIL_TEMPLATES),
 
     EMAIL_ADVANCED_SETTINGS(
             "email.smtp.advancedSettings", PwmSettingSyntax.STRING_ARRAY, PwmSettingCategory.EMAIL_SETTINGS),
@@ -746,6 +750,8 @@ public enum PwmSetting {
             "recovery.oauth.idserver.secret", PwmSettingSyntax.PASSWORD, PwmSettingCategory.RECOVERY_OAUTH),
     RECOVERY_OAUTH_ID_DN_ATTRIBUTE_NAME(
             "recovery.oauth.idserver.dnAttributeName", PwmSettingSyntax.STRING, PwmSettingCategory.RECOVERY_OAUTH),
+    RECOVERY_OAUTH_ID_USERNAME_SEND_VALUE(
+            "recovery.oauth.idserver.usernameSendValue", PwmSettingSyntax.STRING, PwmSettingCategory.RECOVERY_OAUTH),
 
 
     // forgotten username

+ 18 - 16
src/main/java/password/pwm/config/option/IdentityVerificationMethod.java

@@ -31,35 +31,37 @@ import java.util.List;
 import java.util.Locale;
 
 public enum IdentityVerificationMethod implements ConfigurationOption {
-    PREVIOUS_AUTH(      false,  Display.Field_VerificationMethodPreviousAuth),
-    ATTRIBUTES(         true,   Display.Field_VerificationMethodAttributes),
-    CHALLENGE_RESPONSES(true,   Display.Field_VerificationMethodChallengeResponses),
-    TOKEN(              true,   Display.Field_VerificationMethodToken),
-    OTP(                true,   Display.Field_VerificationMethodOTP),
-    REMOTE_RESPONSES(   false,  Display.Field_VerificationMethodRemoteResponses),
-    NAAF(               true,   Display.Field_VerificationMethodNAAF),
-    OAUTH(              true,   Display.Field_VerificationMethodOAuth),
+    PREVIOUS_AUTH(      false,  Display.Field_VerificationMethodPreviousAuth,       Display.Description_VerificationMethodPreviousAuth),
+    ATTRIBUTES(         true,   Display.Field_VerificationMethodAttributes,         Display.Description_VerificationMethodAttributes),
+    CHALLENGE_RESPONSES(true,   Display.Field_VerificationMethodChallengeResponses, Display.Description_VerificationMethodChallengeResponses),
+    TOKEN(              true,   Display.Field_VerificationMethodToken,              Display.Description_VerificationMethodToken),
+    OTP(                true,   Display.Field_VerificationMethodOTP,                Display.Description_VerificationMethodOTP),
+    REMOTE_RESPONSES(   false,  Display.Field_VerificationMethodRemoteResponses,    Display.Description_VerificationMethodRemoteResponses),
+    NAAF(               true,   Display.Field_VerificationMethodNAAF,               Display.Description_VerificationMethodNAAF),
+    OAUTH(              true,   Display.Field_VerificationMethodOAuth,              Display.Description_VerificationMethodOAuth),
 
     ;
     
     private final boolean userSelectable;
-    private final Display displayKey;
+    private final Display labelKey;
+    private final Display descriptionKey;
 
-    IdentityVerificationMethod(final boolean userSelectable, final Display displayKey) {
+    IdentityVerificationMethod(final boolean userSelectable, final Display labelKey, final Display descriptionKey) {
         this.userSelectable = userSelectable;
-        this.displayKey = displayKey;
+        this.labelKey = labelKey;
+        this.descriptionKey = descriptionKey;
     }
 
     public boolean isUserSelectable() {
         return userSelectable;
     }
-    
-    public Display getDisplayKey() {
-        return displayKey;
-    }
 
     public String getLabel(final Configuration configuration, final Locale locale) {
-        return Display.getLocalizedMessage(locale, this.getDisplayKey(), configuration);
+        return Display.getLocalizedMessage(locale, this.labelKey, configuration);
+    }
+
+    public String getDescription(final Configuration configuration, final Locale locale) {
+        return Display.getLocalizedMessage(locale, this.descriptionKey, configuration);
     }
 
     public static IdentityVerificationMethod[] availableValues() {

+ 107 - 0
src/main/java/password/pwm/health/ApplianceStatusChecker.java

@@ -0,0 +1,107 @@
+package password.pwm.health;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.util.EntityUtils;
+import password.pwm.PwmApplication;
+import password.pwm.PwmConstants;
+import password.pwm.PwmEnvironment;
+import password.pwm.http.client.PwmHttpClient;
+import password.pwm.http.client.PwmHttpClientConfiguration;
+import password.pwm.util.FileSystemUtility;
+import password.pwm.util.JsonUtil;
+import password.pwm.util.logging.PwmLogger;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class ApplianceStatusChecker implements HealthChecker {
+    private static final PwmLogger LOGGER = PwmLogger.forClass(ApplianceStatusChecker.class);
+
+    private static final String DEFAULT_DOCKER_HOST = "172.17.0.1";
+    private static final String TOKEN_FILE = "/config/token";
+    private static final int APPLIANCE_PORT = 9443;
+
+    private String applianceHost;
+    private String applianceAccessToken;
+
+    private static class UpdateStatus {
+        boolean updatesReadyForInstall;
+        boolean updateNowEnabled;
+        boolean updateServiceConfigured;
+    }
+
+    @Override
+    public List<HealthRecord> doHealthCheck(final PwmApplication pwmApplication) {
+        final boolean isApplianceAvailable = pwmApplication.getPwmEnvironment().getFlags().contains(PwmEnvironment.ApplicationFlag.Appliance);
+        if (isApplianceAvailable) {
+            try {
+                final List<HealthRecord> healthRecords = new ArrayList<>();
+
+                final String applianceHost = getApplianceHost(pwmApplication);
+                final String applianceAccessToken = getApplianceAccessToken();
+
+                final HttpGet httpGet = new HttpGet(String.format("https://%s:%d/sspr/appliance-update-status", applianceHost, APPLIANCE_PORT));
+                httpGet.setHeader(PwmConstants.HttpHeader.SsprAuthorizationToken.getHttpName(), applianceAccessToken);
+
+                final PwmHttpClientConfiguration.Builder builder = new PwmHttpClientConfiguration.Builder();
+                builder.setPromiscuous(true);
+
+                final HttpResponse httpResponse = PwmHttpClient.getHttpClient(pwmApplication.getConfig(), builder.create()).execute(httpGet);
+                final String jsonString = EntityUtils.toString(httpResponse.getEntity());
+                LOGGER.debug("Response from /sspr/appliance-update-status: " + jsonString);
+
+                final UpdateStatus updateStatus = JsonUtil.deserialize(jsonString, UpdateStatus.class);
+
+                if (updateStatus.updatesReadyForInstall) {
+                    healthRecords.add(new HealthRecord(HealthStatus.WARN, HealthTopic.Appliance, "Appliance security updates are pending installation."));
+                }
+
+                if (!updateStatus.updateNowEnabled) {
+                    healthRecords.add(new HealthRecord(HealthStatus.CAUTION, HealthTopic.Appliance, "Appliance auto-updates are not enabled."));
+                }
+
+                if (!updateStatus.updateServiceConfigured) {
+                    healthRecords.add(new HealthRecord(HealthStatus.CAUTION, HealthTopic.Appliance, "Appliance update service has not been configured."));
+                }
+
+                return Collections.unmodifiableList(healthRecords);
+            } catch (Exception e) {
+                LOGGER.error("An error occurred checking appliance status: " + e.getMessage(), e);
+                return Arrays.asList(new HealthRecord(HealthStatus.WARN, HealthTopic.Appliance, "An error occurred checking appliance update status: " + e.getMessage()));
+            }
+        }
+
+        return Collections.emptyList();
+    }
+
+    private String getApplianceAccessToken() throws IOException {
+        if (applianceAccessToken == null) {
+            applianceAccessToken = StringUtils.trim(FileUtils.readFileToString(new File(TOKEN_FILE)));
+        }
+
+        return applianceAccessToken;
+    }
+
+    private String getApplianceHost(final PwmApplication pwmApplication) {
+        if (applianceHost == null) {
+            try {
+                // The file: /usr/local/tomcat/webapps/sspr/WEB-INF/appliance-host gets created during the docker startup command (see Docker/startup.sh in the SSPR project)
+                final File applianceHostFile = FileSystemUtility.figureFilepath("appliance-host", pwmApplication.getPwmEnvironment().getApplicationPath());
+                applianceHost = FileUtils.readFileToString(applianceHostFile);
+            } catch (IOException e) {
+                LOGGER.error("Unable to read the hostname for the docker host, using default: " + DEFAULT_DOCKER_HOST, e);
+                applianceHost = DEFAULT_DOCKER_HOST;
+            }
+        }
+
+        return applianceHost;
+    }
+
+}

+ 9 - 8
src/main/java/password/pwm/health/HealthMonitor.java

@@ -22,14 +22,6 @@
 
 package password.pwm.health;
 
-import password.pwm.PwmApplication;
-import password.pwm.config.option.DataStorageMethod;
-import password.pwm.error.PwmException;
-import password.pwm.svc.PwmService;
-import password.pwm.util.Helper;
-import password.pwm.util.TimeDuration;
-import password.pwm.util.logging.PwmLogger;
-
 import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -45,6 +37,14 @@ import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
+import password.pwm.PwmApplication;
+import password.pwm.config.option.DataStorageMethod;
+import password.pwm.error.PwmException;
+import password.pwm.svc.PwmService;
+import password.pwm.util.Helper;
+import password.pwm.util.TimeDuration;
+import password.pwm.util.logging.PwmLogger;
+
 public class HealthMonitor implements PwmService {
     private static final PwmLogger LOGGER = PwmLogger.forClass(HealthMonitor.class);
 
@@ -61,6 +61,7 @@ public class HealthMonitor implements PwmService {
         records.add(new ConfigurationChecker());
         records.add(new LocalDBHealthChecker());
         records.add(new CertificateChecker());
+        records.add(new ApplianceStatusChecker());
         HEALTH_CHECKERS = records;
     }
 

+ 1 - 0
src/main/java/password/pwm/health/HealthTopic.java

@@ -28,6 +28,7 @@ import password.pwm.util.LocaleHelper;
 import java.util.Locale;
 
 public enum HealthTopic {
+    Appliance,
     Application,
     Configuration,
     LDAP,

+ 19 - 0
src/main/java/password/pwm/http/PwmHttpResponseWrapper.java

@@ -26,6 +26,7 @@ import password.pwm.AppProperty;
 import password.pwm.PwmConstants;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
+import password.pwm.http.servlet.PwmServletDefinition;
 import password.pwm.util.StringUtil;
 import password.pwm.util.Validator;
 import password.pwm.util.logging.PwmLogger;
@@ -50,6 +51,7 @@ public class PwmHttpResponseWrapper {
         Application,
         Private,
         CurrentURL,
+        PwmServlet,
 
         ;
 
@@ -64,6 +66,9 @@ public class PwmHttpResponseWrapper {
                 case CurrentURL:
                     return httpServletRequest.getRequestURI();
 
+                case PwmServlet:
+                    return determinePwmServletPath(httpServletRequest);
+
                 default:
                     throw new IllegalStateException("undefined CookiePath type: " + this);
             }
@@ -71,6 +76,20 @@ public class PwmHttpResponseWrapper {
         }
     }
 
+    private static String determinePwmServletPath(final HttpServletRequest httpServletRequest) {
+        final String context = httpServletRequest.getContextPath();
+        final String requestPath = httpServletRequest.getRequestURI();
+        for (final PwmServletDefinition servletDefinition : PwmServletDefinition.values()) {
+            for (final String pattern : servletDefinition.urlPatterns()) {
+                final String testPath = context + pattern;
+                if (requestPath.startsWith(testPath)) {
+                    return testPath;
+                }
+            }
+        }
+        return requestPath;
+    }
+
     public enum Flag {
         NonHttpOnly,
         BypassSanitation,

+ 2 - 1
src/main/java/password/pwm/http/PwmSession.java

@@ -243,11 +243,11 @@ public class PwmSession implements Serializable {
             throw new PwmUnrecoverableException(new ErrorInformation(PwmError.ERROR_APP_UNAVAILABLE, "unable to read context manager"));
         }
 
+        final LocalSessionStateBean ssBean = this.getSessionStateBean();
         final List<Locale> knownLocales = pwmApplication.getConfig().getKnownLocales();
         final Locale requestedLocale = LocaleHelper.parseLocaleString(localeString);
         if (knownLocales.contains(requestedLocale) || localeString.equalsIgnoreCase("default")) {
             LOGGER.debug(this, "setting session locale to '" + localeString + "'");
-            final LocalSessionStateBean ssBean = this.getSessionStateBean();
             ssBean.setLocale(localeString.equalsIgnoreCase("default") ? PwmConstants.DEFAULT_LOCALE : requestedLocale);
             if (this.isAuthenticated()) {
                 try {
@@ -260,6 +260,7 @@ public class PwmSession implements Serializable {
             return true;
         } else {
             LOGGER.error(this, "ignoring unknown locale value set request for locale '" + localeString + "'");
+            ssBean.setLocale(PwmConstants.DEFAULT_LOCALE);
             return false;
         }
     }

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

@@ -257,4 +257,15 @@ public class PwmURL {
                 throw new IllegalArgumentException("unknown scheme: " + scheme);
         }
     }
+
+    public String getPostServletPath(final PwmServletDefinition pwmServletDefinition) {
+        final String path = this.uri.getPath();
+        for (final String pattern : pwmServletDefinition.urlPatterns()) {
+            final String patternWithContext = this.contextPath + pattern;
+            if (path.startsWith(patternWithContext)) {
+                return path.substring(patternWithContext.length());
+            }
+        }
+        return "";
+    }
 }

+ 54 - 0
src/main/java/password/pwm/http/bean/LoginServletBean.java

@@ -0,0 +1,54 @@
+/*
+ * 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
+ */
+
+package password.pwm.http.bean;
+
+import com.google.gson.annotations.SerializedName;
+import password.pwm.config.option.SessionBeanMode;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public class LoginServletBean extends PwmSessionBean {
+    @SerializedName("n")
+    private String nextUrl;
+
+    public String getNextUrl() {
+        return nextUrl;
+    }
+
+    public void setNextUrl(final String nextUrl) {
+        this.nextUrl = nextUrl;
+    }
+
+    public Type getType() {
+        return Type.PUBLIC;
+    }
+
+    @Override
+    public Set<SessionBeanMode> supportedModes() {
+        return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(SessionBeanMode.LOCAL, SessionBeanMode.CRYPTCOOKIE)));
+    }
+
+}

+ 1 - 1
src/main/java/password/pwm/http/filter/AuthenticationFilter.java

@@ -555,7 +555,7 @@ public class AuthenticationFilter extends AbstractPwmFilter {
 
             final String originalURL = pwmRequest.getURLwithQueryString();
             final OAuthMachine oAuthMachine = new OAuthMachine(oauthSettings);
-            oAuthMachine.redirectUserToOAuthServer(pwmRequest, originalURL, null);
+            oAuthMachine.redirectUserToOAuthServer(pwmRequest, originalURL, null,null);
             redirected = true;
         }
 

+ 36 - 20
src/main/java/password/pwm/http/servlet/LoginServlet.java

@@ -27,19 +27,19 @@ import password.pwm.PwmConstants;
 import password.pwm.bean.UserIdentity;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
-import password.pwm.error.PwmException;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.HttpMethod;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmURL;
+import password.pwm.http.bean.LoginServletBean;
 import password.pwm.ldap.auth.AuthenticationType;
 import password.pwm.ldap.auth.PwmAuthenticationSource;
 import password.pwm.ldap.auth.SessionAuthenticator;
 import password.pwm.util.CaptchaUtility;
 import password.pwm.util.Helper;
 import password.pwm.util.PasswordData;
-import password.pwm.util.Validator;
+import password.pwm.util.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.ws.server.RestResultBean;
 
@@ -74,6 +74,7 @@ public class LoginServlet extends AbstractPwmServlet {
     public enum LoginServletAction implements ProcessAction {
         login(HttpMethod.POST),
         restLogin(HttpMethod.POST),
+        receiveUrl(HttpMethod.GET),
 
         ;
 
@@ -112,8 +113,6 @@ public class LoginServlet extends AbstractPwmServlet {
         final LoginServletAction action = readProcessAction(pwmRequest);
 
         if (action != null) {
-            Validator.validatePwmFormID(pwmRequest);
-
             switch (action) {
                 case login:
                     processLogin(pwmRequest, passwordOnly);
@@ -123,6 +122,10 @@ public class LoginServlet extends AbstractPwmServlet {
                     processRestLogin(pwmRequest, passwordOnly);
                     break;
 
+                case receiveUrl:
+                    processReceiveUrl(pwmRequest);
+                    break;
+
                 default:
                     Helper.unhandledSwitchStatement(action);
             }
@@ -146,10 +149,7 @@ public class LoginServlet extends AbstractPwmServlet {
         }
 
         // login has succeeded
-        pwmRequest.sendRedirect(determinePostLoginUrl(
-                pwmRequest,
-                pwmRequest.readParameterAsString(PwmConstants.PARAM_POST_LOGIN_URL)
-        ));
+        pwmRequest.sendRedirect(determinePostLoginUrl(pwmRequest));
     }
 
     private void processRestLogin(final PwmRequest pwmRequest, final boolean passwordOnly)
@@ -175,7 +175,7 @@ public class LoginServlet extends AbstractPwmServlet {
         pwmRequest.readParametersAsMap();
 
         // login has succeeded
-        final String nextLoginUrl = determinePostLoginUrl(pwmRequest, valueMap.get(PwmConstants.PARAM_POST_LOGIN_URL));
+        final String nextLoginUrl = determinePostLoginUrl(pwmRequest);
         final RestResultBean restResultBean = new RestResultBean();
         final HashMap<String,String> resultMap = new HashMap<>(Collections.singletonMap("nextURL", nextLoginUrl));
         restResultBean.setData(resultMap);
@@ -183,6 +183,22 @@ public class LoginServlet extends AbstractPwmServlet {
         pwmRequest.outputJsonResult(restResultBean);
     }
 
+    private void processReceiveUrl(final PwmRequest pwmRequest)
+            throws PwmUnrecoverableException, IOException
+    {
+        final String encryptedNextUrl = pwmRequest.readParameterAsString(PwmConstants.PARAM_POST_LOGIN_URL);
+        if (!StringUtil.isEmpty(encryptedNextUrl)) {
+            final String nextUrl = pwmRequest.getPwmApplication().getSecureService().decryptStringValue(encryptedNextUrl);
+            if (!StringUtil.isEmpty(nextUrl)) {
+                final LoginServletBean loginServletBean = pwmRequest.getPwmApplication().getSessionStateService().getBean(pwmRequest, LoginServletBean.class);
+                LOGGER.trace(pwmRequest, "received nextUrl and storing in module bean, value: " + nextUrl);
+                loginServletBean.setNextUrl(nextUrl);
+            }
+        }
+
+        pwmRequest.sendRedirect(PwmServletDefinition.Login);
+    }
+
     private void handleLoginRequest(
             final PwmRequest pwmRequest,
             final Map<String,String> valueMap,
@@ -241,21 +257,16 @@ public class LoginServlet extends AbstractPwmServlet {
         pwmRequest.forwardToJsp(url);
     }
 
-    private static String determinePostLoginUrl(final PwmRequest pwmRequest, final String inputValue)
+    private static String determinePostLoginUrl(final PwmRequest pwmRequest)
             throws PwmUnrecoverableException
     {
-        String decryptedValue = null;
-        try {
-            if (inputValue != null && !inputValue.isEmpty()) {
-                decryptedValue = pwmRequest.getPwmApplication().getSecureService().decryptStringValue(inputValue);
-            }
-        } catch (PwmException e) {
-            LOGGER.warn(pwmRequest, "unable to decrypt post login redirect parameter: " + e.getMessage());
-        }
+        final LoginServletBean loginServletBean = pwmRequest.getPwmApplication().getSessionStateService().getBean(pwmRequest, LoginServletBean.class);
+        final String decryptedValue = loginServletBean.getNextUrl();
 
         if (decryptedValue != null && !decryptedValue.isEmpty()) {
             final PwmURL originalPwmURL = new PwmURL(URI.create(decryptedValue),pwmRequest.getContextPath());
             if (!originalPwmURL.isLoginServlet()) {
+                loginServletBean.setNextUrl(null);
                 return decryptedValue;
             }
         }
@@ -270,13 +281,18 @@ public class LoginServlet extends AbstractPwmServlet {
 
         final String encryptedRedirUrl = pwmRequest.getPwmApplication().getSecureService().encryptToString(originalRequestedUrl);
 
+        final Map<String,String> paramMap = new HashMap<>();
+        paramMap.put(PwmConstants.PARAM_POST_LOGIN_URL, encryptedRedirUrl);
+        paramMap.put(PwmConstants.PARAM_ACTION_REQUEST, LoginServletAction.receiveUrl.toString());
+
         final String redirectUrl = PwmURL.appendAndEncodeUrlParameters(
                 pwmRequest.getContextPath() + PwmServletDefinition.Login.servletUrl(),
-                Collections.singletonMap(PwmConstants.PARAM_POST_LOGIN_URL, encryptedRedirUrl)
+                paramMap
         );
 
-        pwmRequest.sendRedirect(redirectUrl);
+        LOGGER.trace(pwmRequest, "redirecting to self to set nextUrl to: " + originalRequestedUrl);
 
+        pwmRequest.sendRedirect(redirectUrl);
     }
 }
 

+ 38 - 2
src/main/java/password/pwm/http/servlet/forgottenpw/ForgottenPasswordServlet.java

@@ -63,7 +63,6 @@ import password.pwm.http.PwmSession;
 import password.pwm.http.bean.ForgottenPasswordBean;
 import password.pwm.http.filter.AuthenticationFilter;
 import password.pwm.http.servlet.AbstractPwmServlet;
-import password.pwm.util.CaptchaUtility;
 import password.pwm.http.servlet.PwmServletDefinition;
 import password.pwm.http.servlet.oauth.OAuthForgottenPasswordResults;
 import password.pwm.http.servlet.oauth.OAuthMachine;
@@ -85,6 +84,7 @@ import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.svc.token.TokenPayload;
 import password.pwm.svc.token.TokenService;
 import password.pwm.svc.token.TokenType;
+import password.pwm.util.CaptchaUtility;
 import password.pwm.util.Helper;
 import password.pwm.util.JsonUtil;
 import password.pwm.util.PasswordData;
@@ -922,6 +922,8 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
             final UserInfoBean userInfoBean = readUserInfoBean(pwmRequest, forgottenPasswordBean);
             pwmApplication.getAuditManager().submit(AuditEvent.UNLOCK_PASSWORD, userInfoBean, pwmSession);
 
+            sendUnlockNoticeEmail(pwmRequest, forgottenPasswordBean);
+
             pwmRequest.getPwmResponse().forwardToSuccessPage(Message.Success_UnlockAccount);
         } catch (ChaiOperationException e) {
             final String errorMsg = "unable to unlock user " + userIdentity + " error: " + e.getMessage();
@@ -1680,7 +1682,8 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
                 final OAuthSettings oAuthSettings = OAuthSettings.forForgottenPassword(forgottenPasswordProfile);
                 final OAuthMachine oAuthMachine = new OAuthMachine(oAuthSettings);
                 pwmRequest.getPwmApplication().getSessionStateService().saveSessionBeans(pwmRequest);
-                oAuthMachine.redirectUserToOAuthServer(pwmRequest, null, forgottenPasswordProfile.getIdentifier());
+                final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity();
+                oAuthMachine.redirectUserToOAuthServer(pwmRequest, null, userIdentity, forgottenPasswordProfile.getIdentifier());
                 break;
 
 
@@ -1785,6 +1788,39 @@ public class ForgottenPasswordServlet extends AbstractPwmServlet {
 
         return responseSet;
     }
+
+    private static void sendUnlockNoticeEmail(
+            final PwmRequest pwmRequest,
+            final ForgottenPasswordBean forgottenPasswordBean
+    )
+            throws PwmUnrecoverableException, ChaiUnavailableException, IOException, ServletException
+    {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final Configuration config = pwmRequest.getConfig();
+        final Locale locale = pwmRequest.getLocale();
+        final UserIdentity userIdentity = forgottenPasswordBean.getUserIdentity();
+        final EmailItemBean configuredEmailSetting = config.readSettingAsEmail(PwmSetting.EMAIL_UNLOCK, locale);
+
+        if (configuredEmailSetting == null) {
+            LOGGER.debug(pwmRequest, "skipping send unlock notice email for '" + userIdentity + "' no email configured");
+            return;
+        }
+
+        final UserInfoBean userInfoBean = readUserInfoBean(pwmRequest, forgottenPasswordBean);
+        final MacroMachine macroMachine = new MacroMachine(
+                pwmApplication,
+                pwmRequest.getSessionLabel(),
+                userInfoBean,
+                null,
+                LdapUserDataReader.appProxiedReader(pwmApplication, userIdentity)
+        );
+
+        pwmApplication.getEmailQueue().submitEmail(
+                configuredEmailSetting,
+                userInfoBean,
+                macroMachine
+        );
+    }
 }
 
 

+ 36 - 0
src/main/java/password/pwm/http/servlet/helpdesk/HelpdeskServlet.java

@@ -687,6 +687,9 @@ public class HelpdeskServlet extends AbstractPwmServlet {
             intruderManager.convenience().clearUserIdentity(userIdentity);
         }
 
+        // send notice email
+        sendUnlockNoticeEmail(pwmRequest, helpdeskProfile, userIdentity);
+
         final boolean useProxy = helpdeskProfile.readSettingAsBoolean(PwmSetting.HELPDESK_USE_PROXY);
         try {
             final ChaiUser chaiUser = useProxy ?
@@ -1232,4 +1235,37 @@ public class HelpdeskServlet extends AbstractPwmServlet {
         final RestResultBean restResultBean = new RestResultBean(responseBean);
         pwmRequest.outputJsonResult(restResultBean);
     }
+
+    private static void sendUnlockNoticeEmail(
+            final PwmRequest pwmRequest,
+            final HelpdeskProfile helpdeskProfile,
+            final UserIdentity userIdentity
+    )
+            throws PwmUnrecoverableException, ChaiUnavailableException, IOException, ServletException {
+        final PwmApplication pwmApplication = pwmRequest.getPwmApplication();
+        final Configuration config = pwmRequest.getConfig();
+        final Locale locale = pwmRequest.getLocale();
+        final EmailItemBean configuredEmailSetting = config.readSettingAsEmail(PwmSetting.EMAIL_HELPDESK_UNLOCK, locale);
+
+        if (configuredEmailSetting == null) {
+            LOGGER.debug(pwmRequest, "skipping send helpdesk unlock notice email for '" + userIdentity + "' no email configured");
+            return;
+        }
+
+        final HelpdeskDetailInfoBean helpdeskDetailInfoBean = makeHelpdeskDetailInfo(pwmRequest, helpdeskProfile, userIdentity);
+        final MacroMachine macroMachine = new MacroMachine(
+                pwmApplication,
+                pwmRequest.getSessionLabel(),
+                helpdeskDetailInfoBean.getUserInfoBean(),
+                null,
+                LdapUserDataReader.appProxiedReader(pwmApplication, userIdentity)
+        );
+
+        pwmApplication.getEmailQueue().submitEmail(
+                configuredEmailSetting,
+                helpdeskDetailInfoBean.getUserInfoBean(),
+                macroMachine
+        );
+    }
+
 }

+ 6 - 5
src/main/java/password/pwm/http/servlet/oauth/OAuthConsumerServlet.java

@@ -119,7 +119,7 @@ public class OAuthConsumerServlet extends AbstractPwmServlet {
         {
             final String oauthRequestError = pwmRequest.readParameterAsString("error");
             if (oauthRequestError != null && !oauthRequestError.isEmpty()) {
-                final String errorMsg = "error detected from oauth request parameter: " + oauthRequestError;
+                final String errorMsg = "incoming request from remote oauth server is indicating an error: " + oauthRequestError;
                 final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR, errorMsg, "Remote Error: " + oauthRequestError, null);
                 LOGGER.error(pwmSession, errorMsg);
                 pwmRequest.respondWithError(errorInformation);
@@ -163,29 +163,30 @@ public class OAuthConsumerServlet extends AbstractPwmServlet {
         final OAuthMachine oAuthMachine = new OAuthMachine(oAuthSettings);
 
         // make sure request was initiated in users current session
-        /*
         if (!oAuthRequestState.get().isSessionMatch()) {
             try{
                 switch (oAuthUseCaseCase) {
                     case Authentication:
                         LOGGER.debug(pwmSession, "oauth consumer reached but response is not for a request issued during the current session, will redirect back to oauth server for verification update");
                         final String nextURL = oauthState.getNextUrl();
-                        oAuthMachine.redirectUserToOAuthServer(pwmRequest, nextURL, null);
+                        oAuthMachine.redirectUserToOAuthServer(pwmRequest, nextURL, null, null);
                         return;
 
                     case ForgottenPassword:
                         LOGGER.debug(pwmSession, "oauth consumer reached but response is not for a request issued during the current session, will redirect back to forgotten password servlet");
                         pwmRequest.sendRedirect(PwmServletDefinition.ForgottenPassword);
                         return;
+
+                    default:
+                        Helper.unhandledSwitchStatement(oAuthUseCaseCase);
                 }
             } catch (PwmUnrecoverableException e) {
                 final String errorMsg = "unexpected error redirecting user to oauth page: " + e.toString();
-                ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_UNKNOWN, errorMsg);
+                final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_OAUTH_ERROR, errorMsg);
                 pwmRequest.setResponseError(errorInformation);
                 LOGGER.error(errorInformation.toDebugStr());
             }
         }
-        */
 
         final String requestCodeStr = pwmRequest.readParameterAsString(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_CODE));
         LOGGER.trace(pwmSession,"received code from oauth server: " + requestCodeStr);

+ 57 - 0
src/main/java/password/pwm/http/servlet/oauth/OAuthMachine.java

@@ -31,6 +31,7 @@ import org.apache.http.util.EntityUtils;
 import password.pwm.AppProperty;
 import password.pwm.PwmConstants;
 import password.pwm.bean.LoginInfoBean;
+import password.pwm.bean.UserIdentity;
 import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.error.ErrorInformation;
@@ -44,16 +45,20 @@ import password.pwm.http.client.PwmHttpClientConfiguration;
 import password.pwm.http.servlet.PwmServletDefinition;
 import password.pwm.util.BasicAuthInfo;
 import password.pwm.util.JsonUtil;
+import password.pwm.util.StringUtil;
 import password.pwm.util.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
+import password.pwm.util.macro.MacroMachine;
 
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.security.cert.X509Certificate;
+import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 
@@ -89,6 +94,7 @@ public class OAuthMachine {
     public void redirectUserToOAuthServer(
             final PwmRequest pwmRequest,
             final String nextUrl,
+            final UserIdentity userIdentity,
             final String forgottenPasswordProfile
     )
             throws PwmUnrecoverableException, IOException
@@ -108,6 +114,13 @@ public class OAuthMachine {
         urlParams.put(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_STATE),state);
         urlParams.put(config.readAppProperty(AppProperty.HTTP_PARAM_OAUTH_REDIRECT_URI), redirectUri);
 
+        if (userIdentity != null) {
+            final String parametersValue = figureUsernameGrantParam(pwmRequest, userIdentity);
+            if (StringUtil.isEmpty(parametersValue)) {
+                urlParams.put("parameters", parametersValue);
+            }
+        }
+
         final String redirectUrl = PwmURL.appendAndEncodeUrlParameters(settings.getLoginURL(), urlParams);
 
         try{
@@ -402,4 +415,48 @@ public class OAuthMachine {
         final String jsonValue = JsonUtil.serialize(oAuthState);
         return pwmRequest.getPwmApplication().getSecureService().encryptToString(jsonValue);
     }
+
+    private String figureUsernameGrantParam(
+            final PwmRequest pwmRequest,
+            final UserIdentity userIdentity
+    )
+            throws IOException, PwmUnrecoverableException
+    {
+        if (userIdentity == null) {
+            return null;
+        }
+
+        final String macroText = settings.getUsernameSendValue();
+        if (StringUtil.isEmpty(macroText)) {
+            return null;
+        }
+
+        final MacroMachine macroMachine = MacroMachine.forUser(pwmRequest, userIdentity);
+        final String username = macroMachine.expandMacros(macroText);
+        LOGGER.debug(pwmRequest, "calculated username value for user as: " + username);
+
+        final String grantUrl = settings.getLoginURL();
+        final String signUrl = grantUrl.replace("/grant","/sign");
+
+        final Map<String, String> requestPayload;
+        {
+            final Map<String, String> dataPayload = new HashMap<>();
+            dataPayload.put("username", username);
+
+            final List<Map<String,String>> listWrapper = new ArrayList<>();
+            listWrapper.add(dataPayload);
+
+            requestPayload = new HashMap<>();
+            requestPayload.put("data",  JsonUtil.serializeCollection(listWrapper));
+        }
+
+        LOGGER.debug(pwmRequest, "preparing to send username to OAuth /sign endpoint for future injection to /grant redirect");
+        final RestResults restResults = makeHttpRequest(pwmRequest, "OAuth pre-inject username signing service",settings, signUrl, requestPayload);
+
+        final String resultBody = restResults.getResponseBody();
+        final Map<String,String> resultBodyMap = JsonUtil.deserializeStringMap(resultBody);
+        final String data = resultBodyMap.get("data");
+        LOGGER.debug(pwmRequest, "oauth /sign endpoint returned signed username data: " + data);
+        return data;
+    }
 }

+ 8 - 0
src/main/java/password/pwm/http/servlet/oauth/OAuthSettings.java

@@ -39,6 +39,9 @@ public class OAuthSettings implements Serializable {
     private String dnAttributeName;
     private OAuthUseCase use;
     private X509Certificate[] certificates;
+    private String usernameSendValue;
+
+
 
     private OAuthSettings() {
     }
@@ -81,6 +84,10 @@ public class OAuthSettings implements Serializable {
         return certificates;
     }
 
+    public String getUsernameSendValue() {
+        return usernameSendValue;
+    }
+
     public boolean oAuthIsConfigured() {
         return (loginURL != null && !loginURL.isEmpty())
                 && (codeResolveUrl != null && !codeResolveUrl.isEmpty())
@@ -113,6 +120,7 @@ public class OAuthSettings implements Serializable {
         settings.dnAttributeName = config.readSettingAsString(PwmSetting.RECOVERY_OAUTH_ID_DN_ATTRIBUTE_NAME);
         settings.certificates = config.readSettingAsCertificate(PwmSetting.RECOVERY_OAUTH_ID_CERTIFICATE);
         settings.use = OAuthUseCase.ForgottenPassword;
+        settings.usernameSendValue = config.readSettingAsString(PwmSetting.RECOVERY_OAUTH_ID_USERNAME_SEND_VALUE);
         return settings;
     }
 }

+ 46 - 0
src/main/java/password/pwm/http/servlet/peoplesearch/LinkReferenceBean.java

@@ -0,0 +1,46 @@
+/*
+ * 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
+ */
+
+package password.pwm.http.servlet.peoplesearch;
+
+import java.io.Serializable;
+
+class LinkReferenceBean implements Serializable {
+    private String name;
+    private String link;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(final String name) {
+        this.name = name;
+    }
+
+    public String getLink() {
+        return link;
+    }
+
+    public void setLink(final String link) {
+        this.link = link;
+    }
+}

+ 31 - 3
src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchDataReader.java

@@ -53,6 +53,7 @@ import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
 import password.pwm.util.JsonUtil;
 import password.pwm.util.LocaleHelper;
+import password.pwm.util.StringUtil;
 import password.pwm.util.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
@@ -212,11 +213,38 @@ public class PeopleSearchDataReader {
             userDetailBean.setDisplayNames(displayName);
         }
 
+        userDetailBean.setLinks(makeUserDetailLinks(userIdentity));
+
         LOGGER.trace(pwmRequest.getPwmSession(), "finished building userDetail result in " + TimeDuration.fromCurrent(startTime).asCompactString());
         storeDataInCache(pwmRequest.getPwmApplication(), cacheKey, userDetailBean);
         return userDetailBean;
     }
 
+    private List<LinkReferenceBean> makeUserDetailLinks(final UserIdentity actorIdentity) throws PwmUnrecoverableException {
+        final String userLinksStr = pwmRequest.getConfig().readAppProperty(AppProperty.PEOPLESEARCH_VIEW_DETAIL_LINKS);
+        if (StringUtil.isEmpty(userLinksStr)) {
+            return Collections.emptyList();
+        }
+        final Map<String,String> linkMap;
+        try {
+            linkMap = JsonUtil.deserializeStringMap(userLinksStr);
+        } catch (Exception e) {
+            LOGGER.warn(pwmRequest, "error de-serializing configured app property json for detail links: " + e.getMessage());
+            return Collections.emptyList();
+        }
+        final List<LinkReferenceBean> returnList = new ArrayList<>();
+        final MacroMachine macroMachine = getMacroMachine(actorIdentity);
+        for (final String key : linkMap.keySet()) {
+            final String value = linkMap.get(key);
+            final String parsedValue = macroMachine.expandMacros(value);
+            final LinkReferenceBean linkReference = new LinkReferenceBean();
+            linkReference.setName(key);
+            linkReference.setLink(parsedValue);
+            returnList.add(linkReference);
+        }
+        return returnList;
+    }
+
     private List<String> readUserMultiAttributeValues(
             final PwmRequest pwmRequest,
             final UserIdentity userIdentity,
@@ -247,8 +275,8 @@ public class PeopleSearchDataReader {
     }
 
     private CacheKey makeCacheKey(
-            final String operationIdentifer,
-            final String dataIdentifer
+            final String operationIdentifier,
+            final String dataIdentifier
     )
             throws PwmUnrecoverableException
     {
@@ -258,7 +286,7 @@ public class PeopleSearchDataReader {
         } else {
             userIdentity = null;
         }
-        final String keyString = operationIdentifer + "|" + pwmRequest.getPwmApplication().getSecureService().hash(dataIdentifer);
+        final String keyString = operationIdentifier + "|" + pwmRequest.getPwmApplication().getSecureService().hash(dataIdentifier);
         return CacheKey.makeCacheKey(
                 this.getClass(),
                 userIdentity,

+ 17 - 11
src/main/java/password/pwm/http/servlet/peoplesearch/PeopleSearchServlet.java

@@ -204,7 +204,10 @@ public class PeopleSearchServlet extends AbstractPwmServlet {
         final SearchResultBean searchResultBean = peopleSearchDataReader.makeSearchResultBean(username, includeDisplayName);
         searchResultBean.setFromCache(false);
         final RestResultBean restResultBean = new RestResultBean(searchResultBean);
+
+        addExpiresHeadersToResponse(pwmRequest);
         pwmRequest.outputJsonResult(restResultBean);
+
         LOGGER.trace(pwmRequest, "returning " + searchResultBean.getSearchResults().size() + " results for search request '" + username + "'");
     }
 
@@ -236,6 +239,8 @@ public class PeopleSearchServlet extends AbstractPwmServlet {
         try {
             final PeopleSearchDataReader peopleSearchDataReader = new PeopleSearchDataReader(pwmRequest);
             final OrgChartDataBean orgChartData = peopleSearchDataReader.makeOrgChartData(userIdentity);
+
+            addExpiresHeadersToResponse(pwmRequest);
             pwmRequest.outputJsonResult(new RestResultBean(orgChartData));
             StatisticsManager.incrementStat(pwmRequest, Statistic.PEOPLESEARCH_ORGCHART);
         } catch (PwmException e) {
@@ -258,6 +263,8 @@ public class PeopleSearchServlet extends AbstractPwmServlet {
         try {
             final PeopleSearchDataReader peopleSearchDataReader = new PeopleSearchDataReader(pwmRequest);
             final UserDetailBean detailData = peopleSearchDataReader.makeUserDetailRequest(userKey);
+
+            addExpiresHeadersToResponse(pwmRequest);
             pwmRequest.outputJsonResult(new RestResultBean(detailData));
             pwmRequest.getPwmApplication().getStatisticsManager().incrementValue(Statistic.PEOPLESEARCH_DETAILS);
         } catch (PwmOperationalException e) {
@@ -301,21 +308,20 @@ public class PeopleSearchServlet extends AbstractPwmServlet {
             return;
         }
 
-        OutputStream outputStream = null;
-        try {
-            final long maxCacheSeconds = pwmRequest.getConfig().readSettingAsLong(PwmSetting.PEOPLE_SEARCH_MAX_CACHE_SECONDS);
+        addExpiresHeadersToResponse(pwmRequest);
+
+        try (OutputStream outputStream = pwmRequest.getPwmResponse().getOutputStream()) {
             final HttpServletResponse resp = pwmRequest.getPwmResponse().getHttpServletResponse();
             resp.setContentType(photoData.getMimeType());
-            resp.setDateHeader("Expires", System.currentTimeMillis() + (maxCacheSeconds * 1000L));
-            resp.setHeader("Cache-Control", "public, max-age=" + maxCacheSeconds);
 
-            outputStream = pwmRequest.getPwmResponse().getOutputStream();
             outputStream.write(photoData.getContents());
-
-        } finally {
-            if (outputStream != null) {
-                outputStream.close();
-            }
         }
     }
+
+    private void addExpiresHeadersToResponse(final PwmRequest pwmRequest) {
+        final long maxCacheSeconds = pwmRequest.getConfig().readSettingAsLong(PwmSetting.PEOPLE_SEARCH_MAX_CACHE_SECONDS);
+        final HttpServletResponse resp = pwmRequest.getPwmResponse().getHttpServletResponse();
+        resp.setDateHeader("Expires", System.currentTimeMillis() + (maxCacheSeconds * 1000L));
+        resp.setHeader("Cache-Control", "private, max-age=" + maxCacheSeconds);
+    }
 }

+ 10 - 0
src/main/java/password/pwm/http/servlet/peoplesearch/UserDetailBean.java

@@ -23,6 +23,7 @@
 package password.pwm.http.servlet.peoplesearch;
 
 import java.io.Serializable;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
@@ -31,6 +32,7 @@ class UserDetailBean implements Serializable {
     private String userKey;
     private Map<String, AttributeDetailBean> detail;
     private String photoURL;
+    private List<LinkReferenceBean> links = Collections.emptyList();
 
     public List<String> getDisplayNames() {
         return displayNames;
@@ -63,4 +65,12 @@ class UserDetailBean implements Serializable {
     public void setPhotoURL(final String photoURL) {
         this.photoURL = photoURL;
     }
+
+    public List<LinkReferenceBean> getLinks() {
+        return links;
+    }
+
+    public void setLinks(final List<LinkReferenceBean> links) {
+        this.links = links;
+    }
 }

+ 1 - 1
src/main/java/password/pwm/http/state/CryptoCookieBeanImpl.java

@@ -42,7 +42,7 @@ class CryptoCookieBeanImpl implements SessionBeanProvider {
 
     private static final PwmLogger LOGGER = PwmLogger.forClass(CryptoCookieBeanImpl.class);
 
-    private static final PwmHttpResponseWrapper.CookiePath COOKIE_PATH = PwmHttpResponseWrapper.CookiePath.CurrentURL;
+    private static final PwmHttpResponseWrapper.CookiePath COOKIE_PATH = PwmHttpResponseWrapper.CookiePath.PwmServlet;
 
     @Override
     public <E extends PwmSessionBean> E getSessionBean(final PwmRequest pwmRequest, final Class<E> theClass) throws PwmUnrecoverableException {

+ 9 - 0
src/main/java/password/pwm/i18n/Display.java

@@ -231,6 +231,15 @@ public enum Display implements PwmDisplayBundle {
     Field_VerificationMethodNAAF,
     Field_VerificationMethodOAuth,
     Field_VerificationMethod,
+    Description_VerificationMethodPreviousAuth,
+    Description_VerificationMethodToken,
+    Description_VerificationMethodOTP,
+    Description_VerificationMethodChallengeResponses,
+    Description_VerificationMethodAttributes,
+    Description_VerificationMethodRemoteResponses,
+    Description_VerificationMethodNAAF,
+    Description_VerificationMethodOAuth,
+    Description_VerificationMethod,
     Long_Title_ActivateUser,
     Long_Title_Admin,
     Long_Title_ChangePassword,

+ 41 - 0
src/main/java/password/pwm/ldap/auth/SessionAuthenticator.java

@@ -22,12 +22,14 @@
 
 package password.pwm.ldap.auth;
 
+import com.google.gson.reflect.TypeToken;
 import com.novell.ldapchai.ChaiConstant;
 import com.novell.ldapchai.exception.ChaiError;
 import com.novell.ldapchai.exception.ChaiException;
 import com.novell.ldapchai.exception.ChaiUnavailableException;
 import com.novell.ldapchai.exception.ImpossiblePasswordPolicyException;
 import com.novell.ldapchai.provider.ChaiProvider;
+import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.bean.LocalSessionStateBean;
@@ -36,6 +38,8 @@ import password.pwm.bean.SessionLabel;
 import password.pwm.bean.UserIdentity;
 import password.pwm.bean.UserInfoBean;
 import password.pwm.config.PwmSetting;
+import password.pwm.error.ErrorInformation;
+import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.http.PwmSession;
@@ -46,10 +50,16 @@ import password.pwm.svc.intruder.IntruderManager;
 import password.pwm.svc.intruder.RecordType;
 import password.pwm.svc.stats.Statistic;
 import password.pwm.svc.stats.StatisticsManager;
+import password.pwm.util.Helper;
+import password.pwm.util.JsonUtil;
 import password.pwm.util.PasswordData;
+import password.pwm.util.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 
 import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 
 public class SessionAuthenticator {
     private static final PwmLogger LOGGER = PwmLogger.getLogger(SessionAuthenticator.class.getName());
@@ -96,9 +106,40 @@ public class SessionAuthenticator {
             postAuthenticationSequence(userIdentity, authResult);
         } catch (PwmOperationalException e) {
             postFailureSequence(e, username, userIdentity);
+
+            if (readHiddenErrorTypes().contains(e.getError())) {
+                if (Helper.determineIfDetailErrorMsgShown(pwmApplication)) {
+                    LOGGER.debug(pwmSession, "allowing error " + e.getError() + " to be returned though it is configured as a hidden type; "
+                            + "app is currently permitting detailed error messages");
+                } else {
+                    final ErrorInformation errorInformation = new ErrorInformation(PwmError.ERROR_WRONGPASSWORD);
+                    LOGGER.debug(pwmSession, "converting error from ldap " + e.getError() + " to " + PwmError.ERROR_WRONGPASSWORD
+                            + " due to app property " + AppProperty.SECURITY_LOGIN_HIDDEN_ERROR_TYPES.getKey());
+                    throw new PwmOperationalException(errorInformation);
+                }
+            }
+
             throw e;
         }
+    }
 
+    private Set<PwmError> readHiddenErrorTypes() {
+        final String appProperty = pwmApplication.getConfig().readAppProperty(AppProperty.SECURITY_LOGIN_HIDDEN_ERROR_TYPES);
+        final Set<PwmError> returnSet = new HashSet<>();
+        if (!StringUtil.isEmpty(appProperty)) {
+            try {
+                final List<Integer> configuredNumbers = JsonUtil.deserialize(appProperty, new TypeToken<List<Integer>>() {
+                });
+                for (final Integer errorCode : configuredNumbers) {
+                    final PwmError pwmError = PwmError.forErrorNumber(errorCode);
+                    returnSet.add(pwmError);
+                }
+            } catch (Exception e) {
+                LOGGER.error(pwmSession, "error parsing app property " + AppProperty.SECURITY_LOGIN_HIDDEN_ERROR_TYPES.getKey()
+                        + ", error: " + e.getMessage());
+            }
+        }
+        return returnSet;
     }
 
 

+ 4 - 0
src/main/java/password/pwm/util/StringUtil.java

@@ -347,4 +347,8 @@ public abstract class StringUtil {
 
         return new int[0];
     }
+
+    public static boolean isEmpty(final CharSequence input) {
+        return StringUtils.isEmpty(input);
+    }
 }

+ 3 - 1
src/main/resources/password/pwm/AppProperty.properties

@@ -79,7 +79,7 @@ http.resources.expirationSeconds=3600
 http.resources.gzip.enable=true
 http.resources.pathNonceEnable=true
 http.resources.pathNoncePrefix=nonce-
-http.resources.webjarMappings={"/public/resources/dojo/dgrid/":"/webjars/dgrid/1.1.0/","/public/resources/dojo/dojo/":"/webjars/dojo/1.11.2/","/public/resources/dojo/dijit/":"/webjars/dijit/1.11.2/","/public/resources/dojo/dojox/":"/webjars/dojox/1.11.2/","/public/resources/font/font-awesome/":"/webjars/font-awesome/4.7.0/","/public/resources/flags/":"/webjars/famfamfam-flags/1.0.0/dist/","/public/resources/angular/":"/webjars/angular/1.5.8/","/public/resources/angular-ui-router/":"/webjars/angular-ui-router/1.0.0-beta.3/","/public/resources/angular-translate/":"/webjars/angular-translate/2.13.0/","/public/resources/angular-sanitize/":"/webjars/angular-sanitize/1.5.8/"}
+http.resources.webjarMappings={"/public/resources/dojo/dgrid/":"/webjars/dgrid/1.1.0/","/public/resources/dojo/dojo/":"/webjars/dojo/1.11.2/","/public/resources/dojo/dijit/":"/webjars/dijit/1.11.2/","/public/resources/dojo/dojox/":"/webjars/dojox/1.11.2/","/public/resources/font/font-awesome/":"/webjars/font-awesome/4.7.0/","/public/resources/flags/":"/webjars/famfamfam-flags/1.0.0/dist/","/public/resources/angular/":"/webjars/angular/1.5.8/","/public/resources/angular-ui-router/":"/webjars/angular-ui-router/1.0.0-beta.3/","/public/resources/angular-translate/":"/webjars/angular-translate/2.13.0/"}
 http.resources.zipFiles=[]
 http.gzip.enable=true
 http.errors.allowHtml=true
@@ -198,6 +198,7 @@ password.randomGenerator.jitter.count=50
 peoplesearch.displayName.enableAllMacros=false
 peoplesearch.values.verifyUserDN=true
 peoplesearch.values.maxCount=100
+peoplesearch.view.detail.links=
 queue.email.retryTimeoutMs=10000
 queue.email.maxAgeMs=86400000
 queue.email.maxCount=100000
@@ -217,6 +218,7 @@ security.http.promiscuousEnable=false
 security.httpsServer.selfCert.futureSeconds=63113904
 security.httpsServer.selfCert.alg=RSA
 security.httpsServer.selfCert.keySize=2048
+security.login.hiddenErrorTypes=[5016]
 security.responses.hashIterations=100000
 security.input.trim=true
 security.input.password.trim=false

+ 21 - 3
src/main/resources/password/pwm/config/PwmSetting.xml

@@ -799,6 +799,18 @@
             <value>{"to":"@User:Email@","from":"Delete Account Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Account Deletion Notice","bodyPlain":"Your account has been deleted at your request.","bodyHtml":""}</value>
         </default>
     </setting>
+    <setting hidden="false" key="email.helpdesk.unlock" level="1">
+        <flag>MacroSupport</flag>
+        <default>
+            <value>{"to":"@User:Email@","from":"Unlock Account Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Account Unlock Notice","bodyPlain":"Your account has been unlocked by the helpdesk.","bodyHtml":""}</value>
+        </default>
+    </setting>
+    <setting hidden="false" key="email.unlock" level="1">
+        <flag>MacroSupport</flag>
+        <default>
+            <value>{"to":"@User:Email@","from":"Unlock Account Notice \u003c@DefaultEmailFromAddress@\u003e","subject":"Account Unlock Notice","bodyPlain":"Your account has been unlocked.","bodyHtml":""}</value>
+        </default>
+    </setting>
     <setting hidden="false" key="email.smtp.advancedSettings" level="2">
         <regex>^[a-zA-Z0-9.]+=.+$</regex>
         <default/>
@@ -2170,6 +2182,11 @@
     <setting hidden="false" key="recovery.oauth.idserver.dnAttributeName" level="2">
         <default/>
     </setting>
+    <setting hidden="false" key="recovery.oauth.idserver.usernameSendValue" level="2">
+        <flag>MacroSupport</flag>
+        <example>@LDAP:DN@</example>
+        <default/>
+    </setting>
     <setting hidden="false" key="recovery.enable" level="1">
         <default>
             <value>true</value>
@@ -2819,8 +2836,8 @@
             <value>{"name":"givenName","minimumLength":1,"maximumLength":64,"type":"text","required":true,"confirmationRequired":false,"readonly":false,"labels":{"":"First Name"},"regexErrors":{"":""},"description":{"":""},"selectOptions":{}}</value>
             <value>{"name":"sn","minimumLength":1,"maximumLength":64,"type":"text","required":true,"confirmationRequired":false,"readonly":false,"labels":{"":"Last Name"},"regexErrors":{"":""},"description":{"":""},"selectOptions":{}}</value>
             <value>{"name":"title","minimumLength":1,"maximumLength":64,"type":"text","required":true,"confirmationRequired":false,"readonly":false,"labels":{"":"Title"},"regexErrors":{"":""},"description":{"":""},"selectOptions":{}}</value>
-            <value>{"name":"mail","minimumLength":1,"maximumLength":64,"type":"email","required":true,"confirmationRequired":false,"readonly":false,"labels":{"":"Email"},"regexErrors":{"":""},"description":{"":""},"selectOptions":{}}</value>
-            <value>{"name":"telephoneNumber","minimumLength":1,"maximumLength":64,"type":"tel","required":true,"confirmationRequired":false,"readonly":false,"labels":{"":"Telephone"},"regexErrors":{"":""},"description":{"":""},"selectOptions":{}}</value>
+            <value>{"name":"mail","minimumLength":1,"maximumLength":64,"type":"text","required":true,"confirmationRequired":false,"readonly":false,"labels":{"":"Email"},"regexErrors":{"":""},"description":{"":""},"selectOptions":{}}</value>
+            <value>{"name":"telephoneNumber","minimumLength":1,"maximumLength":64,"type":"text","required":true,"confirmationRequired":false,"readonly":false,"labels":{"":"Telephone"},"regexErrors":{"":""},"description":{"":""},"selectOptions":{}}</value>
         </default>
     </setting>
     <setting hidden="false" key="peopleSearch.detail.form" level="1" required="true">
@@ -2850,6 +2867,7 @@
         <options>
             <option value="text">text</option>
             <option value="email">email</option>
+            <option value="tel">telephone</option>
             <option value="userDN">userDN</option>
         </options>
     </setting>
@@ -2876,7 +2894,7 @@
             <value>@LDAP:telephoneNumber@</value>
         </default>
         <properties>
-            <property key="Maximum">4</property>
+            <property key="Maximum">7</property>
         </properties>
     </setting>
     <setting hidden="false" key="peopleSearch.displayName.user" level="1" required="true">

+ 8 - 0
src/main/resources/password/pwm/i18n/Display.properties

@@ -239,6 +239,14 @@ Field_VerificationMethodAttributes=Personal Data
 Field_VerificationMethodRemoteResponses=External Responses
 Field_VerificationMethodNAAF=Advanced Authentication
 Field_VerificationMethodOAuth=External Authentication
+Description_VerificationMethodPreviousAuth=
+Description_VerificationMethodToken=
+Description_VerificationMethodOTP=
+Description_VerificationMethodChallengeResponses=
+Description_VerificationMethodAttributes=
+Description_VerificationMethodRemoteResponses=
+Description_VerificationMethodNAAF=
+Description_VerificationMethodOAuth=
 Field_Placeholder_Answer=Answer
 Long_Title_ActivateUser=Activate a pre-configured account and establish a new password.
 Long_Title_Admin=Administrative functions

+ 1 - 0
src/main/resources/password/pwm/i18n/Health.properties

@@ -91,3 +91,4 @@ HealthTopic_Application=Application
 HealthTopic_SMS=SMS
 HealthTopic_Database=Database
 HealthTopic_Audit=Audit
+HealthTopic_Appliance=Appliance

文件差异内容过多而无法显示
+ 18 - 127
src/main/resources/password/pwm/i18n/PwmSetting.properties


+ 1 - 0
src/main/webapp/WEB-INF/jsp/forgottenpassword-method.jsp

@@ -61,6 +61,7 @@
                         <input type="hidden" name="processAction" value="<%=ForgottenPasswordServlet.ForgottenPasswordAction.verificationChoice%>"/>
                         <input type="hidden" name="pwmFormID" value="<pwm:FormID/>"/>
                     </form>
+                    <p><%=method.getDescription(pwmRequest.getConfig(),pwmRequest.getLocale())%></p>
                 </td>
             </tr>
             <% } %>

+ 5 - 5
src/main/webapp/WEB-INF/jsp/fragment/header-body.jsp

@@ -58,12 +58,13 @@
                 </div>
 
                 <% if (!JspUtility.isFlag(request, PwmRequestFlag.HIDE_HEADER_BUTTONS)) { %>
-                <pwm:if test="<%=PwmIfTest.forcedPageView%>" negate="true">
                     <pwm:if test="<%=PwmIfTest.authenticated%>">
                         <pwm:if test="<%=PwmIfTest.showHome%>">
-                            <a class="header-button" href="<pwm:value name="<%=PwmValue.homeURL%>"/>" id="HomeButton">
-                                <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="pwm-icon pwm-icon-home" title="<pwm:display key="Button_Home"/>"></span></pwm:if>
-                            </a>
+                            <pwm:if test="<%=PwmIfTest.forcedPageView%>" negate="true">
+                                <a class="header-button" href="<pwm:value name="<%=PwmValue.homeURL%>"/>" id="HomeButton">
+                                    <pwm:if test="<%=PwmIfTest.showIcons%>"><span class="pwm-icon pwm-icon-home" title="<pwm:display key="Button_Home"/>"></span></pwm:if>
+                                </a>
+                            </pwm:if>
                         </pwm:if>
                         <pwm:if test="<%=PwmIfTest.showLogout%>">
                             <a class="header-button" href="<pwm:url url='<%=PwmServletDefinition.Logout.servletUrl()%>' addContext="true"/>" id="LogoutButton">
@@ -71,7 +72,6 @@
                             </a>
                         </pwm:if>
                     </pwm:if>
-                </pwm:if>
                 <% } %>
             </div>
         </div>

+ 1 - 1
src/main/webapp/WEB-INF/jsp/fragment/header.jsp

@@ -52,7 +52,7 @@
           data-restClientKey="<pwm:value name="<%=PwmValue.restClientKey%>"/>">
     <meta name="viewport" content="width=device-width, initial-scale = 1.0, user-scalable=no"/>
     <meta http-equiv="X-UA-Compatible" content="IE=10; IE=9; IE=8; IE=7" />
-    <link rel="icon" type="image/x-icon" href="<pwm:url url='/public/resources/favicon.ico' addContext="true"/>"/>
+    <link rel="icon" type="image/png" href="<pwm:url url='/public/resources/favicon.png' addContext="true"/>"/>
     <link rel="stylesheet" type="text/css" href="<pwm:url url='/public/resources/pwm-icons.css' addContext="true"/>"/>
     <link href="<pwm:url url='/public/resources/style.css' addContext="true"/>" rel="stylesheet" type="text/css" media="screen"/>
     <link href="<pwm:url url="%THEME_URL%"/>" rel="stylesheet" type="text/css" media="screen"/>

+ 0 - 3
src/main/webapp/WEB-INF/jsp/login-passwordonly.jsp

@@ -47,9 +47,6 @@
                 <input type="hidden" name="processAction" value="login">
                 <%@ include file="/WEB-INF/jsp/fragment/cancel-button.jsp" %>
                 <input type="hidden" id="pwmFormID" name="pwmFormID" value="<pwm:FormID/>"/>
-                <input type="hidden" id="<%=PwmConstants.PARAM_POST_LOGIN_URL%>" name="<%=PwmConstants.PARAM_POST_LOGIN_URL%>"
-                       value="<%=StringUtil.escapeHtml(JspUtility.getPwmRequest(pageContext).readParameterAsString(PwmConstants.PARAM_POST_LOGIN_URL))%>"/>
-
             </div>
         </form>
         <br/>

+ 0 - 2
src/main/webapp/WEB-INF/jsp/login.jsp

@@ -47,8 +47,6 @@
                 <div class="formFieldWrapper">
                     <input type="<pwm:value name="<%=PwmValue.passwordFieldType%>"/>" name="password" id="password" placeholder="<pwm:display key="Field_Password"/>" required="required" class="inputfield passwordfield"/>
                 </div>
-                <input type="hidden" id="<%=PwmConstants.PARAM_POST_LOGIN_URL%>" name="<%=PwmConstants.PARAM_POST_LOGIN_URL%>"
-                       value="<%=StringUtil.escapeHtml(JspUtility.getPwmRequest(pageContext).readParameterAsString(PwmConstants.PARAM_POST_LOGIN_URL))%>"/>
                 <%@ include file="/WEB-INF/jsp/fragment/captcha-embed.jsp"%>
                 <div class="buttonbar">
                     <button type="submit" class="btn" <pwm:autofocus/> name="button" id="submitBtn">

+ 0 - 1
src/main/webapp/WEB-INF/jsp/peoplesearch.jsp

@@ -42,7 +42,6 @@
 
 <%-- TODO: change these to the 'min' versions (i.e. angular.min.js) --%>
 <pwm:script-ref url="/public/resources/angular/angular.js" />
-<pwm:script-ref url="/public/resources/angular-sanitize/angular-sanitize.js" />
 <pwm:script-ref url="/public/resources/angular-ui-router/release/angular-ui-router.js" />
 <pwm:script-ref url="/public/resources/angular-translate/dist/angular-translate.js" />
 

+ 1 - 1
src/main/webapp/public/index.jsp

@@ -90,7 +90,7 @@
         </a>
         <% } %>
         <% if (index_pwmRequest.getConfig() != null && index_pwmRequest.getConfig().readSettingAsBoolean(PwmSetting.PEOPLE_SEARCH_ENABLE_PUBLIC)) { %>
-        <a id="Button_PeopleSearch" href="<pwm:url addContext="true" url='<%=PwmServletDefinition.PeopleSearch.servletUrl()%>'/>">
+        <a id="Button_PeopleSearch" href="<pwm:url addContext="true" url='/public/peoplesearch'/>">
             <div class="tile">
                 <div class="tile-content">
                     <div class="tile-image pwm-icon-search"></div>

+ 46 - 39
src/main/webapp/public/reference/environment.jsp

@@ -72,12 +72,38 @@
             not recommended.
         </p>
         <h3>Setting the Application Path</h3>
-        The following configuration methods are evaluated by the application in this order until a value is found:
-        <ol>
-            <li><a href="#webxml">Servlet <code>web.xml</code></a></li>
+        The following configuration methods are available:
+        <ul>
+            <li><a href="#envvar">Environment Variable (Recommended)</a></li>
             <li><a href="#property">Java System Property</a></li>
-            <li><a href="#envvar">Environment Variable</a></li>
-        </ol>
+            <li><a href="#webxml">Servlet <code>web.xml</code></a></li>
+        </ul>
+        <h3><a id="envvar">Environment Variable (Recommended)</a></h3>
+        <p>The application will read the <b><%=PwmConstants.PWM_APP_NAME%>_APPLICATIONPATH</b> variable to determine the location of the application path.  Relative paths are not permitted.</p>
+        <p>Because you set this value at the OS level, it will make maintaining and updating the application easier because you will not need to change anything other than applying a new <code>war</code> file.</p>
+        <h3>Linux Example</h3>
+        <div class="codeExample">
+            <code>export <%=PwmConstants.PWM_APP_NAME%>_APPLICATIONPATH='/home/user/<%=PwmConstants.PWM_APP_NAME.toLowerCase()%>-data'</code>
+        </div>
+        <p>This environment variable would typically be set as part of an init or other script file that starts your application server.</p>
+        <h4>Windows Example</h4>
+        <div class="codeExample">
+            <code>set "<%=PwmConstants.PWM_APP_NAME%>_APPLICATIONPATH=c:\<%=PwmConstants.PWM_APP_NAME.toLowerCase()%>-data"</code>
+        </div>
+        <p>This environment variable is typically set as part of a <code>.bat</code> file that starts your application server, or possibly as a system-wide environment variable via the windows control panel.</p>
+        <br/>
+        <h3><a id="property">Java System Property</a></h3>
+        <p>The application will read the java system property <b><%=PwmConstants.PWM_APP_NAME.toLowerCase()%>.applicationPath</b> variable to determine the location of
+            the application path.  Relative paths are not permitted.  These example parameters would be added to the java command
+            line that starts the application server (tomcat) and java process.</p>
+        <h4>Linux Example</h4>
+        <div class="codeExample">
+            <code>-D<%=PwmConstants.PWM_APP_NAME.toLowerCase()%>.applicationPath='/home/user/<%=PwmConstants.PWM_APP_NAME.toLowerCase()%>-data'</code>
+        </div>
+        <h4>Windows Example</h4>
+        <div class="codeExample">
+            <code>-D<%=PwmConstants.PWM_APP_NAME.toLowerCase()%>.applicationPath="c:\<%=PwmConstants.PWM_APP_NAME.toLowerCase()%>-data"</code>
+        </div>
         <h3><a id="webxml">Servlet web.xml</a></h3>
         Modify the servlet <code>WEB-INF/web.xml</code> file.  You must modify the application war file to accomplish this method.  This method accommodates multiple
         applications on a single application server.  File paths must be absolute.
@@ -101,44 +127,11 @@
                 &lt;/context-param&gt;<br/>
             </code>
         </div>
-        <h3><a id="property">Java System Property</a></h3>
-        <p>The application will read the java system property <b><%=PwmConstants.PWM_APP_NAME.toLowerCase()%>.applicationPath</b> variable to determine the location of
-            the application path.  Relative paths are not permitted.  These example parameters would be added to the java command
-            line that starts the application server (tomcat) and java process.</p>
-        <h4>Linux Example</h4>
-        <div class="codeExample">
-            <code>-D<%=PwmConstants.PWM_APP_NAME.toLowerCase()%>.applicationPath='/home/user/<%=PwmConstants.PWM_APP_NAME.toLowerCase()%>-data'</code>
-        </div>
-        <h4>Windows Example</h4>
-        <div class="codeExample">
-            <code>-D<%=PwmConstants.PWM_APP_NAME.toLowerCase()%>.applicationPath="c:\<%=PwmConstants.PWM_APP_NAME.toLowerCase()%>-data"</code>
-        </div>
-        <h3><a id="envvar">Environment Variable</a></h3>
-        <p>The application will read the <b><%=PwmConstants.PWM_APP_NAME%>_APPLICATIONPATH</b> variable to determine the location of the application path.  Relative paths are not permitted.</p>
-        <h3>Linux Example</h3>
-        <div class="codeExample">
-            <code>export <%=PwmConstants.PWM_APP_NAME%>_APPLICATIONPATH='/home/user/<%=PwmConstants.PWM_APP_NAME.toLowerCase()%>-data'</code>
-        </div>
-        <p>This environment variable would typically be set as part of an init or other script file that starts your application server.</p>
-        <h4>Windows Example</h4>
-        <div class="codeExample">
-            <code>set "<%=PwmConstants.PWM_APP_NAME%>_APPLICATIONPATH=c:\<%=PwmConstants.PWM_APP_NAME.toLowerCase()%>-data"</code>
-        </div>
-        <p>This environment variable is typically set as part of a <code>.bat</code> file that starts your application server, or possibly as a system-wide environment variable via the windows control panel.</p>
-        <br/><br/>
-        <h2><code>ApplicationFlags</code></h2>
-        <p>Application flags can be set to enable or disable behaviors in <%=PwmConstants.PWM_APP_NAME%>.   By default, no flags are set.  Setting flags is optional.  Flags are specified as a comma seperated
-            list of values.  Values are case sensitive.  In most cases, you will not need to set an application flag.
-            <table>
-        <tr><td><h3>Flag</h3></td><td><h3>Behavior</h3></td></tr>
-        <tr><td>ManageHttps</td><td>Enable the setting category <code><%=PwmSettingCategory.HTTPS_SERVER.getLabel(PwmConstants.DEFAULT_LOCALE)%></code></td></tr>
-        <tr><td>NoFileLock</td><td>Disable the file lock in the application path directory.</td></tr>
-    </table>
         <h3>Linux Example</h3>
         <div class="codeExample">
             <code>export <%=PwmConstants.PWM_APP_NAME%>_APPLICATIONFLAGS='ManageHttps,NoFileLock'</code>
         </div>
-        <br/><br/>
+        <br/>
         <h1>Environment Variable Names and Servlet Context</h1>
         <p>
             The environment variables listed above, such as <code><%=PwmConstants.PWM_APP_NAME%>_APPLICATIONPATH</code> and <code><%=PwmConstants.PWM_APP_NAME.toLowerCase()%>.applicationPath</code> assume the default context name of
@@ -161,6 +154,20 @@
                 <td><code>/acme2/</code></td><td><code><%=PwmConstants.PWM_APP_NAME.toLowerCase()%>.acme2.applicationPath</code></td><td><code><%=PwmConstants.PWM_APP_NAME%>_ACME2_APPLICATIONPATH</code></td>
             </tr>
         </table>
+        <p>In case of conflict, the application path parameters are evaluated in the following order.</p>
+        <ol>
+            <li><a href="#webxml">Servlet <code>web.xml</code></a></li>
+            <li><a href="#property">Java System Property</a></li>
+            <li><a href="#envvar">Environment Variable (Recommended)</a></li>
+        </ol>
+        <h2><code>ApplicationFlags</code></h2>
+        <p>Application flags can be set to enable or disable behaviors in <%=PwmConstants.PWM_APP_NAME%>.   By default, no flags are set.  Setting flags is optional.  Flags are specified as a comma seperated
+            list of values.  Values are case sensitive.  In most cases, you will not need to set an application flag.
+        <table>
+            <tr><td><h3>Flag</h3></td><td><h3>Behavior</h3></td></tr>
+            <tr><td>ManageHttps</td><td>Enable the setting category <code><%=PwmSettingCategory.HTTPS_SERVER.getLabel(PwmConstants.DEFAULT_LOCALE)%></code></td></tr>
+            <tr><td>NoFileLock</td><td>Disable the file lock in the application path directory.</td></tr>
+        </table>
     </div>
 </div>
 <%@ include file="/WEB-INF/jsp/fragment/footer.jsp" %>

二进制
src/main/webapp/public/resources/favicon.ico


二进制
src/main/webapp/public/resources/favicon.png


+ 3 - 3
src/main/webapp/public/resources/js/configeditor.js

@@ -818,9 +818,9 @@ PWM_CFGEDIT.displaySettingsCategory = function(category) {
     }
     var htmlSettingBody = '';
 
-    if (category == 'LDAP_PROFILE') {
+    if (category == 'LDAP_BASE') {
         htmlSettingBody += '<div style="width: 100%; text-align: center">'
-            + '<button class="btn" id="button-test-LDAP_PROFILE"><span class="btn-icon pwm-icon pwm-icon-bolt"></span>Test LDAP Profile</button>'
+            + '<button class="btn" id="button-test-LDAP_BASE"><span class="btn-icon pwm-icon pwm-icon-bolt"></span>Test LDAP Profile</button>'
             + '</div>';
     } else if (category == 'DATABASE_SETTINGS') {
         htmlSettingBody += '<div style="width: 100%; text-align: center">'
@@ -853,7 +853,7 @@ PWM_CFGEDIT.displaySettingsCategory = function(category) {
         })(loopSetting);
     }
     if (category == 'LDAP_BASE') {
-        PWM_MAIN.addEventHandler('button-test-LDAP_PROFILE', 'click', function(){PWM_CFGEDIT.ldapHealthCheck();});
+        PWM_MAIN.addEventHandler('button-test-LDAP_BASE', 'click', function(){PWM_CFGEDIT.ldapHealthCheck();});
     } else if (category == 'DATABASE_SETTINGS') {
         PWM_MAIN.addEventHandler('button-test-DATABASE_SETTINGS', 'click', function(){PWM_CFGEDIT.databaseHealthCheck();});
     } else if (category == 'SMS_GATEWAY') {

+ 3 - 3
src/test/resources/password/pwm/manual/TestHelper.properties

@@ -23,6 +23,6 @@
 #applicationPath=/home/amb/dsk/t/test-appPath
 #localDBPath=/home/amb/dsk/t/test-appPath/LocalDB
 #configurationFile=/home/amb/dsk/t/test-appPath/PwmConfiguration.xml
-applicationPath=/home/amb/t/appPath
-localDBPath=/home/amb/t/appPath/LocalDB
-configurationFile=/home/amb/t/appPath/PwmConfiguration.xml
+applicationPath=/home/amb/t/appPath/edir
+localDBPath=/home/amb/t/appPath/edir/LocalDB
+configurationFile=/home/amb/t/appPath/edir/PwmConfiguration.xml

部分文件因为文件数量过多而无法显示