fieldmapper and database def for password policies, added password policy to permissi...
authorLlewellyn Marshall <llewellyn.marshall@ncdcr.gov>
Thu, 30 Mar 2023 20:48:51 +0000 (16:48 -0400)
committerLlewellyn Marshall <llewellyn.marshall@ncdcr.gov>
Thu, 13 Apr 2023 20:29:34 +0000 (16:29 -0400)
flesh password policy, show on perm group tree page, fix issues on fm_IDL

grab all password policies in perm group tree editor

function to get password policy

password hint modal

dynamically load password hint in OPAC

move the OPAC password stuff into the myopac pages only since that's the only place where it's relevant.

parity between features on bootstrap and oldschool opacs

18 files changed:
Open-ILS/examples/fm_IDL.xml
Open-ILS/src/eg2/src/app/staff/admin/server/admin-server-splash.component.html
Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.html
Open-ILS/src/eg2/src/app/staff/admin/server/perm-group-tree.component.ts
Open-ILS/src/eg2/src/app/staff/admin/server/routing.module.ts
Open-ILS/src/eg2/src/app/staff/splash.component.html
Open-ILS/src/eg2/src/app/staff/splash.component.ts
Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader.pm
Open-ILS/src/perlmods/lib/OpenILS/WWW/EGCatLoader/Account.pm
Open-ILS/src/sql/Pg/upgrade/xxxx.schema.password_policy.sql [new file with mode: 0644]
Open-ILS/src/templates-bootstrap/opac/myopac/main.tt2
Open-ILS/src/templates-bootstrap/opac/myopac/update_password_msg.tt2
Open-ILS/src/templates/opac/myopac/update_password_msg.tt2
Open-ILS/src/templates/opac/parts/config.tt2
Open-ILS/src/templates/opac/parts/myopac/main_base.tt2
Open-ILS/src/templates/staff/circ/patron/t_edit.tt2
Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js

index 9823a50..68ec282 100644 (file)
@@ -8402,10 +8402,12 @@ SELECT  usr,
                        <field reporter:label="Required Permission" name="application_perm" reporter:datatype="text"/>
                        <field reporter:label="Is User Group" name="usergroup" reporter:datatype="bool"/>
                        <field reporter:label="Hold Priority" name="hold_priority" reporter:datatype="int"/>
+            <field reporter:label="Password Policy" name="password_policy" reporter:datatype="link"/>
                </fields>
                <links>
                        <link field="parent" reltype="has_a" key="id" map="" class="pgt"/>
                        <link field="children" reltype="has_many" key="parent" map="" class="pgt"/>
+                       <link field="password_policy" reltype="has_a" key="id" map="" class="cpp"/>
                </links>
         <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
             <actions>
@@ -14121,6 +14123,24 @@ SELECT  usr,
                </permacrud>
        </class>
 
+       <class id="cpp" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::password_policy" oils_persist:tablename="config.password_policy" reporter:label="Password Policy">
+               <fields oils_persist:primary="id" oils_persist:sequence="config.password_policy_id_seq">
+                       <field name="id" reporter:selector="name" reporter:datatype="id" reporter:label="ID"/>
+            <field name="name"  reporter:datatype="text" oils_persist:i18n="true" reporter:label="Name"/>
+            <field name="hint"  reporter:datatype="text" oils_persist:i18n="true" reporter:label="Password Hint"/>
+            <field name="regex"  reporter:datatype="text" oils_persist:i18n="true" reporter:label="Regular Expression"/>
+            <field name="max_age"  reporter:datatype="int" oils_persist:i18n="true" reporter:label="Maximum Password Age"/>
+               </fields>
+               <permacrud xmlns="http://open-ils.org/spec/opensrf/IDL/permacrud/v1">
+                       <actions>
+                               <create permission="ADMIN_PASSWORD_POLICY" global_required="true"/>
+                               <retrieve permission="ADMIN_PASSWORD_POLICY" global_required="true"/>
+                               <update permission="ADMIN_PASSWORD_POLICY" global_required="true"/>
+                               <delete permission="ADMIN_PASSWORD_POLICY" global_required="true"/>
+                       </actions>
+               </permacrud>
+       </class>
+
     <class id="cusppet" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="config::ui_staff_portal_page_entry_type" oils_persist:tablename="config.ui_staff_portal_page_entry_type" reporter:label="Portal Page Entry Type">
         <fields oils_persist:primary="code">
             <field name="code" reporter:label="Entry Type Code" reporter:datatype="text" reporter:selector="label" oils_obj:required="true"/>
index c6a4108..9efbf54 100644 (file)
@@ -85,6 +85,8 @@
       routerLink="/staff/admin/server/actor/org_unit_type"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Organizational Units"  
       routerLink="/staff/admin/server/actor/org_unit"></eg-link-table-link>
+    <eg-link-table-link i18n-label label="Password Policies"  
+      routerLink="/staff/admin/server/config/password_policy"></eg-link-table-link>    
     <eg-link-table-link i18n-label label="Permission Groups"  
       routerLink="/staff/admin/server/permission/grp_tree"></eg-link-table-link>
     <eg-link-table-link i18n-label label="Permissions"  
index 8fd7349..7b295ec 100644 (file)
               <div class="col-lg-8 font-weight-bold">
                 {{selected.callerData.application_perm()}}
               </div>
+            </div>            
+            <div class="row">
+              <div class="col-lg-4">
+                <label i18n>Password Policy: </label>
+              </div>
+              <div class="col-lg-8 font-weight-bold">
+                {{selected.callerData.password_policy() ? passwordPolicyById(selected.callerData.password_policy()).name() : "No password policy"}}
+              </div>
             </div>
             <div class="row">
               <div class="col-lg-4">
index 5c8b083..9baab9e 100644 (file)
@@ -24,6 +24,7 @@ export class PermGroupTreeComponent implements OnInit {
     tree: Tree;
     selected: TreeNode;
     permissions: IdlObject[];
+    passwordPolicyIDMap: {[id: number]: IdlObject};
     permIdMap: {[id: number]: IdlObject};
     permEntries: ComboboxEntry[];
     permMaps: IdlObject[];
@@ -54,7 +55,8 @@ export class PermGroupTreeComponent implements OnInit {
         this.permissions = [];
         this.permEntries = [];
         this.permMaps = [];
-        this.permIdMap = {};
+        this.permIdMap = {}; 
+        this.passwordPolicyIDMap = {};
     }
 
 
@@ -65,6 +67,8 @@ export class PermGroupTreeComponent implements OnInit {
         await this.loadPermissions();
         this.loadProgress.increment();
         await this.loadPermMaps();
+        this.loadProgress.increment();        
+        await this.loadCpp();
         this.loadProgress.increment();
         this.setOrgDepths();
         this.loadProgress.increment();
@@ -128,6 +132,14 @@ export class PermGroupTreeComponent implements OnInit {
         return this.pcrud.search('pgt', {parent: null},
             {flesh: -1, flesh_fields: {pgt: ['children']}}
         ).pipe(map(pgtTree => this.ingestPgtTree(pgtTree))).toPromise();
+    }    
+    
+    async loadCpp(): Promise<any> {
+        return this.pcrud.retrieveAll('cpp', {order_by: {cpp: 'id'}})
+        .pipe(map(pol => {
+            this.loadProgress.increment();
+            this.passwordPolicyIDMap[+pol.id()] = pol;
+        })).toPromise();
     }
 
     async loadPermissions(): Promise<any> {
@@ -196,6 +208,10 @@ export class PermGroupTreeComponent implements OnInit {
     permById(id: number): IdlObject {
         return this.permIdMap[id];
     }
+    
+    passwordPolicyById(id: number): IdlObject {
+        return this.passwordPolicyIDMap[id];
+    }
 
     // Returns true if the perm map belongs to an ancestore of the
     // currently selected group.
index caadbcb..dd0283e 100644 (file)
@@ -29,6 +29,14 @@ const routes: Routes = [{
         fieldOrder: 'name,owner,ceiling_date,forceto'
     }]
 }, {
+    path: 'config/password_policy',
+    component: BasicAdminPageComponent,
+    data: [{
+        schema: 'config',
+        table: 'password_policy',
+        fieldOrder: 'id,name,regex,max_age'
+    }]
+}, {
     path: 'config/print_template',
     component: PrintTemplateComponent
 }, {
index 9f13368..7fd1c79 100644 (file)
     </div>
   </div>
 
-  <div *ngIf="passwordExpireAge && passwordAge >= (passwordExpireAge - 7)" class="alert alert-danger row">
-         <p i18n>Your password is <b>{{passwordAge}} days</b> old. It is recommended that passwords be updated every <b>{{passwordExpireAge}} days</b>.</p> 
-      <p *ngIf="!canSelfUpdate" i18n>
-        Please contact an administrator to have your password changed or change your password through the OPAC.
-      </p>
-      <div *ngIf="canSelfUpdate">
-      <br>
+  <div *ngIf="passwordExpireAge && passwordAge >= (passwordExpireAge - 7)" class="alert alert-danger grid">
+         <div class="row">
+          <p i18n>Your password is <b>{{passwordAge}} days</b> old. It is recommended that passwords be updated every <b>{{passwordExpireAge}} days</b>.</p> 
+          <p *ngIf="!canSelfUpdate" i18n>
+            Please contact an administrator to have your password changed or change your password through the OPAC.
+          </p>
+      </div>
+      <div class="row" *ngIf="canSelfUpdate">
         <a class="btn btn-warning btn-block" target="_top" href="{{selfUpdateLink}}" i18n> 
         Update Password
         </a>
index 69626d7..6b87635 100644 (file)
@@ -100,12 +100,21 @@ export class StaffSplashComponent implements OnInit {
             }
         );
 
-               //get the maximum password age
-               this.org.settings(['auth.password_expire_age'])
-               .then(settings => {
-                       this.passwordExpireAge = Number(settings['auth.password_expire_age']);
-                       console.log('password expire age: ', settings);
-               });
+               //get the password expire age for the current user              
+        this.net.request(
+            'open-ils.actor',
+            'open-ils.actor.get_password_expire_age',
+            this.auth.token(),
+            this.auth.user().id()).subscribe(
+            (res) => {
+                console.log('password expire age: ', res);
+                this.passwordExpireAge = Number(res);
+            },
+            (err) => {
+                console.error('splash', err);
+            }
+        );
+        
                //get the age of the current user's password            
         this.net.request(
             'open-ils.actor',
index d9ee1a1..851bb86 100644 (file)
@@ -5066,6 +5066,96 @@ sub get_password_last_edit_age {
 }
 
 __PACKAGE__->register_method(
+    method   => "get_password_expire_age",
+    api_name => "open-ils.actor.get_password_expire_age",
+    signature => {
+        desc => "Finds the number of days before a user's password should be updated.",
+        params => [
+            {desc => 'Authentication token',  type => 'string'},
+            {desc => 'Patron ID',             type => 'number'},
+        ],
+        return => {desc => 'Number of days before password update is required'}
+    }
+);
+
+sub get_password_expire_age {
+    my( $self, $client, $auth, $patron_id, $ref_time ) = @_;
+    my $e = new_editor(authtoken => $auth);
+    return $e->event unless $e->checkauth;
+    my $patron = $e->retrieve_actor_user($patron_id);
+    
+    #return unless the requestor is either the patron in question, or can view users at that patron's home ou
+    unless($patron && ($patron_id == $e->requestor->id || $e->allowed('VIEW_USER', $patron->home_ou))) {
+         return $e->event;
+    }
+
+    my $cpp = $e->json_query({
+        select => {cpp => ['max_age']},
+        from => {pgt => 'cpp'},
+        where => {
+            '+pgt' => {
+                id => $patron->profile
+            }
+        }
+    });
+
+    if(defined $cpp){
+        my $max_age = $cpp->[0]->{'max_age'};        
+        if($max_age){
+            return int($max_age);            
+        }
+    }
+    #there's either no password policy or no max_age on the password policy use 
+    return $U->ou_ancestor_setting_value(
+        $patron->home_ou, 'auth.password_expire_age') || 0;  
+}
+
+__PACKAGE__->register_method(
+    method   => "get_password_hint",
+    api_name => "open-ils.actor.get_password_hint",
+    signature => {
+        desc => "Finds the password hint for a user based on their password policy.",
+        params => [
+            {desc => 'Authentication token',  type => 'string'},
+            {desc => 'Patron ID',             type => 'number'},
+        ],
+        return => {desc => 'password policy hint'}
+    }
+);
+
+sub get_password_hint {
+    my( $self, $client, $auth, $patron_id, $ref_time ) = @_;
+    my $e = new_editor(authtoken => $auth);
+    return $e->event unless $e->checkauth;
+    my $patron = $e->retrieve_actor_user($patron_id);
+    
+    #return unless the requestor is either the patron in question, or can view users at that patron's home ou
+    unless($patron && ($patron_id == $e->requestor->id || $e->allowed('VIEW_USER', $patron->home_ou))) {
+         return $e->event;
+    }
+
+    my $cpp = $e->json_query({
+        select => {cpp => ['hint']},
+        from => {pgt => 'cpp'},
+        where => {
+            '+pgt' => {
+                id => $patron->profile
+            }
+        }
+    });
+
+    if(defined $cpp){
+        my $hint = $cpp->[0]->{'hint'};        
+        if($hint){
+            return $hint;            
+        }
+    }
+    
+    #there's either no password policy or no hint on the password policy 
+    return "";  
+}
+
+__PACKAGE__->register_method(
     method   => 'address_alert_test',
     api_name => 'open-ils.actor.address_alert.test',
     signature => {
index cbe6ac0..2ff4f83 100644 (file)
@@ -383,9 +383,6 @@ sub load_common {
             $ctx->{authtoken} = $e->authtoken;
             $ctx->{authtime} = $e->authtime;
             $ctx->{user} = $e->requestor;
-            $ctx->{password_age} = int($U->simplereq(
-                'open-ils.actor',
-                'open-ils.actor.get_password_age', $e->authtoken, $ctx->{user}->id));
             my $card = $self->editor->retrieve_actor_card($ctx->{user}->card);
             $ctx->{active_card} = (ref $card) ? $card->barcode : undef;
             $ctx->{place_unfillable} = 1 if $e->requestor->wsid && $e->allowed('PLACE_UNFILLABLE_HOLD', $e->requestor->ws_ou);
index 3bc5b42..de31299 100644 (file)
@@ -58,6 +58,32 @@ sub prepare_extended_user_info {
     return;
 }
 
+sub prepare_user_password_info {
+    my $self = shift;
+    my $e = $self->editor;
+    my $usr = $self->ctx->{user};
+    
+    my $cpp = $e->json_query({
+        select => {cpp => ['hint','max_age','regex']},
+        from => {pgt => 'cpp'},
+        where => {
+            '+pgt' => {
+                id => $usr->profile
+            }
+        }
+    })->[0];    
+
+    if(defined $cpp){
+       $self->ctx->{password_hint} = $cpp->{'hint'};        
+       $self->ctx->{password_age} = $cpp->{'max_age'};        
+       $self->ctx->{password_regex} = $cpp->{'regex'};        
+    }
+
+    $self->ctx->{password_expire_age} = int($U->simplereq(
+        'open-ils.actor',
+        'open-ils.actor.get_password_expire_age', $e->authtoken, $usr->id));
+}
+
 # Given an event returned by a failed attempt to create a hold, do we have
 # permission to override?  XXX Should the permission check be scoped to a
 # given org_unit context?
@@ -2643,6 +2669,7 @@ sub load_myopac_main {
             pub => 't'
         })
     );
+    $self->prepare_user_password_info;
     return $self->prepare_fines($limit, $offset) || Apache2::Const::OK;
 }
 
@@ -2811,6 +2838,8 @@ sub load_myopac_update_password {
     my $e = $self->editor;
     my $ctx = $self->ctx;
 
+    $self->prepare_user_password_info;
+    
     return Apache2::Const::OK
         unless $self->cgi->request_method eq 'POST';
 
@@ -2828,7 +2857,7 @@ sub load_myopac_update_password {
         return Apache2::Const::OK;             
        }
 
-    my $pw_regex = $ctx->{get_org_setting}->($e->requestor->home_ou, 'global.password_regex');
+    my $pw_regex = $ctx->{password_regex} || $ctx->{get_org_setting}->($e->requestor->home_ou, 'global.password_regex');
 
     if(!$pw_regex) {
         # This regex duplicates the JSPac's default "digit, letter, and 7 characters" rule
diff --git a/Open-ILS/src/sql/Pg/upgrade/xxxx.schema.password_policy.sql b/Open-ILS/src/sql/Pg/upgrade/xxxx.schema.password_policy.sql
new file mode 100644 (file)
index 0000000..be7fcb1
--- /dev/null
@@ -0,0 +1,21 @@
+BEGIN;
+
+--SELECT evergreen.upgrade_deps_block_check('XXXX', :eg_version);
+
+CREATE TABLE config.password_policy (
+    id SERIAL PRIMARY KEY,    
+    name TEXT NOT NULL,
+       hint TEXT,
+    regex TEXT,
+    max_age INT,
+       CONSTRAINT policy_name_unique UNIQUE (name)
+);
+
+ALTER TABLE permission.grp_tree ADD COLUMN password_policy BIGINT REFERENCES config.password_policy(id); 
+
+CREATE INDEX cpp_id_idx ON config.password_policy (id);
+--INSERT INTO permission.perm_list ( id, code, description ) VALUES
+-- ( XX, 'ADMIN_PASSWORD_POLICY', oils_i18n_gettext( XX, 
+--    'Allow a user to view and modify password policies', 'ppl', 'description' ));
+
+COMMIT;
\ No newline at end of file
index 4370012..4634c79 100755 (executable)
             <h3>[% l('My Account Summary') %]</h3>
                 <div class="row">
                     <br>
-                    [% IF ctx.password_age && ctx.password_age_reminder && ctx.disable_password_change != 'true' %]
-                    [% need_password_change = ctx.password_age == -1 || ctx.password_age >= ctx.password_age_reminder %]
+                    [% IF ctx.password_age && ctx.password_expire_age && ctx.disable_password_change != 'true' %]
+                    [% need_password_change = ctx.password_age == -1 || ctx.password_age >= ctx.password_expire_age %]
                     <div class="col-12">
                             [% IF ctx.password_age == -1 %]
                             [% l('You have never changed your password. Please consider updating your password.') %]
-                            [% ELSIF ctx.password_age >= (ctx.password_age_reminder - 7) %]
-                            [% l('Your password is <b>[_1]</b> days old.',ctx.password_age) %][%- IF !need_password_change %] [% l('You will be asked to change your password soon.') %][%- ELSE %] [% l('It is recommended to update your password every <b>[_1]</b> days. Please consider updating your password.',ctx.password_age_reminder) %][% END %]
+                            [% ELSIF ctx.password_age >= (ctx.ctx.password_expire_age - 7) %]
+                            [% l('Your password is <b>[_1]</b> days old.',ctx.password_age) %][%- IF !need_password_change %] [% l('You will be asked to change your password soon.') %][%- ELSE %] [% l('It is recommended to update your password every <b>[_1]</b> days. Please consider updating your password.',ctx.ctx.password_expire_age) %][% END %]
                             [% END %]
                             [% IF need_password_change %]
                             <br>
index 23d5914..555192f 100755 (executable)
@@ -1,3 +1,7 @@
 <div class="password_message">
-[% l('Note: The password must be at least 7 characters in length, contain at least one letter (a-z/A-Z), and contain at least one number.'); %]
+[% IF ctx.password_hint != "" %]
+    [% ctx.password_hint %]
+[% ELSE %]
+    [% l('Note: The password must be at least 7 characters in length, contain at least one letter (a-z/A-Z), and contain at least one number.'); %]
+[% END %]
 </div>
\ No newline at end of file
index 23d5914..555192f 100644 (file)
@@ -1,3 +1,7 @@
 <div class="password_message">
-[% l('Note: The password must be at least 7 characters in length, contain at least one letter (a-z/A-Z), and contain at least one number.'); %]
+[% IF ctx.password_hint != "" %]
+    [% ctx.password_hint %]
+[% ELSE %]
+    [% l('Note: The password must be at least 7 characters in length, contain at least one letter (a-z/A-Z), and contain at least one number.'); %]
+[% END %]
 </div>
\ No newline at end of file
index da7c15d..fbd8b4e 100644 (file)
@@ -284,13 +284,3 @@ contents_truncate_length = 50;
 
 # Edit parts/record/contents.tt2 to designate character length on a field-by-
 # field basis for notes.
-
-
-##############################################################################
-# Password Reminder Settings
-##############################################################################
-
-# days since last password change to start reminding a patron to change their password
-# commenting out this line will disable the reminder
-ctx.password_age_reminder = ctx.get_org_setting(ctx.physical_loc || 1, 'auth.password_expire_age');
-%]
index 3c1271b..cf7d297 100644 (file)
             %]
             <span class="alert">[% l("Your library card expired on [_1]. Please contact a librarian to resolve this issue.", fmt_expire_date) %]</span>
             [% END %]
+            [% IF ctx.password_age && ctx.password_expire_age && ctx.disable_password_change != 'true' %]
+            [% need_password_change = ctx.password_age == -1 || ctx.password_age >= ctx.password_expire_age %]
+            <span class="alert">
+                    [% IF ctx.password_age == -1 %]
+                    [% l('You have never changed your password. Please consider updating your password.') %]
+                    [% ELSIF ctx.password_age >= (ctx.ctx.password_expire_age - 7) %]
+                    [% l('Your password is <b>[_1]</b> days old.',ctx.password_age) %][%- IF !need_password_change %] [% l('You will be asked to change your password soon.') %][%- ELSE %] [% l('It is recommended to update your password every <b>[_1]</b> days. Please consider updating your password.',ctx.ctx.password_expire_age) %][% END %]
+                    [% END %]
+                    [% IF need_password_change %]
+                    <a href='update_password'
+                    title="[% l('Change Password') %]"><i class="fas fa-user-cog"></i> [% l("Change Password") %]</a>
+                    <br>
+                    [% END %]
+            </span>
+            [% END %] 
             </div>
             <table class="acct_sum_table" title="[% l('Account Summary') %]">
                 <tr>
index cf30d2d..2bd813d 100644 (file)
@@ -218,6 +218,8 @@ within the "form" by name for validation.
   [% draw_field_label('au', 'passwd') %]
   [% draw_form_input('au', 'passwd'); %]
   <div class="col-md-6 patron-reg-example">
+    <button class="btn btn-default" 
+    ng-click="password_hint_dialog()" ng-show="password_hint !== ''">[% l('Password Hint') %]</button>
     <button class="btn btn-default" ng-click="generate_password()">
       [% l('Generate Password') %]</button>
     <button class="btn btn-default" ng-show="!patron.isnew"
index 3da947c..65a8569 100644 (file)
@@ -6,6 +6,7 @@ angular.module('egCoreMod')
 
     var service = {
         field_doc : {},            // config.idl_field_doc
+        passwd_pol : {},           // password policy map
         profiles : [],             // permission groups
         profile_entries : [],      // permission gorup display entries
         edit_profiles : [],        // perm groups we can modify
@@ -41,6 +42,7 @@ angular.module('egCoreMod')
             // These are fetched with every instance of the app.
             common_data = [
                 service.get_field_doc(),
+                service.get_password_policies(),
                 service.get_perm_groups(),
                 service.get_perm_group_entries(),
                 service.get_ident_types(),
@@ -514,6 +516,23 @@ angular.module('egCoreMod')
             );
         }
     }
+    
+    service.get_password_policies = function() {
+        if (egCore.env.cpp) {
+            angular.forEach(egCore.env.cpp.list, function(pol) {
+                passwd_pol[pol.id] = pol;
+            }); 
+        } else {
+            return egCore.pcrud.retrieveAll('cpp', {}, {atomic : true}).then(
+                function(pols) {
+                    egCore.env.absorbList(pols, 'cpp')
+                    angular.forEach(egCore.env.cpp.list, function(p) {
+                        service.passwd_pol[p.id()] = p;
+                    });                
+                }
+            );
+        }
+    }
 
     service.searchPermGroupEntries = function(org) {
         return egCore.pcrud.search('pgtde', {org: org, parent: null},
@@ -1423,6 +1442,7 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
         $scope.field_doc = prs.field_doc;
         $scope.edit_profiles = prs.edit_profiles;
         $scope.edit_profile_entries = prs.edit_profile_entries;
+        $scope.passwd_pol = prs.passwd_pol;
         $scope.ident_types = prs.ident_types;
         $scope.locales = prs.locales;
         $scope.net_access_levels = prs.net_access_levels;
@@ -1437,6 +1457,7 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
         $scope.stage_user = prs.stage_user;
         $scope.stage_user_requestor = prs.stage_user_requestor;
         $scope.password_age = prs.password_age;
+        $scope.password_hint = "";
 
         $scope.user_settings = prs.user_settings;
         prs.user_settings = {};
@@ -1466,6 +1487,7 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
 
         prs.set_field_patterns(field_patterns);
         apply_username_regex();
+        apply_password_regex();
 
         add_date_watchers();
 
@@ -1692,6 +1714,7 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
         $scope.patron.profile = grp;
         $scope.set_expire_date();
         $scope.field_modified();
+        apply_password_regex();
     }
 
     $scope.invalid_profile = function() {
@@ -1846,6 +1869,31 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
         });
     }
 
+    $scope.password_hint_dialog = function() {
+        $uibModal.open({
+            templateUrl: './share/t_confirm_dialog',
+            backdrop: 'static',
+            controller: 
+                   ['$scope','$uibModalInstance','title','message',
+            function($scope , $uibModalInstance , title, message) {
+                // scope here is the modal-level scope
+                $scope.title = title;
+                $scope.message = message;
+                $scope.ok = function() { $uibModalInstance.dismiss() }
+                $scope.cancel = function () { $uibModalInstance.dismiss() }
+            }],
+            resolve : {
+                title : function() {
+                    // scope here is the controller-level scope
+                    return "Password Hint";
+                },
+                message : function() {
+                    return $scope.password_hint;
+                }
+            }
+        });
+    }
+
     $scope.cards_dialog = function() {
         $uibModal.open({
             templateUrl: './circ/patron/t_patron_cards_dialog',
@@ -2136,7 +2184,28 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
     $scope.$watch('reg_form.$pristine', function(newVal, oldVal) {
         if (!newVal) egUnloadPrompt.attach($scope);
     });
-
+    function apply_password_regex() {
+        var cpp = $scope.passwd_pol[$scope.patron.profile.password_policy()];
+        var regex = $scope.org_settings['global.password_regex'];
+        $scope.password_hint = "";
+        if(cpp && cpp.regex()){
+            field_patterns.au.passwd = 
+                new RegExp(cpp.regex());
+                       $scope.password_hint = cpp.hint();
+        }
+        else if (regex) {
+            // use library setting if CPP has nothing
+            field_patterns.au.passwd = 
+                new RegExp(regex);                     
+        }
+        else{
+            // if there's no setting we need a default regex
+            field_patterns.au.passwd = 
+                new RegExp('.*');
+        }
+    }
+       
     // username regex (if present) must be removed any time
     // the username matches the barcode to avoid firing the
     // invalid field handlers.
@@ -2229,6 +2298,9 @@ function($scope , $routeParams , $q , $uibModal , $window , egCore ,
                 $scope.barcode_changed(value);
                 apply_username_regex();
                 break;
+            case 'profile':
+                apply_password_regex();
+                break;
             case 'opac.default_phone':
                 if (normalizePhone(value) !== normalizePhone($scope.hold_rel_contacts.default_phone.old)){
                     $scope.hold_rel_contacts.default_phone.newval = value;