1605 lines
53 KiB
JavaScript
Executable File
1605 lines
53 KiB
JavaScript
Executable File
/*
|
|
* This file is part of Cockpit.
|
|
*
|
|
* Copyright (C) 2013 Red Hat, Inc.
|
|
*
|
|
* Cockpit is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Cockpit 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
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import 'polyfills'; // once per application
|
|
|
|
import $ from 'jquery';
|
|
import cockpit from 'cockpit';
|
|
import moment from "moment";
|
|
|
|
import React from 'react';
|
|
import ReactDOM from 'react-dom';
|
|
import { mustache } from 'mustache';
|
|
import * as authorized_keys from './authorized-keys.js';
|
|
|
|
import 'patterns';
|
|
import 'bootstrap-datepicker/dist/js/bootstrap-datepicker';
|
|
import 'form-layout.scss';
|
|
|
|
import { Badge } from '@patternfly/react-core';
|
|
|
|
moment.locale(cockpit.language);
|
|
|
|
const _ = cockpit.gettext;
|
|
const C_ = cockpit.gettext;
|
|
|
|
function passwd_self(old_pass, new_pass) {
|
|
var old_exps = [
|
|
/Current password: $/,
|
|
/.*\(current\) UNIX password: $/,
|
|
];
|
|
var new_exps = [
|
|
/.*New password: $/,
|
|
/.*Retype new password: $/,
|
|
/.*Enter new \w*\s?password: $/,
|
|
/.*Retype new \w*\s?password: $/
|
|
];
|
|
var bad_exps = [
|
|
/.*BAD PASSWORD:.*/
|
|
];
|
|
var too_new_exps = [
|
|
/.*must wait longer to change.*/
|
|
];
|
|
|
|
var dfd = cockpit.defer();
|
|
var buffer = "";
|
|
var sent_new = false;
|
|
var failure = _("Old password not accepted");
|
|
var i;
|
|
|
|
var proc;
|
|
var timeout = window.setTimeout(function() {
|
|
failure = _("Prompting via passwd timed out");
|
|
proc.close("terminated");
|
|
}, 10 * 1000);
|
|
|
|
proc = cockpit.spawn(["/usr/bin/passwd"], { pty: true, environ: ["LC_ALL=C"], err: "out" })
|
|
.always(function() {
|
|
window.clearInterval(timeout);
|
|
})
|
|
.done(function() {
|
|
dfd.resolve();
|
|
})
|
|
.fail(function(ex) {
|
|
if (ex.exit_status)
|
|
ex = new Error(failure);
|
|
dfd.reject(ex);
|
|
})
|
|
.stream(function(data) {
|
|
buffer += data;
|
|
for (i = 0; i < old_exps.length; i++) {
|
|
if (old_exps[i].test(buffer)) {
|
|
buffer = "";
|
|
this.input(old_pass + "\n", true);
|
|
return;
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < too_new_exps.length; i++) {
|
|
if (too_new_exps[i].test(buffer)) {
|
|
buffer = "";
|
|
failure = _("You must wait longer to change your password");
|
|
this.input("\n", true);
|
|
return;
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < new_exps.length; i++) {
|
|
if (new_exps[i].test(buffer)) {
|
|
buffer = "";
|
|
this.input(new_pass + "\n", true);
|
|
failure = _("Failed to change password");
|
|
sent_new = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (sent_new)
|
|
for (i = 0; i < bad_exps.length; i++) {
|
|
if (bad_exps[i].test(buffer)) {
|
|
failure = _("New password was not accepted");
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
|
|
return dfd.promise();
|
|
}
|
|
|
|
function passwd_change(user, new_pass) {
|
|
var dfd = cockpit.defer();
|
|
|
|
cockpit.spawn(["chpasswd"], { superuser: "require", err: "out" })
|
|
.input(user + ":" + new_pass)
|
|
.done(function() {
|
|
dfd.resolve();
|
|
})
|
|
.fail(function(ex, response) {
|
|
if (ex.exit_status) {
|
|
console.log(ex);
|
|
if (response)
|
|
ex = new Error(response);
|
|
else
|
|
ex = new Error(_("Failed to change password"));
|
|
}
|
|
dfd.reject(ex);
|
|
});
|
|
|
|
return dfd.promise();
|
|
}
|
|
|
|
/*
|
|
* Similar to $.when() but serializes, and accepts functions
|
|
* that return promises
|
|
*/
|
|
function chain(functions) {
|
|
var dfd = cockpit.defer();
|
|
var i = 0;
|
|
|
|
/* Either an array or functions passed */
|
|
if (typeof functions == "function")
|
|
functions = arguments;
|
|
|
|
function step() {
|
|
if (i == functions.length) {
|
|
dfd.resolve();
|
|
return;
|
|
}
|
|
|
|
(functions[i])()
|
|
.done(function() {
|
|
step();
|
|
})
|
|
.fail(function(ex) {
|
|
dfd.reject(ex);
|
|
});
|
|
|
|
i += 1;
|
|
}
|
|
|
|
step();
|
|
return dfd.promise();
|
|
}
|
|
|
|
function parse_passwd_content(content) {
|
|
if (!content) {
|
|
console.warn("Couldn't read /etc/passwd");
|
|
return [];
|
|
}
|
|
|
|
var ret = [];
|
|
var lines = content.split('\n');
|
|
var column;
|
|
|
|
for (var i = 0; i < lines.length; i++) {
|
|
if (!lines[i])
|
|
continue;
|
|
column = lines[i].split(':');
|
|
ret.push({
|
|
name: column[0],
|
|
password: column[1],
|
|
uid: parseInt(column[2], 10),
|
|
gid: parseInt(column[3], 10),
|
|
gecos: (column[4] || '').replace(/,*$/, ''),
|
|
home: column[5] || '',
|
|
shell: column[6] || '',
|
|
});
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
function parse_group_content(content) {
|
|
content = (content || "").trim();
|
|
if (!content) {
|
|
console.warn("Couldn't read /etc/group");
|
|
return [];
|
|
}
|
|
|
|
var ret = [];
|
|
var lines = content.split('\n');
|
|
var column;
|
|
|
|
for (var i = 0; i < lines.length; i++) {
|
|
if (!lines[i])
|
|
continue;
|
|
column = lines[i].split(':');
|
|
ret.push({
|
|
name: column[0],
|
|
password: column[1],
|
|
gid: parseInt(column[2], 10),
|
|
userlist: column[3].split(','),
|
|
});
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
function password_quality(password) {
|
|
var dfd = cockpit.defer();
|
|
|
|
cockpit.spawn('/usr/bin/pwscore', { err: "message" })
|
|
.input(password)
|
|
.done(function(content) {
|
|
var quality = parseInt(content, 10);
|
|
if (quality === 0) {
|
|
dfd.reject(new Error(_("Password is too weak")));
|
|
} else if (quality <= 33) {
|
|
dfd.resolve("weak");
|
|
} else if (quality <= 66) {
|
|
dfd.resolve("okay");
|
|
} else if (quality <= 99) {
|
|
dfd.resolve("good");
|
|
} else {
|
|
dfd.resolve("excellent");
|
|
}
|
|
})
|
|
.fail(function(ex) {
|
|
dfd.reject(new Error(ex.message || _("Password is not acceptable")));
|
|
});
|
|
|
|
return dfd.promise();
|
|
}
|
|
|
|
function is_user_in_group(user, group) {
|
|
for (var i = 0; group.userlist && i < group.userlist.length; i++) {
|
|
if (group.userlist[i] === user)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
class AccountItem extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.click = this.click.bind(this);
|
|
}
|
|
|
|
click(ev) {
|
|
if (!ev)
|
|
return;
|
|
|
|
if (ev.type === 'click' && ev.button !== 0)
|
|
return;
|
|
|
|
if (ev.type === 'keypress' && ev.key !== "Enter")
|
|
return;
|
|
|
|
cockpit.location.go([this.props.name]);
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<li className="cockpit-account" role="presentation" onClick={this.click} onKeyPress={this.click}>
|
|
<div className="cockpit-account-pic pficon pficon-user" />
|
|
<div className="cockpit-account-real-name">{this.props.gecos.split(',')[0]}</div>
|
|
<div className="cockpit-account-user-name">
|
|
<a href={"#/" + this.props.name}>{this.props.name}</a>
|
|
{this.props.current && <Badge className="cockpit-account-badge">{_("Your account")}</Badge>}
|
|
</div>
|
|
</li>
|
|
);
|
|
}
|
|
}
|
|
AccountItem.displayName = 'AccountItem';
|
|
|
|
class AccountList extends React.Component {
|
|
render() {
|
|
var i;
|
|
var items = [];
|
|
for (i in this.props.accounts)
|
|
items.push(React.createElement(AccountItem, Object.assign({
|
|
key: this.props.accounts[i].name,
|
|
current: this.props.current_user === this.props.accounts[i].name
|
|
},
|
|
this.props.accounts[i])));
|
|
return (
|
|
<>
|
|
{items}
|
|
</>
|
|
);
|
|
}
|
|
}
|
|
AccountList.displayName = 'AccountList';
|
|
|
|
function log_unexpected_error(error) {
|
|
console.warn("Unexpected error", error);
|
|
}
|
|
|
|
PageAccounts.prototype = {
|
|
_init: function() {
|
|
this.id = "accounts";
|
|
this.permission = cockpit.permission({ admin: true });
|
|
this.permission.addEventListener("changed", () => this.update());
|
|
},
|
|
|
|
getTitle: function() {
|
|
return C_("page-title", "Accounts");
|
|
},
|
|
|
|
show: function() {
|
|
},
|
|
|
|
setup: function() {
|
|
$('#accounts-create').on('click', $.proxy(this, "create"));
|
|
},
|
|
|
|
enter: function() {
|
|
var self = this;
|
|
|
|
function parse_accounts(content) {
|
|
self.accounts = parse_passwd_content(content);
|
|
self.update();
|
|
}
|
|
|
|
this.handle_passwd = cockpit.file('/etc/passwd');
|
|
|
|
this.handle_passwd.read()
|
|
.done(parse_accounts)
|
|
.fail(log_unexpected_error);
|
|
|
|
this.handle_passwd.watch(parse_accounts);
|
|
},
|
|
|
|
leave: function() {
|
|
if (this.handle_passwd) {
|
|
this.handle_passwd.close();
|
|
this.handle_passwd = null;
|
|
}
|
|
},
|
|
|
|
update: function() {
|
|
if (!this.accounts)
|
|
return;
|
|
|
|
const current_user = this.permission.user;
|
|
this.accounts.sort(function (a, b) {
|
|
if (current_user && current_user.name === a.name) return -1;
|
|
else if (current_user && current_user.name === b.name) return 1;
|
|
else if (!a.gecos) return -1;
|
|
else if (!b.gecos) return 1;
|
|
else return a.gecos.localeCompare(b.gecos);
|
|
});
|
|
|
|
var accounts = this.accounts.filter(function(account) {
|
|
return !((account.uid < 1000 && account.uid !== 0) ||
|
|
account.shell.match(/^(\/usr)?\/sbin\/nologin/) ||
|
|
account.shell === '/bin/false');
|
|
});
|
|
|
|
ReactDOM.render(
|
|
React.createElement(AccountList, { accounts: accounts, current_user: this.permission.user ? this.permission.user.name : "" }),
|
|
document.getElementById('accounts-list')
|
|
);
|
|
},
|
|
|
|
create: function () {
|
|
PageAccountsCreate.accounts = this.accounts;
|
|
$('#accounts-create-dialog').modal('show');
|
|
},
|
|
|
|
go: function (user) {
|
|
cockpit.location.go([user]);
|
|
}
|
|
};
|
|
|
|
function PageAccounts() {
|
|
this._init();
|
|
}
|
|
|
|
PageAccountsCreate.prototype = {
|
|
_init: function() {
|
|
this.id = "accounts-create-dialog";
|
|
this.username_dirty = false;
|
|
},
|
|
|
|
show: function() {
|
|
},
|
|
|
|
setup: function() {
|
|
var self = this;
|
|
$('#accounts-create-cancel').on('click', $.proxy(this, "cancel"));
|
|
$('#accounts-create-create').on('click', $.proxy(this, "create"));
|
|
$('#accounts-create-dialog .check-passwords').on('keydown change', $.proxy(this, "validate"));
|
|
$('#accounts-create-real-name').on('input', $.proxy(this, "suggest_username"));
|
|
$('#accounts-create-user-name').on('input', function() { self.username_dirty = true });
|
|
},
|
|
|
|
enter: function() {
|
|
$('#accounts-create-user-name').val("");
|
|
$('#accounts-create-real-name').val("");
|
|
$('#accounts-create-pw1').val("");
|
|
$('#accounts-create-pw2').val("");
|
|
$('#accounts-create-locked').prop('checked', false);
|
|
$('#accounts-create-password-meter').removeClass("weak okay good excellent");
|
|
$("#accounts-create-dialog").dialog("failure", null);
|
|
this.username_dirty = false;
|
|
},
|
|
|
|
leave: function() {
|
|
},
|
|
|
|
validate: function() {
|
|
var ex;
|
|
var fails = [];
|
|
var pw = $('#accounts-create-pw1').val();
|
|
if ($('#accounts-create-pw2').val() != pw) {
|
|
ex = new Error(_("The passwords do not match"));
|
|
ex.target = "#accounts-create-pw2";
|
|
fails.push(ex);
|
|
}
|
|
if (!$('#accounts-create-user-name').val()) {
|
|
ex = new Error(_("No user name specified"));
|
|
ex.target = '#accounts-create-user-name';
|
|
fails.push(ex);
|
|
}
|
|
if (!$('#accounts-create-real-name').val()) {
|
|
ex = new Error(_("No real name specified"));
|
|
ex.target = '#accounts-create-real-name';
|
|
fails.push(ex);
|
|
}
|
|
|
|
/* The first check is immediately complete */
|
|
var dfd = cockpit.defer();
|
|
if (fails.length)
|
|
dfd.reject(fails);
|
|
else
|
|
dfd.resolve();
|
|
|
|
var promise_password = password_quality(pw)
|
|
.fail(function(ex) {
|
|
ex.target = "#accounts-create-pw2";
|
|
})
|
|
.always(function(arg) {
|
|
var strength = this.state() == "resolved" ? arg : "weak";
|
|
var meter = $("#accounts-create-password-meter")
|
|
.removeClass("weak okay good excellent");
|
|
if (pw)
|
|
meter.addClass(strength);
|
|
var message = $("#accounts-create-password-meter-message");
|
|
if (strength == "excellent") {
|
|
message.text(_("Excellent password"));
|
|
} else {
|
|
message.text("");
|
|
}
|
|
});
|
|
|
|
var promise_username = this.check_username()
|
|
.fail(function(ex) {
|
|
ex.target = "#accounts-create-user-name";
|
|
});
|
|
|
|
// Can't use Promise.all() here, because this promise is passed to
|
|
// dialog(), which expects a promise with a progress() method (see
|
|
// pkg/lib/patterns.js)
|
|
// eslint-disable-next-line cockpit/no-cockpit-all
|
|
return cockpit.all(dfd.promise(), promise_password, promise_username);
|
|
},
|
|
|
|
cancel: function() {
|
|
$('#accounts-create-dialog').modal('hide');
|
|
},
|
|
|
|
create: function() {
|
|
var tasks = [
|
|
() => {
|
|
const dfd = cockpit.defer();
|
|
cockpit.spawn(["/usr/sbin/useradd", "-D"])
|
|
.done(defaults => {
|
|
defaults.split("\n").forEach(item => {
|
|
if (item.indexOf("SHELL=") === 0) {
|
|
this.user_shell = item.split("=")[1] || "";
|
|
}
|
|
});
|
|
dfd.resolve();
|
|
})
|
|
.fail(dfd.resolve); // Don't fail if we cannot read defaults
|
|
return dfd.promise();
|
|
},
|
|
() => {
|
|
var prog = ["/usr/sbin/useradd", "--create-home", "-s", this.user_shell || "/bin/bash"];
|
|
if ($('#accounts-create-real-name').val()) {
|
|
prog.push('-c');
|
|
prog.push($('#accounts-create-real-name').val());
|
|
}
|
|
prog.push($('#accounts-create-user-name').val());
|
|
return cockpit.spawn(prog, { superuser: "require", err: "message" });
|
|
}
|
|
];
|
|
|
|
tasks.push(function change_passwd() {
|
|
return passwd_change($('#accounts-create-user-name').val(), $('#accounts-create-pw1').val());
|
|
});
|
|
|
|
if ($('#accounts-create-locked').prop('checked')) {
|
|
tasks.push(function adjust_locked() {
|
|
return cockpit.spawn([
|
|
"/usr/sbin/usermod",
|
|
$('#accounts-create-user-name').val(),
|
|
"--lock"
|
|
], { superuser: "require", err: "message" });
|
|
});
|
|
}
|
|
|
|
var promise = this.validate()
|
|
.fail(function(ex) {
|
|
$("#accounts-create-password-meter-message").hide();
|
|
$("#accounts-create-dialog").dialog("failure", ex);
|
|
})
|
|
.done(function() {
|
|
promise = chain(tasks);
|
|
$("#accounts-create-dialog").dialog("promise", promise);
|
|
});
|
|
|
|
$("#accounts-create-dialog").dialog("wait", promise);
|
|
},
|
|
|
|
is_valid_char_username: function(c) {
|
|
return (c >= 'a' && c <= 'z') ||
|
|
(c >= 'A' && c <= 'Z') ||
|
|
(c >= '0' && c <= '9') ||
|
|
c == '.' || c == '_' || c == '-';
|
|
},
|
|
|
|
check_username: function() {
|
|
var dfd = cockpit.defer();
|
|
var username = $('#accounts-create-user-name').val();
|
|
|
|
for (var i = 0; i < username.length; i++) {
|
|
if (!this.is_valid_char_username(username[i])) {
|
|
dfd.reject(new Error(
|
|
_("The user name can only consist of letters from a-z, digits, dots, dashes and underscores.")
|
|
));
|
|
return dfd.promise();
|
|
}
|
|
}
|
|
|
|
for (var k = 0; k < PageAccountsCreate.accounts.length; k++) {
|
|
if (PageAccountsCreate.accounts[k].name == username) {
|
|
dfd.reject(new Error(_("This user name already exists")));
|
|
return dfd.promise();
|
|
}
|
|
}
|
|
|
|
dfd.resolve();
|
|
return dfd.promise();
|
|
},
|
|
|
|
suggest_username: function() {
|
|
var self = this;
|
|
|
|
function remove_diacritics(str) {
|
|
var translate_table = {
|
|
a : '[àáâãäå]',
|
|
ae: 'æ',
|
|
c : '[čç]',
|
|
d : 'ď',
|
|
e : '[èéêë]',
|
|
i : '[íìïî]',
|
|
l : '[ĺľ]',
|
|
n : '[ňñ]',
|
|
o : '[òóôõö]',
|
|
oe: 'œ',
|
|
r : '[ŕř]',
|
|
s : 'š',
|
|
t : 'ť',
|
|
u : '[ùúůûűü]',
|
|
y : '[ýÿ]',
|
|
z : 'ž',
|
|
};
|
|
for (var i in translate_table)
|
|
str = str.replace(new RegExp(translate_table[i], 'g'), i);
|
|
|
|
for (var k = 0; k < str.length;) {
|
|
if (!self.is_valid_char_username(str[k]))
|
|
str = str.substr(0, k) + str.substr(k + 1);
|
|
else
|
|
k++;
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
function make_username(realname) {
|
|
var result = "";
|
|
var name = realname.split(' ');
|
|
|
|
if (name.length === 1)
|
|
result = name[0].toLowerCase();
|
|
else if (name.length > 1)
|
|
result = name[0][0].toLowerCase() + name[name.length - 1].toLowerCase();
|
|
|
|
return remove_diacritics(result);
|
|
}
|
|
|
|
if (this.username_dirty)
|
|
return;
|
|
|
|
var username = make_username($('#accounts-create-real-name').val());
|
|
$('#accounts-create-user-name').val(username);
|
|
}
|
|
|
|
};
|
|
|
|
function PageAccountsCreate() {
|
|
this._init();
|
|
}
|
|
|
|
PageAccount.prototype = {
|
|
_init: function(user) {
|
|
this.id = "account-page";
|
|
this.section_id = "accounts";
|
|
this.roles = [];
|
|
this.role_template = $("#role-entry-tmpl").html();
|
|
mustache.parse(this.role_template);
|
|
|
|
this.keys_template = $("#authorized-keys-tmpl").html();
|
|
mustache.parse(this.keys_template);
|
|
this.authorized_keys = null;
|
|
|
|
this.user = user;
|
|
|
|
this.permission = cockpit.permission({ admin: true });
|
|
this.permission.addEventListener("changed", () => this.update_accounts_privileged());
|
|
},
|
|
|
|
getTitle: function() {
|
|
return C_("page-title", "Accounts");
|
|
},
|
|
|
|
update_accounts_privileged: function() {
|
|
$(".accounts-self-privileged").addClass("accounts-privileged");
|
|
|
|
$(".accounts-privileged:not('.accounts-current-account')").update_privileged(
|
|
this.permission, cockpit.format(
|
|
_("The user <b>$0</b> is not permitted to modify accounts"),
|
|
this.permission.user ? this.permission.user.name : '')
|
|
);
|
|
$(".accounts-privileged").find("input")
|
|
.attr('disabled', this.permission.allowed === false);
|
|
|
|
// not exactly a permission, but needs to be adjusted after the above
|
|
if (this.permission.allowed && !this.logged)
|
|
$('#account-logout').attr('disabled', true);
|
|
|
|
// enable fields for current account.
|
|
$(".accounts-current-account").update_privileged(
|
|
{ allowed: true }, ""
|
|
);
|
|
$(".accounts-current-account").find("input")
|
|
.attr('disabled', false);
|
|
|
|
if ($('#account-user-name').text() === 'root' && this.permission.allowed) {
|
|
$("#account-delete").update_privileged({ allowed: false },
|
|
_("Unable to delete root account"));
|
|
$("#account-real-name-wrapper").update_privileged({ allowed: false },
|
|
_("Unable to rename root account"));
|
|
$("#account-real-name").prop('disabled', true);
|
|
}
|
|
},
|
|
|
|
show: function() {
|
|
var self = this;
|
|
$("#account").toggle(!!self.account_id);
|
|
$("#account-failure").toggle(!self.account_id);
|
|
},
|
|
|
|
setup: function() {
|
|
$('#account .breadcrumb a').on("click", function() {
|
|
cockpit.location.go('/');
|
|
});
|
|
|
|
$('#account-real-name').on('change', $.proxy(this, "change_real_name"));
|
|
$('#account-real-name').on('keydown', $.proxy(this, "real_name_edited"));
|
|
$('#account-set-password').on('click', $.proxy(this, "set_password"));
|
|
$('#account-delete').on('click', $.proxy(this, "delete_account"));
|
|
$('#account-logout').on('click', $.proxy(this, "logout_account"));
|
|
$('#account-locked').on('change', $.proxy(this, "change_locked", true, null));
|
|
$('#add-authorized-key').on('click', $.proxy(this, "add_key"));
|
|
$('#add-authorized-key-dialog').on('hidden.bs.modal', function () {
|
|
$("#authorized-keys-text").val("");
|
|
});
|
|
},
|
|
|
|
setup_keys: function (user_name, home_dir) {
|
|
var self = this;
|
|
if (!self.authorized_keys) {
|
|
self.authorized_keys = authorized_keys.instance(user_name, home_dir);
|
|
$(self.authorized_keys).on("changed", function () {
|
|
self.update();
|
|
});
|
|
}
|
|
},
|
|
|
|
remove_key: function (ev) {
|
|
if (!this.authorized_keys)
|
|
return;
|
|
|
|
var key = $(ev.target).data("raw");
|
|
$(".account-remove-key").prop('disabled', true);
|
|
this.authorized_keys.remove_key(key)
|
|
.fail(show_unexpected_error)
|
|
.always(function () {
|
|
$(".account-remove-key").prop('disabled', false);
|
|
});
|
|
},
|
|
|
|
add_key: function () {
|
|
if (!this.authorized_keys) {
|
|
$("#add-authorized-key-dialog").modal('hide');
|
|
return;
|
|
}
|
|
|
|
var key = $("#authorized-keys-text").val();
|
|
var promise = this.authorized_keys.add_key(key);
|
|
$("#add-authorized-key-dialog").dialog("promise", promise);
|
|
},
|
|
|
|
get_user: function() {
|
|
var self = this;
|
|
function parse_user(content) {
|
|
var accounts = parse_passwd_content(content);
|
|
|
|
for (var i = 0; i < accounts.length; i++) {
|
|
if (accounts[i].name !== self.account_id)
|
|
continue;
|
|
|
|
self.account = accounts[i];
|
|
self.setup_keys(self.account.name, self.account.home);
|
|
self.update();
|
|
return;
|
|
}
|
|
|
|
/* no such account find, clear it */
|
|
self.account = null;
|
|
}
|
|
|
|
this.handle_passwd = cockpit.file('/etc/passwd');
|
|
self.handle_shadow = cockpit.file('/etc/shadow', { superuser: "try" });
|
|
var saw_shadow = false;
|
|
|
|
this.handle_passwd.read()
|
|
.done(parse_user)
|
|
.fail(log_unexpected_error);
|
|
|
|
this.handle_passwd.watch(function(content) {
|
|
parse_user(content);
|
|
if (!saw_shadow)
|
|
self.get_expire();
|
|
if (self.account)
|
|
self.get_locked();
|
|
});
|
|
self.handle_shadow.watch(function() {
|
|
saw_shadow = true;
|
|
self.get_expire();
|
|
});
|
|
},
|
|
|
|
get_roles: function() {
|
|
var self = this;
|
|
|
|
var role_groups = {
|
|
wheel: _("Server Administrator"),
|
|
sudo: _("Server Administrator"),
|
|
docker: _("Container Administrator"),
|
|
weldr: _("Image Builder")
|
|
};
|
|
|
|
function parse_groups(content) {
|
|
var groups = parse_group_content(content);
|
|
while (self.roles.length > 0)
|
|
self.roles.pop();
|
|
for (var i = 0; i < groups.length; i++) {
|
|
var name = groups[i].name;
|
|
if (role_groups[name]) {
|
|
self.roles.push({
|
|
name: name,
|
|
desc: role_groups[name],
|
|
id: groups[i].gid,
|
|
member: is_user_in_group(self.account_id, groups[i]),
|
|
});
|
|
}
|
|
}
|
|
$(self).triggerHandler("roles");
|
|
self.update();
|
|
}
|
|
|
|
this.handle_groups = cockpit.file('/etc/group');
|
|
|
|
this.handle_groups.read()
|
|
.done(parse_groups)
|
|
.fail(log_unexpected_error);
|
|
|
|
this.handle_groups.watch(parse_groups);
|
|
},
|
|
|
|
get_last_login: function() {
|
|
var self = this;
|
|
|
|
function parse_last_login(data) {
|
|
data = data.split('\n')[1]; // throw away header
|
|
if (data.length === 0) return null;
|
|
data = data.split(' '); // get last column - separated by spaces
|
|
|
|
if (data[data.length - 1].indexOf('**Never logged in**') > -1)
|
|
return null;
|
|
else
|
|
return new Date(data[data.length - 1]);
|
|
}
|
|
|
|
cockpit.spawn(["/usr/bin/lastlog", "-u", self.account_id], { environ: ["LC_ALL=C"] })
|
|
.done(function (data) {
|
|
self.lastLogin = parse_last_login(data);
|
|
self.update();
|
|
})
|
|
.fail(function() {
|
|
self.lastLogin = null;
|
|
self.update();
|
|
});
|
|
},
|
|
|
|
get_locked: function(update_display) {
|
|
update_display = typeof update_display !== 'undefined' ? update_display : true;
|
|
var dfd = cockpit.defer();
|
|
var self = this;
|
|
|
|
function parse_locked(content) {
|
|
var status = content.split(" ")[1];
|
|
// libuser uses "LK", shadow-utils use "L".
|
|
return status && (status == "LK" || status == "L");
|
|
}
|
|
|
|
cockpit.spawn(["/usr/bin/passwd", "-S", self.account_id], { environ: ["LC_ALL=C"], superuser: "require" })
|
|
.done(function(content) {
|
|
self.locked = parse_locked(content);
|
|
if (update_display)
|
|
self.update();
|
|
dfd.resolve(self.locked);
|
|
})
|
|
.fail(function(error) {
|
|
dfd.reject(error);
|
|
});
|
|
|
|
return dfd.promise();
|
|
},
|
|
|
|
get_logged: function() {
|
|
var self = this;
|
|
if (!self.account_id) {
|
|
self.logged = false;
|
|
self.update();
|
|
return;
|
|
}
|
|
|
|
function parse_logged(content) {
|
|
self.logged = content.length > 0;
|
|
if (!self.logged)
|
|
self.get_last_login();
|
|
else
|
|
self.update();
|
|
}
|
|
|
|
cockpit.spawn(["/usr/bin/w", "-sh", self.account_id])
|
|
.done(parse_logged)
|
|
.fail(log_unexpected_error);
|
|
},
|
|
|
|
get_expire: function() {
|
|
var self = this;
|
|
|
|
function parse_expire(data) {
|
|
var i, line;
|
|
data = data.split('\n');
|
|
|
|
var account_expiration = '';
|
|
var account_date = '';
|
|
|
|
var password_expiration = '';
|
|
var password_days = -1;
|
|
|
|
for (i = 0; i < data.length; i++) {
|
|
line = data[i].split(': ');
|
|
if (line[0] && line[0].indexOf("Password expires") === 0) {
|
|
if (line[1].indexOf("never") === 0) {
|
|
password_expiration = _("Never expire password");
|
|
} else if (line[1].indexOf("password must be changed") === 0) {
|
|
password_expiration = _("Password must be changed");
|
|
} else {
|
|
password_expiration = cockpit.format(_("Require password change on $0"), moment(line[1]).format('LL'));
|
|
}
|
|
} else if (line[0] && line[0].indexOf("Account expires") === 0) {
|
|
if (line[1].indexOf("never") === 0) {
|
|
account_expiration = _("Never lock account");
|
|
} else {
|
|
account_date = new Date(line[1] + " 12:00:00 UTC");
|
|
account_expiration = cockpit.format(_("Lock account on $0"), moment(line[1]).format('LL'));
|
|
}
|
|
} else if (line[0] && line[0].indexOf("Maximum number of days between password change") === 0) {
|
|
password_days = line[1];
|
|
}
|
|
}
|
|
|
|
$('#account-expiration-button').text(account_expiration);
|
|
$('#account-expiration-button').data('expire-date', account_date);
|
|
|
|
$('#password-expiration-button').text(password_expiration);
|
|
$('#password-expiration-button').data('expire-days', password_days);
|
|
|
|
self.update();
|
|
}
|
|
|
|
cockpit.spawn(["/usr/bin/chage", "-l", self.account_id],
|
|
{ environ: ["LC_ALL=C"], err: "message", superuser: "try" })
|
|
.done(function(data) {
|
|
parse_expire(data);
|
|
})
|
|
.fail(function(ex) {
|
|
parse_expire("");
|
|
});
|
|
},
|
|
|
|
enter: function(account_id) {
|
|
this.account_id = account_id;
|
|
this.roles_changed = false;
|
|
|
|
$("#account-real-name").removeAttr("data-dirty");
|
|
$('#password-reset-button').data('account-id', this.account_id);
|
|
$('#password-expiration-button').data('account-id', this.account_id);
|
|
$('#account-expiration-button').data('account-id', this.account_id);
|
|
|
|
this.get_user();
|
|
this.get_roles();
|
|
this.get_locked();
|
|
this.get_logged();
|
|
this.get_expire();
|
|
},
|
|
|
|
leave: function() {
|
|
if (this.handle_passwd) {
|
|
this.handle_passwd.close();
|
|
this.handle_passwd = null;
|
|
}
|
|
|
|
if (this.handle_groups) {
|
|
this.handle_groups.close();
|
|
this.handle_groups = null;
|
|
}
|
|
|
|
if (this.authorized_keys) {
|
|
$(this.authorized_keys).off();
|
|
this.authorized_keys.close();
|
|
this.authorized_keys = null;
|
|
}
|
|
|
|
$('#account-failure').prop("hidden", true);
|
|
},
|
|
|
|
update: function() {
|
|
if (this.account) {
|
|
$('#account').prop("hidden", false);
|
|
$('#account-failure').prop("hidden", true);
|
|
var name = $("#account-real-name");
|
|
|
|
var title_name = this.account.gecos;
|
|
if (title_name)
|
|
title_name = title_name.split(',')[0];
|
|
else
|
|
title_name = this.account.name;
|
|
|
|
$("#account-title").text(title_name);
|
|
if (!name.attr("data-dirty"))
|
|
$('#account-real-name').val(this.account.gecos);
|
|
|
|
$('#account-user-name').text(this.account.name);
|
|
|
|
if (this.logged)
|
|
$('#account-last-login').text(_("Logged In"));
|
|
else if (!this.lastLogin)
|
|
$('#account-last-login').text(_("Never"));
|
|
else
|
|
$('#account-last-login').text(moment(this.lastLogin).format('LLL'));
|
|
|
|
if (typeof this.locked != 'undefined') {
|
|
$('#account-locked').prop('checked', this.locked);
|
|
$('#account-locked').prop('disabled', false);
|
|
} else {
|
|
$('#account-locked').prop('disabled', true);
|
|
}
|
|
|
|
if (this.authorized_keys) {
|
|
var keys = this.authorized_keys.keys;
|
|
var state = this.authorized_keys.state;
|
|
var keys_html = mustache.render(this.keys_template, {
|
|
keys: keys,
|
|
empty: keys.length === 0 && state == "ready",
|
|
denied: state == "access-denied",
|
|
failed: state == "failed",
|
|
});
|
|
$('#account-authorized-keys-list').html(keys_html);
|
|
$(".account-remove-key")
|
|
.on("click", $.proxy(this, "remove_key"));
|
|
$('#account-authorized-keys').show();
|
|
} else {
|
|
$('#account-authorized-keys').hide();
|
|
}
|
|
|
|
if (this.account.uid !== 0) {
|
|
var html = mustache.render(this.role_template,
|
|
{ roles: this.roles, changed: this.roles_changed });
|
|
$('#account-change-roles-roles').html(html);
|
|
$('#account-roles').parents('tr')
|
|
.show();
|
|
$('#account-roles [data-toggle="tooltip"]')
|
|
.tooltip();
|
|
$("#account-change-roles-roles :input")
|
|
.on("change", $.proxy(this, "change_role"));
|
|
} else {
|
|
$('#account-roles')
|
|
.parents('tr')
|
|
.hide();
|
|
}
|
|
$('#account .breadcrumb .active')
|
|
.text(title_name);
|
|
|
|
// check accounts-self-privileged whether account is the same as currently logged in user
|
|
$(".accounts-self-privileged")
|
|
.toggleClass("accounts-current-account",
|
|
this.user.id == this.account.uid);
|
|
} else {
|
|
$('#account').hide();
|
|
$('#account-failure').prop("hidden", false);
|
|
$('#account-real-name').val("");
|
|
$('#account-user-name').text("");
|
|
$('#account-last-login').text("");
|
|
$('#account-locked').prop('checked', false);
|
|
$('#account-change-roles-roles').html("");
|
|
$('#account .breadcrumb .active').text("?");
|
|
}
|
|
this.update_accounts_privileged();
|
|
},
|
|
|
|
change_role: function(ev) {
|
|
var self = this;
|
|
var name = $(ev.target).data("name");
|
|
var id = $(ev.target).data("gid");
|
|
if (!name || !id || !this.account.name)
|
|
return;
|
|
|
|
var proc;
|
|
var checked = $(ev.target).prop('checked');
|
|
var input_elements =
|
|
$('#account button:not([disabled]), #account input:not([disabled]), #account a:not([disabled])');
|
|
input_elements.prop('disabled', true);
|
|
if (checked) {
|
|
proc = cockpit.spawn(["/usr/sbin/usermod", this.account.name, "-G", id, "-a"],
|
|
{ superuser: "require", err: "message" });
|
|
} else {
|
|
proc = cockpit.spawn(["/usr/bin/gpasswd", "-d", this.account.name, name],
|
|
{ superuser: "require", err: "message" });
|
|
}
|
|
|
|
proc.then(function(data) {
|
|
if (!data && checked)
|
|
data = "Added " + self.account.name + " to group " + name;
|
|
else if (!data && !checked)
|
|
data = "Removed " + self.account.name + " from group " + name;
|
|
console.log(data);
|
|
if (self.logged)
|
|
self.roles_changed = true;
|
|
self.update();
|
|
}, show_unexpected_error).finally(function() {
|
|
input_elements.prop('disabled', false);
|
|
});
|
|
},
|
|
|
|
real_name_edited: function() {
|
|
$("#account-real-name").attr("data-dirty", "true");
|
|
},
|
|
|
|
check_role_for_self_mod: function () {
|
|
return (this.account.name == this.user.name || !!this.permission.allowed);
|
|
},
|
|
|
|
change_real_name: function() {
|
|
var self = this;
|
|
|
|
var name = $("#account-real-name");
|
|
name.attr("data-dirty", "true");
|
|
|
|
if (!self.check_role_for_self_mod()) {
|
|
self.update();
|
|
return;
|
|
}
|
|
|
|
// TODO: unwanted chars check
|
|
var value = name.val();
|
|
var input_elements =
|
|
$('#account button:not([disabled]), #account input:not([disabled]), #account a:not([disabled])');
|
|
input_elements.prop('disabled', true);
|
|
cockpit.spawn(["/usr/sbin/usermod", self.account.name, "--comment", value],
|
|
{ superuser: "try", err: "message" })
|
|
.done(function(data) {
|
|
self.account.gecos = value;
|
|
self.update();
|
|
name.removeAttr("data-dirty");
|
|
})
|
|
.fail(show_unexpected_error)
|
|
.finally(function() {
|
|
input_elements.prop('disabled', false);
|
|
});
|
|
},
|
|
|
|
change_locked: function(verify_status, desired_lock_state) {
|
|
desired_lock_state = desired_lock_state !== null
|
|
? desired_lock_state : $('#account-locked').prop('checked');
|
|
var self = this;
|
|
var input_elements =
|
|
$('#account button:not([disabled]), #account input:not([disabled]), #account a:not([disabled])');
|
|
input_elements.prop('disabled', true);
|
|
cockpit.spawn(["/usr/sbin/usermod",
|
|
this.account.name,
|
|
desired_lock_state ? "--lock" : "--unlock"], { superuser: "require", err: "message" })
|
|
.done(function() {
|
|
self.get_locked(false)
|
|
.done(function(locked) {
|
|
/* if we care about what the lock state should be and it doesn't match, try to change again
|
|
this is a workaround for different ways of handling a locked account
|
|
https://github.com/cockpit-project/cockpit/issues/1216
|
|
https://bugzilla.redhat.com/show_bug.cgi?id=853153
|
|
This seems to be fixed in fedora 23 (usermod catches the different locking behavior)
|
|
*/
|
|
if (verify_status && desired_lock_state !== locked) {
|
|
console.log("Account locked state doesn't match desired value, trying again.");
|
|
// only retry once to avoid uncontrolled recursion
|
|
self.change_locked(false, desired_lock_state);
|
|
} else {
|
|
self.update();
|
|
}
|
|
});
|
|
})
|
|
.fail(show_unexpected_error)
|
|
.finally(function() {
|
|
input_elements.prop('disabled', false);
|
|
});
|
|
},
|
|
|
|
set_password: function() {
|
|
if (!this.check_role_for_self_mod())
|
|
return;
|
|
|
|
PageAccountSetPassword.user_name = this.account.name;
|
|
/* TODO: get rid of this once monitoring /etc/shadow will be implemented */
|
|
PageAccountSetPassword.update_callback = $.proxy(this, "enter");
|
|
$('#account-set-password-dialog').modal('show');
|
|
},
|
|
|
|
delete_account: function() {
|
|
PageAccountConfirmDelete.user_name = this.account.name;
|
|
$('#account-confirm-delete-dialog').modal('show');
|
|
},
|
|
|
|
logout_account: function() {
|
|
var input_elements =
|
|
$('#account button:not([disabled]), #account input:not([disabled]), #account a:not([disabled])');
|
|
input_elements
|
|
.prop('disabled', true);
|
|
cockpit.spawn(["/usr/bin/loginctl", "terminate-user", this.account.name],
|
|
{ superuser: "try", err: "message" })
|
|
.done($.proxy(this, "get_logged"))
|
|
.fail(show_unexpected_error)
|
|
.finally(function() {
|
|
input_elements.prop('disabled', false);
|
|
});
|
|
},
|
|
};
|
|
|
|
function PageAccount(user) {
|
|
this._init(user);
|
|
}
|
|
|
|
PageAccountConfirmDelete.prototype = {
|
|
_init: function() {
|
|
this.id = "account-confirm-delete-dialog";
|
|
},
|
|
|
|
show: function() {
|
|
},
|
|
|
|
setup: function() {
|
|
$('#account-confirm-delete-apply').on('click', $.proxy(this, "apply"));
|
|
},
|
|
|
|
enter: function() {
|
|
$('#account-confirm-delete-files').prop('checked', false);
|
|
$('#account-confirm-delete-title').text(cockpit.format(_("Delete $0"), PageAccountConfirmDelete.user_name));
|
|
},
|
|
|
|
leave: function() {
|
|
},
|
|
|
|
apply: function() {
|
|
var prog = ["/usr/sbin/userdel"];
|
|
|
|
if ($('#account-confirm-delete-files').prop('checked'))
|
|
prog.push("-r");
|
|
|
|
prog.push(PageAccountConfirmDelete.user_name);
|
|
|
|
cockpit.spawn(prog, { superuser: "require", err: "message" })
|
|
.done(function () {
|
|
$('#account-confirm-delete-dialog').modal('hide');
|
|
cockpit.location.go("/");
|
|
})
|
|
.fail(show_unexpected_error);
|
|
}
|
|
};
|
|
|
|
function PageAccountConfirmDelete() {
|
|
this._init();
|
|
}
|
|
|
|
function AccountExpiration() {
|
|
$("#account-expiration").on("show.bs.modal", function(ev) {
|
|
var account_id = $(ev.relatedTarget).data("account-id");
|
|
$("#account-expiration").data("account-id", account_id);
|
|
|
|
/* Fill in initial dialog values */
|
|
var expire_date = $(ev.relatedTarget).data("expire-date");
|
|
$(expire_date ? '#account-expiration-expires' : "#account-expiration-never").prop('checked', true);
|
|
$('#account-expiration-input').val(expire_date ? expire_date.toISOString().substr(0, 10) : "");
|
|
$('#account-expiration-input').prop('disabled', !expire_date);
|
|
|
|
/* TRANSLATORS: This is split up and therefore cannot use ngettext plurals */
|
|
var parts = _("Lock account on $0").split("$0");
|
|
$("#account-expiration-before").text(parts[0]);
|
|
$("#account-expiration-after").text(parts[1]);
|
|
});
|
|
|
|
$('#account-expiration .pf-m-primary').on('click', function() {
|
|
var date, value;
|
|
var ex;
|
|
var promise = null;
|
|
|
|
/* Parse the dialog data and validate */
|
|
if ($('#account-expiration-expires').prop('checked')) {
|
|
value = $('#account-expiration-input').val();
|
|
if (!value) {
|
|
ex = new Error(_("Please specify an expiration date"));
|
|
ex.target = "#account-expiration-input";
|
|
} else {
|
|
date = new Date(value + "T12:00:00Z");
|
|
if (isNaN(date.getTime()) || date.getTime() < 0) {
|
|
ex = new Error(_("Invalid expiration date"));
|
|
ex.target = "#account-expiration-input";
|
|
}
|
|
}
|
|
if (ex)
|
|
promise = cockpit.reject(ex);
|
|
}
|
|
|
|
if ($('#account-expiration-never').prop('checked')) {
|
|
date = null;
|
|
promise = null;
|
|
}
|
|
|
|
/* Actually perform the action if valid */
|
|
var prog = ["/usr/sbin/usermod", "-e"];
|
|
var account_id = $("#account-expiration").data("account-id");
|
|
if (!promise) {
|
|
if (date)
|
|
prog.push(date.toISOString().substr(0, 10));
|
|
else
|
|
prog.push("");
|
|
prog.push(account_id);
|
|
promise = cockpit.spawn(prog, { superuser : true, err: "message" });
|
|
}
|
|
|
|
$("#account-expiration").dialog("promise", promise);
|
|
});
|
|
|
|
$('#account-expiration input').on('change', function() {
|
|
$('#account-expiration-input').prop('disabled', $("#account-expiration-never").prop('checked'));
|
|
});
|
|
|
|
$('#account-expiration-input').datepicker({
|
|
autoclose: true,
|
|
todayHighlight: true,
|
|
format: 'yyyy-mm-dd'
|
|
});
|
|
|
|
$('#account-expiration-input').on("show.bs.modal", function(ev) {
|
|
ev.stopPropagation();
|
|
});
|
|
}
|
|
|
|
function PasswordExpiration() {
|
|
var never = 99999;
|
|
|
|
$("#password-expiration").on("show.bs.modal", function(ev) {
|
|
var account_id = $(ev.relatedTarget).data("account-id");
|
|
$("#password-expiration").data("account-id", account_id);
|
|
|
|
/* Fill in initial dialog values */
|
|
var expire_days = parseInt($(ev.relatedTarget).data("expire-days"), 10);
|
|
if (isNaN(expire_days) || expire_days < 0 || expire_days >= never)
|
|
expire_days = null;
|
|
$(expire_days ? '#password-expiration-expires' : "#password-expiration-never").prop('checked', true);
|
|
$('#password-expiration-input').val(expire_days || "");
|
|
$('#password-expiration-input').prop('disabled', !expire_days);
|
|
|
|
/* TRANSLATORS: This is split up and therefore cannot use ngettext plurals */
|
|
var parts = _("Require password change every $0 days").split("$0");
|
|
$("#password-expiration-before").text(parts[0]);
|
|
$("#password-expiration-after").text(parts[1]);
|
|
});
|
|
|
|
$('#password-expiration .pf-m-primary').on('click', function() {
|
|
var days, ex;
|
|
var promise = null;
|
|
|
|
if ($('#password-expiration-expires').prop('checked'))
|
|
days = parseInt($('#password-expiration-input')
|
|
.val()
|
|
.trim(), 10);
|
|
if ($('#password-expiration-never').prop('checked'))
|
|
days = never;
|
|
|
|
if (isNaN(days) || days < 0) {
|
|
ex = new Error(_("Invalid number of days"));
|
|
ex.target = "#password-expiration-input";
|
|
promise = cockpit.reject(ex);
|
|
}
|
|
|
|
var account_id = $("#password-expiration").data("account-id");
|
|
|
|
if (!promise) {
|
|
promise = cockpit.spawn(["/usr/bin/passwd", "-x", String(days), account_id],
|
|
{ superuser: true, err: "message" });
|
|
}
|
|
|
|
$("#password-expiration").dialog("promise", promise);
|
|
});
|
|
|
|
$('#password-expiration input').on('change', function() {
|
|
$('#password-expiration-input').prop('disabled', $("#password-expiration-never").prop('checked'));
|
|
});
|
|
}
|
|
|
|
function PasswordReset() {
|
|
$("#password-reset").on("show.bs.modal", function(ev) {
|
|
var account_id = $(ev.relatedTarget).data("account-id");
|
|
$("#password-reset").data("account-id", account_id);
|
|
|
|
var msg = cockpit.format(_("The account '$0' will be forced to change their password on next login"),
|
|
account_id);
|
|
$("#password-reset .modal-body p").text(msg);
|
|
});
|
|
|
|
$("#password-reset .pf-m-danger").on("click", function() {
|
|
var account_id = $("#password-reset").data("account-id");
|
|
var promise = cockpit.spawn(["/usr/bin/passwd", "-e", account_id],
|
|
{ superuser : true, err: "message" });
|
|
$("#password-reset").dialog("promise", promise);
|
|
});
|
|
}
|
|
|
|
PageAccountSetPassword.prototype = {
|
|
_init: function(user) {
|
|
this.id = "account-set-password-dialog";
|
|
this.user = user;
|
|
},
|
|
|
|
show: function() {
|
|
if (this.user.id === 0 || this.user.name !== PageAccountSetPassword.user_name) {
|
|
$('#account-set-password-old')
|
|
.toggle(false);
|
|
$('#account-set-password-old').prev()
|
|
.toggle(false);
|
|
$('#account-set-password-pw1')
|
|
.focus();
|
|
} else {
|
|
$('#account-set-password-old')
|
|
.toggle(true);
|
|
$('#account-set-password-old').prev()
|
|
.toggle(true);
|
|
$('#account-set-password-old')
|
|
.focus();
|
|
}
|
|
},
|
|
|
|
setup: function() {
|
|
$('#account-set-password-apply').on('click', $.proxy(this, "apply"));
|
|
$('#account-set-password-dialog .check-passwords').on('keydown change', $.proxy(this, "validate"));
|
|
},
|
|
|
|
enter: function() {
|
|
$('#account-set-password-old').val("");
|
|
$('#account-set-password-pw1').val("");
|
|
$('#account-set-password-pw2').val("");
|
|
$('#account-set-password-meter').removeClass("weak okay good excellent");
|
|
$("#account-set-password-dialog").dialog("failure", null);
|
|
},
|
|
|
|
leave: function() {
|
|
},
|
|
|
|
validate: function() {
|
|
var ex;
|
|
var pw = $('#account-set-password-pw1').val();
|
|
if ($('#account-set-password-pw2').val() != pw) {
|
|
ex = new Error(_("The passwords do not match"));
|
|
ex.target = "#account-set-password-pw2";
|
|
}
|
|
|
|
var dfd = cockpit.defer();
|
|
if (ex)
|
|
dfd.reject(ex);
|
|
else
|
|
dfd.resolve();
|
|
|
|
var promise = password_quality(pw)
|
|
.fail(function(ex) {
|
|
ex.target = "#account-set-password-pw2";
|
|
})
|
|
.always(function(arg) {
|
|
var strength = (this.state() == "resolved") ? arg : "weak";
|
|
var meter = $("#account-set-password-meter")
|
|
.removeClass("weak okay good excellent");
|
|
if (pw)
|
|
meter.addClass(strength);
|
|
var message = $("#account-set-password-meter-message");
|
|
if (strength == "excellent") {
|
|
message.text(_("Excellent password"));
|
|
} else {
|
|
message.text("");
|
|
}
|
|
});
|
|
|
|
// Can't use Promise.all() here, because this promise is passed to
|
|
// dialog(), which expects a promise with a progress() method (see
|
|
// pkg/lib/patterns.js)
|
|
// eslint-disable-next-line cockpit/no-cockpit-all
|
|
return cockpit.all(dfd.promise(), promise);
|
|
},
|
|
|
|
apply: function() {
|
|
var self = this;
|
|
|
|
var promise = this.validate()
|
|
.done(function() {
|
|
var user = PageAccountSetPassword.user_name;
|
|
var password = $('#account-set-password-pw1').val();
|
|
|
|
if (self.user.name === user)
|
|
promise = passwd_self($('#account-set-password-old').val(), password);
|
|
else
|
|
promise = passwd_change(user, password);
|
|
|
|
$("#account-set-password-dialog").dialog("promise", promise);
|
|
})
|
|
.fail(function(ex) {
|
|
$("#account-set-password-meter-message").prop("hidden", true);
|
|
$("#account-set-password-dialog").dialog("failure", ex);
|
|
});
|
|
$("#account-set-password-dialog").dialog("wait", promise);
|
|
}
|
|
};
|
|
|
|
function PageAccountSetPassword(user) {
|
|
this._init(user);
|
|
}
|
|
|
|
/* INITIALIZATION AND NAVIGATION
|
|
*
|
|
* The code above still uses the legacy 'Page' abstraction for both
|
|
* pages and dialogs, and expects page.setup, page.enter, page.show,
|
|
* and page.leave to be called at the right times.
|
|
*
|
|
* We cater to this with a little compatability shim consisting of
|
|
* 'dialog_setup', 'page_show', and 'page_hide'.
|
|
*/
|
|
|
|
function show_error_dialog(title, message) {
|
|
if (message) {
|
|
$("#error-popup-title").text(title);
|
|
$("#error-popup-message").text(message);
|
|
} else {
|
|
$("#error-popup-title").text(_("Error"));
|
|
$("#error-popup-message").text(title);
|
|
}
|
|
|
|
$('.modal[role="dialog"]').modal('hide');
|
|
$('#error-popup').modal('show');
|
|
}
|
|
|
|
function show_unexpected_error(error) {
|
|
show_error_dialog(_("Unexpected error"), error.message || error);
|
|
}
|
|
|
|
function dialog_setup(d) {
|
|
d.setup();
|
|
$('#' + d.id)
|
|
.on('show.bs.modal', function () { d.enter() })
|
|
.on('shown.bs.modal', function () { d.show() })
|
|
.on('hidden.bs.modal', function () { d.leave() });
|
|
}
|
|
|
|
function page_show(p, arg) {
|
|
if (p._entered_)
|
|
p.leave();
|
|
p.enter(arg);
|
|
p._entered_ = true;
|
|
$('#' + p.id).prop("hidden", false);
|
|
p.show();
|
|
}
|
|
|
|
function page_hide(p) {
|
|
$('#' + p.id).prop("hidden", true);
|
|
if (p._entered_) {
|
|
p.leave();
|
|
p._entered_ = false;
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
var overview_page;
|
|
var account_page;
|
|
|
|
cockpit.user().done(function (user) {
|
|
function navigate() {
|
|
var path = cockpit.location.path;
|
|
|
|
if (path.length === 0) {
|
|
page_hide(account_page);
|
|
page_show(overview_page);
|
|
} else if (path.length === 1) {
|
|
page_hide(overview_page);
|
|
page_show(account_page, path[0]);
|
|
} else { /* redirect */
|
|
console.warn("not a users location: " + path);
|
|
cockpit.location = '';
|
|
}
|
|
|
|
$("body").prop("hidden", false);
|
|
}
|
|
|
|
cockpit.translate();
|
|
|
|
overview_page = new PageAccounts();
|
|
overview_page.setup();
|
|
|
|
account_page = new PageAccount(user);
|
|
account_page.setup();
|
|
|
|
dialog_setup(new PageAccountsCreate());
|
|
dialog_setup(new PageAccountConfirmDelete());
|
|
dialog_setup(new PageAccountSetPassword(user));
|
|
|
|
AccountExpiration();
|
|
PasswordExpiration();
|
|
PasswordReset();
|
|
|
|
$(cockpit).on("locationchanged", navigate);
|
|
navigate();
|
|
});
|
|
}
|
|
|
|
$(init);
|