🏑 index : quine-global/quine-anaconda-webui.git

/*
 * Copyright (C) 2025 Red Hat, Inc.
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

import cockpit from "cockpit";

import * as python from "python.js";

import {
    clearRootPassword,
    setCryptedRootPassword,
    setIsRootAccountLocked,
    setUsers,
} from "../apis/users.js";

import { objectToDbus } from "../apis/helpers.js";

import encryptUserPw from "../scripts/encrypt-user-pw.py";

/**
 * Whether the installer UI may change root password / lock state.
 *
 * Ported from Anaconda's shared UI helper (same control flow as
 * `can_modify_root_configuration` in pyanaconda):
 * https://github.com/rhinstaller/anaconda/blob/main/pyanaconda/ui/lib/users.py
 *
 * @param {object} opts
 * @param {boolean} opts.automatedInstall  Same role as Anaconda `flags.automatedInstall`
 * @param {boolean} opts.canChangeRoot     Same role as Anaconda `conf.ui.can_change_root`
 * @param {boolean} opts.canChangeRootPassword  Users module `CanChangeRootPassword`
 * @returns {boolean}
 */
export const canModifyRootConfiguration = ({
    automatedInstall,
    canChangeRoot,
    canChangeRootPassword,
}) => {
    // Allow changes in the interactive mode.
    if (!automatedInstall) {
        return true;
    }

    // Does the configuration allow changes?
    if (canChangeRoot) {
        return true;
    }

    // Allow changes if the root account isn't
    // already configured by the kickstart file.
    if (canChangeRootPassword) {
        return true;
    }

    return false;
};

/**
 * Whether the Accounts user-creation section may be edited when the Users module
 * already lists accounts (e.g. kickstart / interactive defaults).
 * Aligns with Anaconda’s use of `conf.ui.can_change_users` for the user spoke.
 *
 * @param {object} opts
 * @param {boolean} opts.canChangeUsers Same role as Anaconda `conf.ui.can_change_users`
 * @param {boolean} opts.usersSpecifiedByKickstart Installer already has user entries to show
 * @returns {boolean}
 */
export const canModifyUserConfiguration = ({ canChangeUsers, usersSpecifiedByKickstart }) => {
    return !usersSpecifiedByKickstart || canChangeUsers;
};

const cryptUserPassword = async (password) => {
    const crypted = await python.spawn(encryptUserPw, password, { environ: ["LC_ALL=C.UTF-8"], err: "message" });
    return crypted;
};

export const applyAccounts = async (accounts) => {
    if ((accounts.users?.length ?? 0) === 0) {
        await setUsers([]);
    } else if (accounts.canModifyUserConfiguration) {
        const password = typeof accounts.password === "string" ? accounts.password : "";
        const hasUserPassword = password.length > 0;
        const userPassword = hasUserPassword
            ? await cryptUserPassword(password)
            : "";
        const first = accounts.users?.[0] ?? {};
        const firstUserDbus = firstUserToDbus({
            ...first,
            isCrypted: hasUserPassword,
            password: userPassword,
        });
        const existing = accounts.users ?? [];
        const users = existing.length > 0
            ? [firstUserDbus, ...existing.slice(1).map(u => objectToDbus(u))]
            : [firstUserDbus];
        await setUsers(users);
    }

    if (accounts.canModifyRootConfiguration) {
        await setIsRootAccountLocked(!accounts.isRootEnabled);
        if (accounts.isRootEnabled) {
            const cryptedRootPw = await cryptUserPassword(accounts.rootPassword);
            await setCryptedRootPassword({ password: cryptedRootPw });
        } else {
            await clearRootPassword();
        }
    }
};

const firstUserToDbus = (firstUser) => {
    return {
        gecos: cockpit.variant("s", firstUser.gecos ?? ""),
        groups: cockpit.variant("as", firstUser.groups ?? ["wheel"]),
        "is-crypted": cockpit.variant("b", firstUser.isCrypted ?? true),
        name: cockpit.variant("s", firstUser.name ?? ""),
        password: cockpit.variant("s", firstUser.password ?? ""),
    };
};