
com.aoindustries.aoserv.daemon.posix.linux.LinuxAccountManager Maven / Gradle / Ivy
/*
* aoserv-daemon - Server management daemon for the AOServ Platform.
* Copyright (C) 2001-2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022 AO Industries, Inc.
* [email protected]
* 7262 Bull Pen Cir
* Mobile, AL 36695
*
* This file is part of aoserv-daemon.
*
* aoserv-daemon 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 3 of the License, or
* (at your option) any later version.
*
* aoserv-daemon 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 aoserv-daemon. If not, see .
*/
package com.aoindustries.aoserv.daemon.posix.linux;
import com.aoapps.collections.AoCollections;
import com.aoapps.hodgepodge.io.stream.StreamableInput;
import com.aoapps.hodgepodge.io.stream.StreamableOutput;
import com.aoapps.hodgepodge.util.Tuple2;
import com.aoapps.io.posix.PosixFile;
import com.aoapps.io.posix.Stat;
import com.aoapps.lang.SysExits;
import com.aoapps.lang.util.BufferManager;
import com.aoapps.lang.util.ErrorPrinter;
import com.aoapps.lang.validation.ValidationException;
import com.aoapps.tempfiles.TempFile;
import com.aoapps.tempfiles.TempFileContext;
import com.aoindustries.aoserv.client.AoservConnector;
import com.aoindustries.aoserv.client.distribution.OperatingSystemVersion;
import com.aoindustries.aoserv.client.linux.Group;
import com.aoindustries.aoserv.client.linux.GroupServer;
import com.aoindustries.aoserv.client.linux.LinuxId;
import com.aoindustries.aoserv.client.linux.PosixPath;
import com.aoindustries.aoserv.client.linux.Server;
import com.aoindustries.aoserv.client.linux.Shell;
import com.aoindustries.aoserv.client.linux.User;
import com.aoindustries.aoserv.client.linux.UserServer;
import com.aoindustries.aoserv.client.linux.UserType;
import com.aoindustries.aoserv.daemon.AoservDaemon;
import com.aoindustries.aoserv.daemon.AoservDaemonConfiguration;
import com.aoindustries.aoserv.daemon.backup.BackupManager;
import com.aoindustries.aoserv.daemon.client.AoservDaemonProtocol;
import com.aoindustries.aoserv.daemon.httpd.HttpdOperatingSystemConfiguration;
import com.aoindustries.aoserv.daemon.posix.GroupFile;
import com.aoindustries.aoserv.daemon.posix.GshadowFile;
import com.aoindustries.aoserv.daemon.posix.PasswdFile;
import com.aoindustries.aoserv.daemon.posix.ShadowFile;
import com.aoindustries.aoserv.daemon.util.BuilderThread;
import com.aoindustries.aoserv.daemon.util.DaemonFileUtils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
import java.io.Writer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* TODO: Watch for changes in /etc/passwd and /etc/group and auto-run rebuild.
* TODO: This will more promptly add new system users and groups to the master.
*
* @author AO Industries, Inc.
*/
public final class LinuxAccountManager extends BuilderThread {
private static final Logger logger = Logger.getLogger(LinuxAccountManager.class.getName());
/**
* Lockfile for password file updates.
*
* TODO: Is this sufficient locking, or should we also lock each individual file while updating? (/etc/passwd.lock, ...)
*
*/
private static final File PWD_LOCK = new File("/etc/.pwd.lock");
private static final PosixFile SUDOERS_D = new PosixFile("/etc/sudoers.d");
private static final String BASHRC = ".bashrc";
private static final File cronDirectory = new File("/var/spool/cron");
public static boolean comparePassword(User.Name username, String password) throws IOException, SQLException {
String crypted = ShadowFile.getEncryptedPassword(username).getElement1();
if (crypted.equals(User.NO_PASSWORD_CONFIG_VALUE)) {
return false;
}
int len = crypted.length();
if (len < 2) {
return false;
}
String salt;
if (crypted.charAt(0) == '$') {
// Select salt up to but not including the last '$'
int lastPos = crypted.lastIndexOf('$');
salt = crypted.substring(0, lastPos);
if (salt.length() < 3) {
return false;
}
} else {
// Assume old-school DES
salt = crypted.substring(0, 2);
}
String newCrypted = PosixFile.crypt(password, salt);
return crypted.equals(newCrypted);
}
private static final Object rebuildLock = new Object();
@Override
@SuppressWarnings({"UseSpecificCatch", "TooBroadCatch"})
protected boolean doRebuild() {
try {
rebuildLinuxAccountSettings();
return true;
} catch (ThreadDeath td) {
throw td;
} catch (Throwable t) {
logger.log(Level.SEVERE, null, t);
return false;
}
}
@SuppressWarnings("try")
private static void rebuildLinuxAccountSettings() throws IOException, SQLException {
Server thisServer = AoservDaemon.getThisServer();
HttpdOperatingSystemConfiguration osConfig = HttpdOperatingSystemConfiguration.getHttpOperatingSystemConfiguration();
OperatingSystemVersion osv = thisServer.getHost().getOperatingSystemVersion();
int osvId = osv.getPkey();
if (
osvId != OperatingSystemVersion.CENTOS_5_I686_AND_X86_64
&& osvId != OperatingSystemVersion.CENTOS_7_X86_64
) {
throw new AssertionError("Unsupported OperatingSystemVersion: " + osv);
}
int uidMin = thisServer.getUidMin().getId();
int uidMax = thisServer.getUidMax().getId();
int gidMin = thisServer.getGidMin().getId();
int gidMax = thisServer.getGidMax().getId();
if (logger.isLoggable(Level.FINER)) {
logger.finer("uidMin=" + uidMin + ", uidMax=" + uidMax + ", gidMin=" + gidMin + ", gidMax=" + gidMax);
}
synchronized (rebuildLock) {
// Get the lists from the database
List lsas = thisServer.getLinuxServerAccounts();
boolean hasFtpShell = false;
boolean hasPasswdShell = false;
final Set usernames;
final Set usernameStrs;
final Set uids;
final Set homeDirs;
final Map passwdEntries;
List lsgs = thisServer.getLinuxServerGroups();
final Map> groups;
final Map groupEntries;
Set restorecon = new LinkedHashSet<>();
try {
try (
FileChannel fileChannel = FileChannel.open(PWD_LOCK.toPath(), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
FileLock fileLock = fileChannel.lock();
) {
// Add any system groups found, updating lsgs
{
Map groupFile;
synchronized (GroupFile.groupLock) {
groupFile = GroupFile.readGroupFile();
}
boolean modified = false;
for (GroupFile.Entry entry : groupFile.values()) {
Group.Name groupName = entry.getGroupName();
if (
entry.getGid() < gidMin
|| entry.getGid() > gidMax
|| groupName.equals(Group.AOADMIN)
// AOServ Schema:
|| groupName.equals(Group.ACCOUNTING)
|| groupName.equals(Group.BILLING)
|| groupName.equals(Group.DISTRIBUTION)
|| groupName.equals(Group.INFRASTRUCTURE)
|| groupName.equals(Group.MANAGEMENT)
|| groupName.equals(Group.MONITORING)
|| groupName.equals(Group.RESELLER)
// Amazon EC2 cloud-init
|| groupName.equals(Group.CENTOS)
) {
boolean found = false;
for (GroupServer lsg : lsgs) {
if (lsg.getLinuxGroup().getName().equals(groupName)) {
found = true;
break;
}
}
if (!found) {
int gid = entry.getGid();
if (logger.isLoggable(Level.FINE)) {
logger.fine("Adding system group: " + groupName + " #" + gid);
}
thisServer.addSystemGroup(groupName, gid);
modified = true;
}
}
}
if (modified) {
lsgs = thisServer.getLinuxServerGroups();
}
}
// Add any system users found, updating lsas
{
Map passwdFile;
synchronized (PasswdFile.passwdLock) {
passwdFile = PasswdFile.readPasswdFile();
}
boolean modified = false;
for (PasswdFile.Entry entry : passwdFile.values()) {
User.Name username = entry.getUsername();
if (
entry.getUid() < uidMin
|| entry.getUid() > uidMax
|| username.equals(User.AOADMIN)
// AOServ Schema:
|| username.equals(User.ACCOUNTING)
|| username.equals(User.BILLING)
|| username.equals(User.DISTRIBUTION)
|| username.equals(User.INFRASTRUCTURE)
|| username.equals(User.MANAGEMENT)
|| username.equals(User.MONITORING)
|| username.equals(User.RESELLER)
// Amazon EC2 cloud-init
|| username.equals(User.CENTOS)
) {
boolean found = false;
for (UserServer lsa : lsas) {
if (lsa.getLinuxAccount().getUsername().getUsername().equals(username)) {
found = true;
break;
}
}
if (!found) {
int uid = entry.getUid();
if (logger.isLoggable(Level.FINE)) {
logger.fine("Adding system user: " + username + " #" + uid);
}
thisServer.addSystemUser(
username,
uid,
entry.getGid(),
entry.getFullName(),
entry.getOfficeLocation(),
entry.getOfficePhone(),
entry.getHomePhone(),
entry.getHome(),
entry.getShell()
);
modified = true;
}
}
}
if (modified) {
lsas = thisServer.getLinuxServerAccounts();
}
}
// Install /usr/bin/ftppasswd and /usr/bin/ftponly if required by any UserServer
for (UserServer lsa : lsas) {
PosixPath shellPath = lsa.getLinuxAccount().getShell().getPath();
if (
shellPath.equals(Shell.FTPONLY)
|| shellPath.equals(Shell.FTPPASSWD)
) {
hasFtpShell = true;
break;
}
}
if (hasFtpShell) {
PackageManager.installPackage(PackageManager.PackageName.AOSERV_FTP_SHELLS);
}
// Add /usr/bin/passwd to /etc/shells if required by any UserServer
for (UserServer lsa : lsas) {
if (lsa.getLinuxAccount().getShell().getPath().equals(Shell.PASSWD)) {
hasPasswdShell = true;
break;
}
}
if (hasPasswdShell) {
PackageManager.installPackage(PackageManager.PackageName.AOSERV_PASSWD_SHELL);
}
// Build passwd data
{
int size = lsas.size();
usernames = AoCollections.newLinkedHashSet(size);
usernameStrs = AoCollections.newLinkedHashSet(size);
uids = AoCollections.newLinkedHashSet(size);
homeDirs = AoCollections.newLinkedHashSet(size);
passwdEntries = AoCollections.newLinkedHashMap(size);
boolean hasRoot = false;
for (UserServer lsa : lsas) {
User la = lsa.getLinuxAccount();
User.Name username = la.getUsername_id();
if (!usernames.add(username)) {
throw new SQLException("Duplicate username: " + username);
}
if (!usernameStrs.add(username.toString())) {
throw new AssertionError();
}
uids.add(lsa.getUid().getId());
PosixPath home = lsa.getHome();
homeDirs.add(home.toString());
GroupServer primaryGroup = lsa.getPrimaryLinuxServerGroup();
if (primaryGroup == null) {
throw new SQLException("Unable to find primary GroupServer for username=" + username + " on " + lsa.getServer());
}
PosixPath shell = la.getShell().getPath();
// CentOS 5 requires /bin/bash, but CentOS 7 ships with /sbin/nologin.
// Unfortunately, in our current schema the shell is set of all servers at once.
// This ugly hack allows us to store the new version, and it will be converted
// for compatibility with CentOS 5 on-the-fly.
if (
osvId == OperatingSystemVersion.CENTOS_5_I686_AND_X86_64
&& username.equals(User.CYRUS)
&& shell.equals(Shell.NOLOGIN)
) {
if (logger.isLoggable(Level.INFO)) {
logger.info("Converting " + shell + " to " + Shell.BASH + " for " + username);
shell = Shell.BASH;
}
//} else if (
// username.equals(User.JENKINS)
// && home.toString().equals("/home/jenkins")
//) {
// // TODO: Remove this once JCA's Jenkins moved to newer version on separate virtual server:
// if (logger.isLoggable(Level.INFO)) {
// logger.info("Converting " + shell + " to " + Shell.BASH + " for " + username + " to be compatible with previous Jenkins installations in " + home);
// shell = Shell.BASH;
// }
}
if (
passwdEntries.put(
username,
new PasswdFile.Entry(
username,
lsa.getUid().getId(),
primaryGroup.getGid().getId(),
la.getName(),
la.getOfficeLocation(),
la.getOfficePhone(),
la.getHomePhone(),
home,
shell
)
) != null
) {
throw new SQLException("Duplicate username: " + username);
}
if (username.equals(User.ROOT)) {
hasRoot = true;
}
}
if (!hasRoot) {
throw new SQLException(User.ROOT + " user not found");
}
}
// Build group data
{
int size = lsgs.size();
groups = AoCollections.newLinkedHashMap(size);
groupEntries = AoCollections.newLinkedHashMap(size);
boolean hasRoot = false;
for (GroupServer lsg : lsgs) {
Group.Name groupName = lsg.getLinuxGroup().getName();
Set groupMembers = new LinkedHashSet<>();
{
for (UserServer altAccount : lsg.getAlternateLinuxServerAccounts()) {
User.Name userId = altAccount.getLinuxAccount_username_id();
if (!groupMembers.add(userId)) {
throw new SQLException("Duplicate group member: " + userId);
}
}
}
if (groups.put(groupName, groupMembers) != null) {
throw new SQLException("Duplicate group name: " + groupName);
}
if (
groupEntries.put(
groupName,
new GroupFile.Entry(
groupName,
lsg.getGid().getId(),
groupMembers
)
) != null
) {
throw new SQLException("Duplicate group name: " + groupName);
}
if (groupName.equals(Group.ROOT)) {
hasRoot = true;
}
}
if (!hasRoot) {
throw new SQLException(Group.ROOT + " group not found");
}
}
synchronized (PasswdFile.passwdLock) {
synchronized (ShadowFile.shadowLock) {
synchronized (GroupFile.groupLock) {
synchronized (GshadowFile.gshadowLock) {
// Build new file contents
final byte[] newPasswdContent = PasswdFile.buildPasswdFile(passwdEntries, uidMin, uidMax);
final byte[] newShadowContent = ShadowFile.buildShadowFile(usernames);
final byte[] newGroupContent = GroupFile.buildGroupFile(groupEntries, gidMin, gidMax);
final byte[] newGshadowContent = GshadowFile.buildGshadowFile(groups);
// Write any updates
PasswdFile.writePasswdFile(newPasswdContent, restorecon);
ShadowFile.writeShadowFile(newShadowContent, restorecon);
GroupFile.writeGroupFile(newGroupContent, restorecon);
GshadowFile.writeGshadowFile(newGshadowContent, restorecon);
}
}
}
}
// restorecon any changed before releasing lock
DaemonFileUtils.restorecon(restorecon);
restorecon.clear();
}
// A list of all files to delete is created so that all the data can
// be backed-up before removal.
List deleteFileList = new ArrayList<>();
// Create any home directories that do not exist.
for (UserServer lsa : lsas) {
User la = lsa.getLinuxAccount();
String type = la.getType().getName();
User.Name username = la.getUsername_id();
GroupServer primaryLsg = lsa.getPrimaryLinuxServerGroup();
int uid = lsa.getUid().getId();
int gid = primaryLsg.getGid().getId();
boolean copySkel = false;
final PosixPath homePath = lsa.getHome();
final PosixFile homeDir = new PosixFile(homePath.toString());
if (!homeDir.getStat().exists()) {
// Make the parent of the home directory if it does not exist
PosixFile parent = homeDir.getParent();
if (!parent.getStat().exists()) {
if (logger.isLoggable(Level.INFO)) {
logger.info("mkdir \"" + parent + '"');
}
parent.mkdir(true, 0755);
}
// Look for home directory being moved
PosixFile oldHome;
{
PosixPath defaultHome = UserServer.getDefaultHomeDirectory(username);
PosixPath hashedHome = UserServer.getHashedHomeDirectory(username);
if (homePath.equals(defaultHome)) {
oldHome = new PosixFile(hashedHome.toString());
} else if (homePath.equals(hashedHome)) {
oldHome = new PosixFile(defaultHome.toString());
} else {
oldHome = null;
}
}
if (
oldHome != null
// Don't move a home directory still being used
&& !homeDirs.contains(oldHome.getPath())
// Only move when exists
&& oldHome.getStat().exists()
) {
// Move the home directory from old location
if (logger.isLoggable(Level.INFO)) {
logger.info("mv \"" + oldHome + "\" \"" + homeDir + '"');
}
oldHome.renameTo(homeDir);
// This moved directory requires restorecon
restorecon.add(homeDir);
} else {
// Make the home directory
if (logger.isLoggable(Level.INFO)) {
logger.info("mkdir \"" + homeDir + '"');
}
homeDir.mkdir(false, 0700);
copySkel = true;
}
}
// Set up the directory if it was just created or was created as root before
final String homeStr = homeDir.getPath();
// Homes in /www will have all the skel copied, but will not set the directory perms
boolean isWwwAndUser =
homeStr.startsWith(osConfig.getHttpdSitesDirectory().toString() + '/')
&& (type.equals(UserType.USER) || type.equals(UserType.APPLICATION))
&& la.getFtpGuestUser() == null;
// Only build directories for accounts that are in /home/ or user account in /www/
if (
isWwwAndUser
|| homeStr.startsWith("/home/")
) {
boolean chownHome;
{
Stat homeDirStat = homeDir.getStat();
chownHome = !isWwwAndUser
&& (
homeDirStat.getUid() == PosixFile.ROOT_UID
|| homeDirStat.getGid() == PosixFile.ROOT_GID
)
// Do not set permissions for encrypted home directories
&& !(new PosixFile(homeStr + ".aes256.img").getStat().exists());
if (chownHome) {
copySkel = true;
}
}
// Copy the /etc/skel directory
if (
copySkel
// Only copy the files for user accounts
&& (type.equals(UserType.USER) || type.equals(UserType.APPLICATION))
) {
File skel = new File("/etc/skel");
String[] skelList = skel.list();
if (skelList != null) {
for (String filename : skelList) {
PosixFile homeFile = new PosixFile(homeDir, filename, true);
if (!homeFile.getStat().exists()) {
PosixFile skelFile = new PosixFile(skel, filename);
if (logger.isLoggable(Level.INFO)) {
logger.info("cp \"" + skelFile + "\" \"" + homeFile + '"');
}
skelFile.copyTo(homeFile, false);
if (logger.isLoggable(Level.INFO)) {
logger.info("chown " + uid + ':' + gid + " \"" + homeFile + '"');
}
homeFile.chown(uid, gid);
}
}
}
}
// Set final directory ownership now that home directory completely setup
if (chownHome) {
if (logger.isLoggable(Level.INFO)) {
logger.info("chown " + uid + ':' + gid + " \"" + homeDir + '"');
}
homeDir.chown(uid, gid);
// Now done in mkdir above: homeDir.setMode(0700);
}
}
}
// restorecon any moved home directories
DaemonFileUtils.restorecon(restorecon);
restorecon.clear();
/*
* Remove any home directories that should not exist.
*/
Set keepHashDirs = new HashSet<>();
for (char ch = 'a'; ch <= 'z'; ch++) {
PosixFile hashDir = new PosixFile("/home/" + ch);
if (homeDirs.contains(hashDir.getPath())) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("hashDir is a home directory, not cleaning: " + hashDir);
}
} else if (hashDir.getStat().exists()) {
boolean hasHomeDir = false;
List hashDirToDelete = new ArrayList<>();
String[] homeList = hashDir.list();
if (homeList != null) {
for (String dirName : homeList) {
PosixFile dir = new PosixFile(hashDir, dirName, true);
String dirPath = dir.getPath();
// Allow encrypted form of home directory
if (dirPath.endsWith(".aes256.img")) {
dirPath = dirPath.substring(0, dirPath.length() - ".aes256.img".length());
}
if (homeDirs.contains(dirPath)) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("hashDir has home directory: " + dir);
}
hasHomeDir = true;
} else {
if (logger.isLoggable(Level.FINE)) {
logger.fine("hashDir has an extra directory: " + dir);
}
hashDirToDelete.add(dir.getFile());
}
}
}
if (hasHomeDir) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("hashDir still has home directories: " + hashDir);
}
for (File toDelete : hashDirToDelete) {
if (logger.isLoggable(Level.INFO)) {
logger.info("Scheduling for removal: " + toDelete);
}
deleteFileList.add(toDelete);
}
keepHashDirs.add(hashDir.getPath());
} else {
if (logger.isLoggable(Level.FINE)) {
logger.fine("hashDir does not have any home directories, will be deleted completely: " + hashDir);
}
}
}
}
// Direct children of /home
PosixFile homeDir = new PosixFile("/home");
String[] homeList = homeDir.list();
if (homeList != null) {
for (String dirName : homeList) {
PosixFile dir = new PosixFile(homeDir, dirName, true);
String dirPath = dir.getPath();
if (keepHashDirs.contains(dirPath)) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("Keeping hashDir that is still used: " + dir);
}
} else {
// Allow encrypted form of home directory
if (dirPath.endsWith(".aes256.img")) {
dirPath = dirPath.substring(0, dirPath.length() - ".aes256.img".length());
}
if (homeDirs.contains(dirPath)) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("Is a home directory: " + dir);
}
} else {
File toDelete = dir.getFile();
if (logger.isLoggable(Level.INFO)) {
logger.info("Scheduling for removal: " + toDelete);
}
deleteFileList.add(toDelete);
}
}
}
}
/*
* Remove any cron jobs that should not exist.
*/
String[] cronList = cronDirectory.list();
if (cronList != null) {
for (String filename : cronList) {
// Filename must be the username of one of the users to be kept intact
if (!usernameStrs.contains(filename)) {
File toDelete = new File(cronDirectory, filename);
if (logger.isLoggable(Level.INFO)) {
logger.info("Scheduling for removal: " + toDelete);
}
deleteFileList.add(toDelete);
}
}
}
// Configure sudo
if (osvId == OperatingSystemVersion.CENTOS_7_X86_64) {
Map sudoers = new LinkedHashMap<>();
for (UserServer lsa : lsas) {
String sudo = lsa.getSudo();
if (sudo != null) {
sudoers.put(lsa.getLinuxAccount().getUsername().getUsername().toString(), sudo);
}
}
Set sudoersFiles = AoCollections.newHashSet(sudoers.size()); // Filenames might not match username when added by a package
if (!sudoers.isEmpty()) {
// Install package when first needed
PackageManager.installPackage(PackageManager.PackageName.SUDO);
// Create /etc/sudoers.d if missing
if (!SUDOERS_D.getStat().exists()) {
SUDOERS_D.mkdir(false, 0750);
}
// Update any files
ByteArrayOutputStream bout = new ByteArrayOutputStream();
for (Map.Entry entry : sudoers.entrySet()) {
String username = entry.getKey();
String sudo = entry.getValue();
bout.reset();
try (Writer out = new OutputStreamWriter(bout, StandardCharsets.UTF_8)) {
out.write("##\n"
+ "## Configured by ");
out.write(LinuxAccountManager.class.getName());
out.write("\n"
+ "## \n"
+ "## See ");
out.write(UserServer.class.getName());
out.write(".getSudo()\n"
+ "##\n");
out.write(username);
out.write(' ');
out.write(sudo);
out.write('\n');
}
String sudoersFilename;
if (
// Amazon EC2 cloud-init
username.equals(User.CENTOS.toString())
&& PackageManager.getInstalledPackage(PackageManager.PackageName.CLOUD_INIT) != null
) {
// Overwrite the file that is created by the "cloud-init" package on boot
sudoersFilename = "90-cloud-init-users";
} else {
sudoersFilename = username;
}
DaemonFileUtils.atomicWrite(
new PosixFile(SUDOERS_D, sudoersFilename, true),
bout.toByteArray(),
0440,
PosixFile.ROOT_UID,
PosixFile.ROOT_GID,
null,
restorecon
);
sudoersFiles.add(sudoersFilename);
}
}
// restorecon any new config files
DaemonFileUtils.restorecon(restorecon);
restorecon.clear();
// Delete any extra files
String[] list = SUDOERS_D.list();
if (list != null) {
for (String filename : list) {
if (!sudoersFiles.contains(filename)) {
File toDelete = new File(SUDOERS_D.getFile(), filename);
if (logger.isLoggable(Level.INFO)) {
logger.info("Scheduling for removal: " + toDelete);
}
deleteFileList.add(toDelete);
}
}
}
} else if (osvId == OperatingSystemVersion.CENTOS_5_I686_AND_X86_64) {
// Not supporting sudo on these operating system versions
} else {
throw new AssertionError("Unsupported OperatingSystemVersion: " + osv);
}
// Disable and enable accounts
// TODO: Put "!" in from of the password when disabled, like done for usermod --lock
// TODO: Then no longer have PredisablePassword stored in the master.
// TODO: Consider effect on isPasswordSet and comparePassword (should password check still work on disabled user?)
for (UserServer lsa : lsas) {
String prePassword = lsa.getPredisablePassword();
if (lsa.isDisabled()) {
// Account is disabled
if (prePassword == null) {
User.Name username = lsa.getLinuxAccount_username_id();
if (logger.isLoggable(Level.INFO)) {
logger.info("Storing predisable password for " + username);
}
lsa.setPredisablePassword(getEncryptedPassword(username).getElement1());
if (logger.isLoggable(Level.INFO)) {
logger.info("Clearing password for " + username);
}
setPassword(username, null, false);
}
} else {
// Account is enabled
if (prePassword != null) {
User.Name username = lsa.getLinuxAccount_username_id();
if (logger.isLoggable(Level.INFO)) {
logger.info("Restoring password for " + username);
}
setEncryptedPassword(username, prePassword, null);
if (logger.isLoggable(Level.INFO)) {
logger.info("Clearing predisable password for " + username);
}
lsa.setPredisablePassword(null);
}
}
}
// Only the top level server in a physical server gets to kill processes
if (AoservDaemonConfiguration.isNested()) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("This server is nested, not killing processes.");
}
} else if (thisServer.getFailoverServer() != null) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("This server is in a fail-over state, not killing processes; parent server will kill processes.");
}
} else {
List nestedServers = thisServer.getNestedServers();
/*
* Kill any processes that are running as a UID that
* should not exist on this server.
*/
File procDir = new File("/proc");
String[] procList = procDir.list();
if (procList != null) {
for (String filename : procList) {
int flen = filename.length();
boolean allNum = true;
for (int d = 0; d < flen; d++) {
char ch = filename.charAt(d);
if (ch < '0' || ch > '9') {
allNum = false;
break;
}
}
if (allNum) {
try {
int pid = Integer.parseInt(filename);
LinuxProcess process = new LinuxProcess(pid);
int uid = process.getUid();
// Never kill root processes, just to be safe
if (uid != UserServer.ROOT_UID) {
// Search each server
UserServer lsa;
if (
!uids.contains(uid)
|| (lsa = thisServer.getLinuxServerAccount(LinuxId.valueOf(uid))) == null
|| lsa.isDisabled()
) {
// Also must not be in a nested server
boolean foundInNested = false;
for (Server nestedServer : nestedServers) {
lsa = nestedServer.getLinuxServerAccount(LinuxId.valueOf(uid));
if (
lsa != null
&& !lsa.isDisabled()
) {
foundInNested = true;
break;
}
}
if (!foundInNested) {
if (logger.isLoggable(Level.INFO)) {
logger.info("Killing process # " + pid + " running as user # " + uid);
}
process.killProc();
}
}
}
} catch (FileNotFoundException err) {
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "It is normal that this is thrown if the process has already closed", err);
}
} catch (IOException | ValidationException err) {
logger.log(Level.SEVERE, "filename=" + filename, err);
}
}
}
}
}
/*
* Recursively find and remove any temporary files that should not exist.
*/
try {
List tmpToDelete = new ArrayList<>();
AoservDaemon.findUnownedFiles(new File("/tmp"), uids, tmpToDelete, 0);
AoservDaemon.findUnownedFiles(new File("/var/tmp"), uids, tmpToDelete, 0);
for (File toDelete : tmpToDelete) {
if (logger.isLoggable(Level.INFO)) {
logger.info("Scheduling for removal: " + toDelete);
}
deleteFileList.add(toDelete);
}
} catch (FileNotFoundException err) {
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE, "This may normally occur because of the dynamic nature of the tmp directories", err);
}
}
// Back-up and delete the files scheduled for removal.
BackupManager.backupAndDeleteFiles(deleteFileList);
// Remove shell packages if installed and no longer required
if (AoservDaemonConfiguration.isPackageManagerUninstallEnabled()) {
if (!hasPasswdShell) {
PackageManager.removePackage(PackageManager.PackageName.AOSERV_PASSWD_SHELL);
}
if (!hasFtpShell) {
PackageManager.removePackage(PackageManager.PackageName.AOSERV_FTP_SHELLS);
}
}
} catch (InterruptedException e) {
logger.log(Level.WARNING, null, e);
// Restore the interrupted status
Thread.currentThread().interrupt();
} finally {
DaemonFileUtils.restorecon(restorecon);
}
}
}
public static String getAutoresponderContent(PosixPath path) throws IOException, SQLException {
PosixFile file = new PosixFile(path.toString());
String content;
if (file.getStat().exists()) {
StringBuilder sb = new StringBuilder();
Server thisServer = AoservDaemon.getThisServer();
try (
InputStream in = new BufferedInputStream(
file.getSecureInputStream(
thisServer.getUidMin().getId(),
thisServer.getGidMin().getId()
)
)
) {
// TODO: This is assuming ISO-8859-1 encoding. Is this correct here?
int ch;
while ((ch = in.read()) != -1) {
sb.append((char) ch);
}
}
content = sb.toString();
} else {
content = "";
}
return content;
}
public static String getCronTable(User.Name username) throws IOException {
File cronFile = new File(cronDirectory, username.toString());
String cronTable;
if (cronFile.exists()) {
StringBuilder sb = new StringBuilder();
try (InputStream in = new BufferedInputStream(new FileInputStream(cronFile))) {
// TODO: This is assuming ISO-8859-1 encoding. Is this correct here?
int ch;
while ((ch = in.read()) != -1) {
sb.append((char) ch);
}
}
cronTable = sb.toString();
} else {
cronTable = "";
}
return cronTable;
}
/**
* @see ShadowFile#getEncryptedPassword(com.aoindustries.aoserv.client.validator.User.Name)
*/
public static Tuple2 getEncryptedPassword(User.Name username) throws IOException, SQLException {
return ShadowFile.getEncryptedPassword(username);
}
public static void setBashProfile(UserServer lsa, String profile) throws IOException, SQLException {
String profileLine = "[ -f '" + profile + "' ] && . '" + profile + "'";
String oldProfileLine = ". " + profile;
PosixFile profileFile = new PosixFile(lsa.getHome().toString(), BASHRC);
// Make sure the file exists
if (profileFile.getStat().exists()) {
Server thisServer = AoservDaemon.getThisServer();
int uidMin = thisServer.getUidMin().getId();
int gidMin = thisServer.getGidMin().getId();
boolean found = false;
// Read the old file, looking for the source in the file
try (BufferedReader in = new BufferedReader(new InputStreamReader(profileFile.getSecureInputStream(uidMin, gidMin)))) {
String line;
while ((line = in.readLine()) != null) {
line = line.trim();
if (
line.equals(profileLine)
|| line.equals(oldProfileLine)
) {
found = true;
break;
}
}
}
if (!found) {
try (RandomAccessFile out = profileFile.getSecureRandomAccessFile("rw", uidMin, gidMin)) {
out.seek(out.length());
out.seek(out.length());
out.write('\n');
out.writeBytes(profileLine);
out.write('\n');
}
}
}
}
public static void setAutoresponderContent(PosixPath path, String content, int uid, int gid) throws IOException, SQLException {
Server thisServer = AoservDaemon.getThisServer();
int uidMin = thisServer.getUidMin().getId();
int gidMin = thisServer.getGidMin().getId();
File file = new File(path.toString());
synchronized (rebuildLock) {
if (content == null) {
if (file.exists()) {
Files.delete(file.toPath());
}
} else {
try (
PrintWriter out = new PrintWriter(
new BufferedOutputStream(
new PosixFile(file).getSecureOutputStream(uid, gid, 0600, true, uidMin, gidMin)
)
)
) {
out.print(content);
}
}
}
}
public static void setCronTable(User.Name username, String cronTable) throws IOException, SQLException {
Server thisServer = AoservDaemon.getThisServer();
int uidMin = thisServer.getUidMin().getId();
int gidMin = thisServer.getGidMin().getId();
File cronFile = new File(cronDirectory, username.toString());
synchronized (rebuildLock) {
if (cronTable.isEmpty()) {
if (cronFile.exists()) {
Files.delete(cronFile.toPath());
}
} else {
try (
PrintWriter out = new PrintWriter(
new BufferedOutputStream(
new PosixFile(cronFile).getSecureOutputStream(
PosixFile.ROOT_UID,
thisServer.getLinuxServerAccount(username).getPrimaryLinuxServerGroup().getGid().getId(),
0600,
true,
uidMin,
gidMin
)
)
)
) {
out.print(cronTable);
}
}
}
}
/**
* @see ShadowFile#setEncryptedPassword(com.aoindustries.aoserv.client.validator.User.Name, java.lang.String, java.lang.Integer)
*/
public static void setEncryptedPassword(User.Name username, String encryptedPassword, Integer changedDate) throws IOException, SQLException {
Server linuxServer = AoservDaemon.getThisServer();
UserServer lsa = linuxServer.getLinuxServerAccount(username);
if (lsa == null) {
throw new SQLException("Unable to find UserServer: " + username + " on " + linuxServer);
}
ShadowFile.setEncryptedPassword(username, encryptedPassword, changedDate);
}
@SuppressWarnings("deprecation")
public static void setPassword(User.Name username, String plainPassword, boolean updateChangedDate) throws IOException, SQLException {
Server linuxServer = AoservDaemon.getThisServer();
UserServer lsa = linuxServer.getLinuxServerAccount(username);
if (lsa == null) {
throw new SQLException("Unable to find UserServer: " + username + " on " + linuxServer);
}
PosixFile.CryptAlgorithm cryptAlgorithm;
OperatingSystemVersion osv = linuxServer.getHost().getOperatingSystemVersion();
switch (osv.getPkey()) {
case OperatingSystemVersion.CENTOS_5_I686_AND_X86_64:
cryptAlgorithm = PosixFile.CryptAlgorithm.MD5;
break;
case OperatingSystemVersion.CENTOS_7_X86_64:
cryptAlgorithm = PosixFile.CryptAlgorithm.SHA512;
break;
default:
throw new AssertionError("Unsupported OperatingSystemVersion: " + osv);
}
ShadowFile.setPassword(username, plainPassword, cryptAlgorithm, updateChangedDate);
}
private static LinuxAccountManager linuxAccountManager;
@SuppressWarnings("UseOfSystemOutOrSystemErr")
public static void start() throws IOException, SQLException {
Server thisServer = AoservDaemon.getThisServer();
OperatingSystemVersion osv = thisServer.getHost().getOperatingSystemVersion();
int osvId = osv.getPkey();
synchronized (System.out) {
if (
// Nothing is done for these operating systems
osvId != OperatingSystemVersion.CENTOS_5_DOM0_I686
&& osvId != OperatingSystemVersion.CENTOS_5_DOM0_X86_64
&& osvId != OperatingSystemVersion.CENTOS_7_DOM0_X86_64
// Check config after OS check so config entry not needed
&& AoservDaemonConfiguration.isManagerEnabled(LinuxAccountManager.class)
&& linuxAccountManager == null
) {
System.out.print("Starting LinuxAccountManager: ");
// Must be a supported operating system
if (
osvId == OperatingSystemVersion.CENTOS_5_I686_AND_X86_64
|| osvId == OperatingSystemVersion.CENTOS_7_X86_64
) {
AoservConnector conn = AoservDaemon.getConnector();
linuxAccountManager = new LinuxAccountManager();
conn.getFtp().getGuestUser().addTableListener(linuxAccountManager, 0);
conn.getLinux().getUser().addTableListener(linuxAccountManager, 0);
conn.getLinux().getGroupUser().addTableListener(linuxAccountManager, 0);
conn.getLinux().getUserServer().addTableListener(linuxAccountManager, 0);
conn.getLinux().getGroupServer().addTableListener(linuxAccountManager, 0);
// TODO: This seemed to not pick-up correctly. Add a delay?
PackageManager.addPackageListener(linuxAccountManager); // React to users and groups added by RPMs
System.out.println("Done");
} else {
System.out.println("Unsupported OperatingSystemVersion: " + osv);
}
}
}
}
public static void tarHomeDirectory(StreamableOutput out, User.Name username) throws IOException, SQLException {
UserServer lsa = AoservDaemon.getThisServer().getLinuxServerAccount(username);
PosixPath home = lsa.getHome();
try (
TempFileContext tempFileContext = new TempFileContext();
TempFile tempFile = tempFileContext.createTempFile("tar_home_directory_", ".tar")
) {
AoservDaemon.exec(
"/bin/tar",
"-c",
"-C",
home.toString(),
"-f",
tempFile.getFile().getPath(),
"."
);
try (InputStream in = new FileInputStream(tempFile.getFile())) {
byte[] buff = BufferManager.getBytes();
try {
int ret;
while ((ret = in.read(buff, 0, BufferManager.BUFFER_SIZE)) != -1) {
out.writeByte(AoservDaemonProtocol.NEXT);
out.writeShort(ret);
out.write(buff, 0, ret);
}
} finally {
BufferManager.release(buff, false);
}
}
}
}
public static void untarHomeDirectory(StreamableInput in, User.Name username) throws IOException, SQLException {
Server thisServer = AoservDaemon.getThisServer();
int uidMin = thisServer.getUidMin().getId();
int gidMin = thisServer.getGidMin().getId();
synchronized (rebuildLock) {
UserServer lsa = thisServer.getLinuxServerAccount(username);
PosixPath home = lsa.getHome();
try (
TempFileContext tempFileContext = new TempFileContext();
TempFile tempFile = tempFileContext.createTempFile("untar_home_directory_", ".tar")
) {
int code;
try (OutputStream out = new FileOutputStream(tempFile.getFile())) {
byte[] buff = BufferManager.getBytes();
try {
while ((code = in.readByte()) == AoservDaemonProtocol.NEXT) {
int len = in.readShort();
in.readFully(buff, 0, len);
out.write(buff, 0, len);
}
} finally {
BufferManager.release(buff, false);
}
}
if (code != AoservDaemonProtocol.DONE) {
if (code == AoservDaemonProtocol.IO_EXCEPTION) {
throw new IOException(in.readUTF());
} else if (code == AoservDaemonProtocol.SQL_EXCEPTION) {
throw new SQLException(in.readUTF());
} else {
throw new IOException("Unknown result: " + code);
}
}
AoservDaemon.exec(
"/bin/tar",
"-x",
"-C",
home.toString(),
"-f",
tempFile.getFile().getPath()
);
}
}
}
public static void waitForRebuild() {
if (linuxAccountManager != null) {
linuxAccountManager.waitForBuild();
}
}
@Override
public String getProcessTimerDescription() {
return "Rebuild Linux Accounts";
}
@Override
public long getProcessTimerMaximumTime() {
return 15L * 60 * 1000;
}
/**
* Allows manual rebuild without the necessity of running the entire daemon (use carefully, only when main daemon not running).
*/
@SuppressWarnings({"UseSpecificCatch", "TooBroadCatch", "UseOfSystemOutOrSystemErr"})
public static void main(String[] args) {
try {
rebuildLinuxAccountSettings();
} catch (ThreadDeath td) {
throw td;
} catch (Throwable t) {
ErrorPrinter.printStackTraces(t, System.err);
System.exit(SysExits.getSysExit(t));
}
}
private LinuxAccountManager() {
// Do nothing
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy