317 lines
12 KiB
JavaScript
317 lines
12 KiB
JavaScript
/*
|
|
* This file is part of Cockpit.
|
|
*
|
|
* Copyright (C) 2020 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 cockpit from 'cockpit';
|
|
import React from 'react';
|
|
|
|
import { Checkbox, Form, FormGroup, TextInput, Popover, Flex, FlexItem, Radio } from '@patternfly/react-core';
|
|
import { has_errors } from "./dialog-utils.js";
|
|
import { passwd_change } from "./password-dialogs.js";
|
|
import { password_quality, PasswordFormFields } from "cockpit-components-password.jsx";
|
|
import { show_modal_dialog, apply_modal_dialog } from "cockpit-components-dialog.jsx";
|
|
import { HelpIcon } from '@patternfly/react-icons';
|
|
|
|
const _ = cockpit.gettext;
|
|
|
|
function AccountCreateBody({ state, errors, change }) {
|
|
const {
|
|
real_name, user_name,
|
|
locked, change_passw_force
|
|
} = state;
|
|
|
|
return (
|
|
<Form isHorizontal onSubmit={apply_modal_dialog}>
|
|
<FormGroup label={_("Full name")}
|
|
helperTextInvalid={errors && errors.real_name}
|
|
validated={(errors && errors.real_name) ? "error" : "default"}
|
|
fieldId="accounts-create-real-name">
|
|
<TextInput id="accounts-create-real-name"
|
|
validated={(errors && errors.real_name) ? "error" : "default"}
|
|
value={real_name} onChange={value => change("real_name", value)} />
|
|
</FormGroup>
|
|
|
|
<FormGroup label={_("User name")}
|
|
helperTextInvalid={errors && errors.user_name}
|
|
validated={(errors && errors.user_name) ? "error" : "default"}
|
|
fieldId="accounts-create-user-name">
|
|
<TextInput id="accounts-create-user-name"
|
|
validated={(errors && errors.user_name) ? "error" : "default"}
|
|
value={user_name} onChange={value => change("user_name", value)} />
|
|
</FormGroup>
|
|
|
|
<PasswordFormFields password_label={_("Password")}
|
|
password_confirm_label={_("Confirm")}
|
|
error_password={errors && errors.password}
|
|
error_password_confirm={errors && errors.password_confirm}
|
|
idPrefix="accounts-create-password"
|
|
change={change} />
|
|
|
|
<FormGroup label={_("Authentication")} fieldId="accounts-create-locked" hasNoPaddingTop>
|
|
<Radio id="account-use-password"
|
|
label={_("Use password")}
|
|
isChecked={!locked} onChange={checked => change("locked", !checked)}
|
|
description={
|
|
<Checkbox id="accounts-create-force-password-change"
|
|
className="pf-u-mb-xs"
|
|
label={_("Require password change on first login")}
|
|
isChecked={change_passw_force} onChange={checked => change("change_passw_force", checked)} />
|
|
} />
|
|
|
|
<Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
|
|
<FlexItem spacer={{ default: 'spacerSm' }}>
|
|
<Radio id="accounts-create-locked"
|
|
isChecked={locked} onChange={checked => change("locked", checked)}
|
|
label={_("Disallow password authentication")} />
|
|
</FlexItem>
|
|
|
|
<FlexItem spacer={{ default: 'spacerLg' }}>
|
|
<Popover bodyContent={_("Other authentication methods are still available even when interactive password authentication is not allowed.")}
|
|
showClose={false}>
|
|
<HelpIcon />
|
|
</Popover>
|
|
</FlexItem>
|
|
</Flex>
|
|
</FormGroup>
|
|
</Form>
|
|
);
|
|
}
|
|
|
|
function is_valid_char_username(c) {
|
|
return (c >= 'a' && c <= 'z') ||
|
|
(c >= 'A' && c <= 'Z') ||
|
|
(c >= '0' && c <= '9') ||
|
|
c == '.' || c == '_' || c == '-';
|
|
}
|
|
|
|
function validate_username(username, accounts) {
|
|
if (!username)
|
|
return _("No user name specified");
|
|
|
|
for (let i = 0; i < username.length; i++) {
|
|
if (!is_valid_char_username(username[i]))
|
|
return _("The user name can only consist of letters from a-z, digits, dots, dashes and underscores.");
|
|
}
|
|
|
|
for (let k = 0; k < accounts.length; k++) {
|
|
if (accounts[k].name == username)
|
|
return _("This user name already exists");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function suggest_username(realname) {
|
|
function remove_diacritics(str) {
|
|
const translate_table = {
|
|
a: '[àáâãäå]',
|
|
ae: 'æ',
|
|
c: '[čç]',
|
|
d: 'ď',
|
|
e: '[èéêë]',
|
|
i: '[íìïî]',
|
|
l: '[ĺľ]',
|
|
n: '[ňñ]',
|
|
o: '[òóôõö]',
|
|
oe: 'œ',
|
|
r: '[ŕř]',
|
|
s: 'š',
|
|
t: 'ť',
|
|
u: '[ùúůûűü]',
|
|
y: '[ýÿ]',
|
|
z: 'ž',
|
|
};
|
|
for (const i in translate_table)
|
|
str = str.replace(new RegExp(translate_table[i], 'g'), i);
|
|
|
|
for (let k = 0; k < str.length;) {
|
|
if (!is_valid_char_username(str[k]))
|
|
str = str.substr(0, k) + str.substr(k + 1);
|
|
else
|
|
k++;
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
let result = "";
|
|
const 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);
|
|
}
|
|
|
|
export function account_create_dialog(accounts) {
|
|
let dlg = null;
|
|
const state = {
|
|
real_name: "",
|
|
user_name: "",
|
|
password: "",
|
|
password_confirm: "",
|
|
locked: false,
|
|
confirm_weak: false,
|
|
change_passw_force: false,
|
|
};
|
|
let errors = { };
|
|
|
|
let old_password = null;
|
|
let user_name_dirty = false;
|
|
|
|
function change(field, value) {
|
|
state[field] = value;
|
|
errors = { };
|
|
|
|
if (field == "user_name")
|
|
user_name_dirty = true;
|
|
|
|
if (!user_name_dirty && field == "real_name")
|
|
state.user_name = suggest_username(state.real_name);
|
|
|
|
if (state.password != old_password) {
|
|
state.confirm_weak = false;
|
|
old_password = state.password;
|
|
}
|
|
|
|
if (field == "change_passw_force")
|
|
state.locked = false;
|
|
|
|
if (field == "locked")
|
|
state.change_passw_force = false;
|
|
|
|
update();
|
|
}
|
|
|
|
function validate(force, real_name, user_name, password, password_confirm) {
|
|
const errs = { };
|
|
|
|
if (!real_name)
|
|
errors.real_name = _("No real name specified");
|
|
|
|
if (password != password_confirm)
|
|
errs.password_confirm = _("The passwords do not match");
|
|
|
|
if (password.length > 256)
|
|
errs.password = _("Password is longer than 256 characters");
|
|
|
|
errs.user_name = validate_username(user_name, accounts);
|
|
|
|
return password_quality(password, force)
|
|
.catch(ex => {
|
|
errs.password = (ex.message || ex.toString()).replace("\n", " ");
|
|
})
|
|
.then(() => {
|
|
errors = errs;
|
|
return !has_errors(errs);
|
|
});
|
|
}
|
|
|
|
function create(real_name, user_name, password, locked, force_change) {
|
|
return cockpit.spawn(["/usr/sbin/useradd", "-D"], { superuser: "require" })
|
|
.catch(() => "")
|
|
.then(defaults => {
|
|
let shell = null;
|
|
defaults.split("\n").forEach(item => {
|
|
if (item.indexOf("SHELL=") === 0) {
|
|
shell = item.split("=")[1] || "";
|
|
}
|
|
});
|
|
const prog = ["/usr/sbin/useradd", "--create-home", "-s", shell || "/bin/bash"];
|
|
if (real_name) {
|
|
prog.push('-c');
|
|
prog.push(real_name);
|
|
}
|
|
prog.push(user_name);
|
|
return cockpit.spawn(prog, { superuser: "require", err: "message" })
|
|
.then(() => passwd_change(user_name, password))
|
|
.then(() => {
|
|
if (locked)
|
|
return cockpit.spawn([
|
|
"/usr/sbin/usermod",
|
|
user_name,
|
|
"--lock"
|
|
], { superuser: "require", err: "message" });
|
|
if (force_change)
|
|
return cockpit.spawn([
|
|
"/usr/bin/passwd",
|
|
"-e",
|
|
user_name
|
|
], { superuser: "require", err: "message" });
|
|
});
|
|
});
|
|
}
|
|
|
|
function passwd_check(force_weak, real_name, user_name, password, password_confirm, locked, force_change) {
|
|
return validate(force_weak, real_name, user_name, password, password_confirm).then(valid => {
|
|
if (valid)
|
|
return create(real_name, user_name, password, locked, force_change);
|
|
else {
|
|
if (!errors.real_name && !errors.user_name && !errors.password_confirm && state.password.length <= 256) {
|
|
state.confirm_weak = true;
|
|
}
|
|
update();
|
|
return Promise.reject();
|
|
}
|
|
});
|
|
}
|
|
|
|
function update() {
|
|
const props = {
|
|
id: "accounts-create-dialog",
|
|
title: _("Create new account"),
|
|
body: <AccountCreateBody state={state} errors={errors} change={change} />
|
|
};
|
|
|
|
const footer = {
|
|
actions: [
|
|
{
|
|
caption: _("Create"),
|
|
style: "primary",
|
|
clicked: () => {
|
|
return passwd_check(false, state.real_name, state.user_name, state.password, state.password_confirm, state.locked, state.change_passw_force);
|
|
},
|
|
disabled: state.confirm_weak
|
|
}
|
|
]
|
|
};
|
|
if (state.confirm_weak) {
|
|
footer.actions.push(
|
|
{
|
|
caption: _("Create account with weak password"),
|
|
style: "warning",
|
|
clicked: () => {
|
|
return passwd_check(true, state.real_name, state.user_name, state.password, state.password_confirm, state.locked);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
if (!dlg)
|
|
dlg = show_modal_dialog(props, footer);
|
|
else {
|
|
dlg.setProps(props);
|
|
dlg.setFooterProps(footer);
|
|
}
|
|
}
|
|
|
|
update();
|
|
}
|