<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>
</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"/>
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"
<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">
tree: Tree;
selected: TreeNode;
permissions: IdlObject[];
+ passwordPolicyIDMap: {[id: number]: IdlObject};
permIdMap: {[id: number]: IdlObject};
permEntries: ComboboxEntry[];
permMaps: IdlObject[];
this.permissions = [];
this.permEntries = [];
this.permMaps = [];
- this.permIdMap = {};
+ this.permIdMap = {};
+ this.passwordPolicyIDMap = {};
}
await this.loadPermissions();
this.loadProgress.increment();
await this.loadPermMaps();
+ this.loadProgress.increment();
+ await this.loadCpp();
this.loadProgress.increment();
this.setOrgDepths();
this.loadProgress.increment();
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> {
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.
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
}, {
</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>
}
);
- //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',
}
__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 => {
$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);
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?
pub => 't'
})
);
+ $self->prepare_user_password_info;
return $self->prepare_fines($limit, $offset) || Apache2::Const::OK;
}
my $e = $self->editor;
my $ctx = $self->ctx;
+ $self->prepare_user_password_info;
+
return Apache2::Const::OK
unless $self->cgi->request_method eq 'POST';
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
--- /dev/null
+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
<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>
<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
<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
# 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');
-%]
%]
<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>
[% 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"
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
// 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(),
);
}
}
+
+ 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},
$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;
$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 = {};
prs.set_field_patterns(field_patterns);
apply_username_regex();
+ apply_password_regex();
add_date_watchers();
$scope.patron.profile = grp;
$scope.set_expire_date();
$scope.field_modified();
+ apply_password_regex();
}
$scope.invalid_profile = function() {
});
}
+ $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',
$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.
$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;