UNPKG

penguins-eggs

Version:

A remaster system tool, compatible with Almalinux, Alpine, Arch, Debian, Devuan, Fedora, Manjaro, Opensuse, Ubuntu and derivatives

214 lines (213 loc) 8.47 kB
/** * src/classes/sys-users.ts * penguins-eggs v.25.7.x / ecmascript 2020 * "THE SYSUSER MASTER" * Gestione pura Node.js per utenti e gruppi di sistema. * Sostituisce i binari (useradd/usermod/deluser) per garantire operazioni atomiche * e compatibilità SELinux (Fedora/RHEL) scrivendo file puliti. */ import * as bcrypt from 'bcryptjs'; import fs from 'fs'; import path from 'path'; import { exec } from '../lib/utils.js'; export default class SysUsers { distroFamily; group = []; // File "minori" gestiti a righe raw per semplicità gshadowLines = []; // Cache in memoria passwd = []; shadow = []; subgidLines = []; subuidLines = []; targetRoot; constructor(targetRoot, distroFamily) { this.targetRoot = targetRoot; this.distroFamily = distroFamily; } /** * Crea un nuovo utente completo */ addUser(user, cleanPassword) { // Rimuovi se esiste (idempotenza) this.removeUser(user.username); // 1. Passwd this.passwd.push(user); // 2. Shadow (Hash Password) const salt = bcrypt.genSaltSync(10); const hash = bcrypt.hashSync(cleanPassword, salt); this.shadow.push({ expire: '', hash, inactive: '', lastChange: '19700', // Data approssimativa max: '99999', min: '0', username: user.username, warn: '7' }); // 3. Gruppo Primario // Solo se non esiste già un gruppo con quel nome if (!this.group.find((g) => g.groupName === user.username)) { this.group.push({ gid: user.gid, groupName: user.username, members: [], password: 'x' }); } // 4. GShadow (placeholder) this.gshadowLines.push(`${user.username}:!::`); // 5. SubUID/SubGID (Podman rootless) // Calcolo offset standard: 100000 + (UID-1000)*65536 const uidNum = Number.parseInt(user.uid); if (!isNaN(uidNum) && uidNum >= 1000) { const startUid = 100_000 + (uidNum - 1000) * 65_536; const subEntry = `${user.username}:${startUid}:65536`; this.subuidLines.push(subEntry); this.subgidLines.push(subEntry); } } /** * Aggiunge utente a un gruppo supplementare */ addUserToGroup(username, groupName) { const grp = this.group.find((g) => g.groupName === groupName); if (grp && !grp.members.includes(username)) { grp.members.push(username); } // Se il gruppo non esiste, lo ignoriamo silenziosamente o potremmo crearlo } // ========================================================================= // API PUBBLICA // ========================================================================= /** * Carica tutti i file di configurazione in memoria */ load() { this.passwd = this.parsePasswd(this.readFile('etc/passwd')); this.shadow = this.parseShadow(this.readFile('etc/shadow')); this.group = this.parseGroup(this.readFile('etc/group')); this.gshadowLines = this.readFile('etc/gshadow'); this.subuidLines = this.readFile('etc/subuid'); this.subgidLines = this.readFile('etc/subgid'); } /** * Rimuove completamente un utente */ removeUser(username) { this.passwd = this.passwd.filter((u) => u.username !== username); this.shadow = this.shadow.filter((s) => s.username !== username); this.group = this.group.filter((g) => g.groupName !== username); // Rimuovi dai membri di altri gruppi for (const g of this.group) { g.members = g.members.filter((m) => m !== username); } this.gshadowLines = this.gshadowLines.filter((l) => !l.startsWith(`${username}:`)); this.subuidLines = this.subuidLines.filter((l) => !l.startsWith(`${username}:`)); this.subgidLines = this.subgidLines.filter((l) => !l.startsWith(`${username}:`)); } /** * Salva lo stato della memoria su disco e applica SELinux fix */ async save() { // Serializzazione const passwdContent = this.serializePasswd(this.passwd); const shadowContent = this.serializeShadow(this.shadow); const groupContent = this.serializeGroup(this.group); // Scrittura Atomica + Fix SELinux await this.writeFile('etc/passwd', passwdContent, 'passwd_file_t'); await this.writeFile('etc/shadow', shadowContent, 'shadow_t'); await this.writeFile('etc/group', groupContent, 'passwd_file_t'); // File raw if (this.gshadowLines.length > 0) await this.writeFile('etc/gshadow', this.gshadowLines.join('\n'), 'shadow_t'); if (this.subuidLines.length > 0) await this.writeFile('etc/subuid', this.subuidLines.join('\n'), 'passwd_file_t'); if (this.subgidLines.length > 0) await this.writeFile('etc/subgid', this.subgidLines.join('\n'), 'passwd_file_t'); } /** * Cambia password utente */ setPassword(username, password) { const entry = this.shadow.find((s) => s.username === username); if (entry) { const salt = bcrypt.genSaltSync(10); entry.hash = bcrypt.hashSync(password, salt); entry.lastChange = '19700'; } } // ========================================================================= // IMPLEMENTAZIONE FILE (Privata) // ========================================================================= parseGroup(lines) { return lines .map((line) => { const p = line.split(':'); if (p.length < 3) return null; return { gid: p[2], groupName: p[0], members: p[3] && p[3].trim() ? p[3].split(',') : [], password: p[1] }; }) .filter((g) => g !== null); } parsePasswd(lines) { return lines .map((line) => { const p = line.split(':'); if (p.length < 7) return null; return { gecos: p[4], gid: p[3], home: p[5], password: p[1], shell: p[6], uid: p[2], username: p[0] }; }) .filter((u) => u !== null); } // --- PARSERS & SERIALIZERS --- parseShadow(lines) { return lines .map((line) => { const p = line.split(':'); if (p.length < 2) return null; return { expire: p[7] || '', hash: p[1], inactive: p[6] || '', lastChange: p[2] || '', max: p[4] || '', min: p[3] || '', username: p[0], warn: p[5] || '' }; }) .filter((u) => u !== null); } readFile(relativePath) { const fullPath = path.join(this.targetRoot, relativePath); if (fs.existsSync(fullPath)) { return fs .readFileSync(fullPath, 'utf8') .split('\n') .filter((l) => l.trim().length > 0); } return []; } serializeGroup(entries) { return entries.map((g) => `${g.groupName}:${g.password}:${g.gid}:${g.members.join(',')}`).join('\n'); } serializePasswd(entries) { return entries.map((u) => `${u.username}:${u.password}:${u.uid}:${u.gid}:${u.gecos}:${u.home}:${u.shell}`).join('\n'); } serializeShadow(entries) { return entries.map((s) => `${s.username}:${s.hash}:${s.lastChange}:${s.min}:${s.max}:${s.warn}:${s.inactive}:${s.expire}:`).join('\n'); } async writeFile(relativePath, content, contextType) { const fullPath = path.join(this.targetRoot, relativePath); // Crea dir se manca (es. /etc/sudoers.d/ o simili) const dir = path.dirname(fullPath); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); try { // 1. Scrittura fs.writeFileSync(fullPath, content + '\n'); // 2. Fix SELinux (Solo RHEL Family) if (['almalinux', 'centos', 'fedora', 'rhel', 'rocky'].includes(this.distroFamily)) { // await exec, echo false per non sporcare i log await exec(`chcon -t ${contextType} ${fullPath}`, { echo: false }).catch(() => { }); } } catch (error) { console.error(`SysUsers Error writing ${relativePath}:`, error); } } }