GP-394 Added svrAdmin grant and revoke repository access command support. Added Ghidra Server asynchronous command processing and improved svrAdmin -list command usage.

This commit is contained in:
ghidra1 2022-03-15 14:35:38 -04:00
parent ee268dea09
commit 8446a00aff
8 changed files with 1015 additions and 653 deletions

View file

@ -0,0 +1,303 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.server;
import java.io.*;
import java.util.*;
import javax.security.auth.x500.X500Principal;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import ghidra.framework.remote.User;
import ghidra.framework.store.local.LocalFileSystem;
import ghidra.util.exception.DuplicateNameException;
import utilities.util.FileUtilities;
/**
* <code>CommandProcessor</code> provides server processing of commands
* queued by the {@link ServerAdmin} class which corresponds to the <code>svrAdmin</code>
* shell command.
*/
public class CommandProcessor {
static final Logger log = LogManager.getLogger(CommandProcessor.class);
// Queued commands
static final String ADD_USER_COMMAND = "-add";
static final String REMOVE_USER_COMMAND = "-remove";
static final String RESET_USER_COMMAND = "-reset";
static final String SET_USER_DN_COMMAND = "-dn";
static final String GRANT_USER_COMMAND = "-grant";
static final String REVOKE_USER_COMMAND = "-revoke";
static final String PASSWORD_OPTION = "--p"; // applies to add and reset commands
private static final String ADMIN_CMD_DIR = LocalFileSystem.HIDDEN_DIR_PREFIX + "admin";
private static final String COMMAND_FILE_EXT = ".cmd";
// private static final int LOCK_TIMEOUT = 30000;
/**
* Command file filter
*/
static final FileFilter CMD_FILE_FILTER =
f -> f.isFile() && f.getName().endsWith(COMMAND_FILE_EXT);
/**
* File date comparator
*/
static final Comparator<File> FILE_DATE_COMPARATOR = (f1, f2) -> {
long t1 = f1.lastModified();
long t2 = f2.lastModified();
long diff = t1 - t2;
if (diff == 0) {
return 0;
}
return diff < 0 ? -1 : 1;
};
private CommandProcessor() {
}
/**
* Split a command string into individual arguments.
* @param cmd command string
* @return array of command arguments
*/
private static String[] splitCommand(String cmd) {
ArrayList<String> argList = new ArrayList<>();
int startIx = 0;
int endIx = 0;
int len = cmd.length();
boolean insideQuote = false;
while (endIx < len) {
char c = cmd.charAt(endIx);
if (!insideQuote && startIx == endIx) {
if (c == ' ' || c == '\"') {
insideQuote = (c == '\"');
startIx = ++endIx;
continue;
}
}
if (c == (insideQuote ? '\"' : ' ')) {
argList.add(cmd.substring(startIx, endIx));
startIx = ++endIx;
insideQuote = false;
}
else {
++endIx;
}
}
if (startIx != endIx) {
argList.add(cmd.substring(startIx, endIx));
}
String[] args = new String[argList.size()];
argList.toArray(args);
return args;
}
/**
* Process the specified command.
* @param repositoryMgr server's repository manager
* @param cmd command string
* @throws IOException if IO error occurs while processing command
*/
private static void processCommand(RepositoryManager repositoryMgr, String cmd)
throws IOException {
UserManager userMgr = repositoryMgr.getUserManager();
String[] args = splitCommand(cmd);
switch (args[0]) {
case ADD_USER_COMMAND: // add user
String sid = args[1];
char[] pwdHash = null;
if (args.length == 4 && args[2].contentEquals(PASSWORD_OPTION)) {
pwdHash = args[3].toCharArray();
}
try {
userMgr.addUser(sid, pwdHash);
}
catch (DuplicateNameException e) {
log.error("Add User Failed: " + e.getMessage());
}
break;
case REMOVE_USER_COMMAND: // remove user
sid = args[1];
if (!userMgr.removeUser(sid)) {
log.info("User not found: '" + sid + "'");
}
break;
case RESET_USER_COMMAND: // reset user
sid = args[1];
pwdHash = null;
if (args.length == 4 && args[2].contentEquals(PASSWORD_OPTION)) {
pwdHash = args[3].toCharArray();
}
if (!userMgr.resetPassword(sid, pwdHash)) {
log.info("Failed to reset password for user '" + sid + "'");
}
else if (pwdHash != null) {
log.info("User '" + sid + "' password reset to specified password");
}
else {
log.info("User '" + sid + "' password reset to default password");
}
break;
case SET_USER_DN_COMMAND: // set/add user with DN for PKI
sid = args[1];
X500Principal x500User = new X500Principal(args[2]);
if (userMgr.isValidUser(sid)) {
userMgr.setDistinguishedName(sid, x500User);
}
else {
try {
userMgr.addUser(sid, x500User);
}
catch (DuplicateNameException e) {
// should never occur
}
}
log.info("User '" + sid + "' DN set (" + x500User.getName() + ")");
break;
case GRANT_USER_COMMAND: // grant repository access
sid = args[1];
int permission = parsePermission(args[2]);
String repName = args[3];
if (!userMgr.isValidUser(sid)) {
log.error(
"Failed to grant access for '" + sid +
"', user has not been added to server.");
return;
}
if (permission < 0) {
log.error("Failed to process grant command. Invalid permission: " + args[2]);
return;
}
Repository rep = repositoryMgr.getRepository(repName);
if (rep == null) {
log.error("Failed to grant access for '" + sid + "', repository '" + repName +
"' not found.");
}
rep.setUserPermission(sid, permission);
break;
case REVOKE_USER_COMMAND: // grant repository access
sid = args[1];
repName = args[2];
rep = repositoryMgr.getRepository(repName);
if (rep == null) {
log.error("Failed to revoke access for '" + sid + "', repository '" + repName +
"' not found.");
}
rep.removeUser(sid);
break;
default:
log.error("Failed to process unrecognized command: " + args[0]);
}
}
static int parsePermission(String permissionStr) {
if ("+r".equals(permissionStr)) {
return User.READ_ONLY;
}
if ("+w".equals(permissionStr)) {
return User.WRITE;
}
if ("+a".equals(permissionStr)) {
return User.ADMIN;
}
return -1;
}
static File getCommandDir(File serverRootDir) {
return new File(serverRootDir, ADMIN_CMD_DIR);
}
static File getOrCreateCommandDir(RepositoryManager repositoryMgr) {
File cmdDir = getCommandDir(repositoryMgr.getRootDir());
if (!cmdDir.exists()) {
// ensure process owner creates queued command directory
cmdDir.mkdir();
}
return cmdDir;
}
/**
* Process all queued commands for the specified server.
* @param repositoryMgr server's repository manager
* @throws IOException
*/
static void processCommands(RepositoryManager repositoryMgr) throws IOException {
File cmdDir = getOrCreateCommandDir(repositoryMgr);
File[] files = cmdDir.listFiles(CMD_FILE_FILTER);
if (files == null) {
log.error("Failed to access command queue " + cmdDir.getAbsolutePath() +
": possible permission problem");
return;
}
if (files.length == 0) {
return;
}
log.info("Processing queued commands");
Arrays.sort(files, FILE_DATE_COMPARATOR);
for (File file : files) {
List<String> cmdList = FileUtilities.getLines(file);
for (String cmdStr : cmdList) {
if (cmdStr.isBlank()) {
continue;
}
try {
processCommand(repositoryMgr, cmdStr.trim());
}
catch (ArrayIndexOutOfBoundsException e) {
log.error("Error occured processing command: " + cmdStr);
}
}
file.delete();
}
}
/**
* Store a list of command strings to a new command file.
* @param cmdList list of command strings
* @param cmdDir command file directory (must exist)
* @throws IOException
*/
static void writeCommands(List<String> cmdList, File cmdDir) throws IOException {
File cmdTempFile = null;
try {
// Write command to temp file
cmdTempFile = File.createTempFile("adm", ".tmp", cmdDir);
FileUtils.writeLines(cmdTempFile, cmdList);
// Rename temp file to *.cmd file
String cmdFilename = cmdTempFile.getName();
cmdFilename = cmdFilename.substring(0, cmdFilename.length() - 4) + COMMAND_FILE_EXT;
File cmdFile = new File(cmdTempFile.getParentFile(), cmdFilename);
if (!cmdTempFile.renameTo(cmdFile)) {
throw new IOException("file error");
}
cmdTempFile = null;
}
finally {
if (cmdTempFile != null) {
cmdTempFile.delete();
}
}
}
}

View file

@ -0,0 +1,114 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.server;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
/**
* <code>CommandWatcher</code> watches the command queue directory (~admin) for new
* command files and initiates their processing in the order they were issued.
* The use of the {@link WatchService} is limited to detection of command file creation
* and invokes {@link RepositoryManager#processCommandQueue()} when one or more
* command files have been queued or an {@link StandardWatchEventKinds#OVERFLOW}
* event occurs.
*/
public class CommandWatcher implements Runnable {
private RepositoryManager repositoryMgr;
private Path cmdDirPath;
private WatchService watcher;
CommandWatcher(RepositoryManager repositoryMgr) throws IOException {
this.repositoryMgr = repositoryMgr;
watcher = FileSystems.getDefault().newWatchService();
cmdDirPath = CommandProcessor.getOrCreateCommandDir(repositoryMgr).toPath();
cmdDirPath.register(watcher, StandardWatchEventKinds.ENTRY_CREATE);
}
void dispose() {
try {
watcher.close();
}
catch (IOException e) {
// ignore
}
}
@Override
public void run() {
RepositoryManager.log.info("Command watcher started");
while (true) {
// wait for key to be signaled
WatchKey key;
try {
key = watcher.take();
}
catch (InterruptedException | ClosedWatchServiceException e) {
break;
}
boolean processCommands = false;
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
// An OVERFLOW event can occur if events are lost or discarded.
if (kind == StandardWatchEventKinds.OVERFLOW) {
processCommands = true;
continue;
}
// The filename is the
// context of the event.
@SuppressWarnings("unchecked")
WatchEvent<Path> ev = (WatchEvent<Path>) event;
Path filename = ev.context();
// Verify that the new file is a command file - ignore all others
Path child = cmdDirPath.resolve(filename);
File file = child.toFile();
// Only care about command files which still exist since
// they may have already been consumed
if (CommandProcessor.CMD_FILE_FILTER.accept(child.toFile()) && file.exists()) {
processCommands = true;
}
}
if (processCommands) {
try {
repositoryMgr.processCommandQueue();
}
catch (Exception e) {
RepositoryManager.log.error("Command processing failure: " + e.toString(), e);
}
}
// Reset the key to receive further watch events.
// Key will become invalid when closed/disposed
boolean valid = key.reset();
if (!valid) {
break;
}
}
RepositoryManager.log.info("Command watcher terminated.");
}
}

View file

@ -71,14 +71,15 @@ public class Repository implements FileSystemListener, RepositoryLogger {
/**
* Create a new Repository at the given path; the directory has already
* been created.
* @param mgr repository manager
* @param currentUser user creating the repository, or null if the
* repository exists
* @param rootFile root file for this repository
* @param initialize true means
* @throws IOException
* @param name repository name
* @throws IOException if filesystem error occurs
*/
public Repository(RepositoryManager mgr, String currentUser, File rootFile, String name)
throws IOException, UserAccessException {
throws IOException {
this.mgr = mgr;
this.name = name;
@ -275,14 +276,17 @@ public class Repository implements FileSystemListener, RepositoryLogger {
/**
* Get the name of this repository.
* @return name of the repository.
* @throws IOException
*/
public String getName() {
return name;
}
/**
* @see FileSystem#getItemCount()
* Get the total number of items contained within this repository.
* See {@link FileSystem#getItemCount()}.
* @return total number of repository items
* @throws IOException if filesystem IO error occurs
* @throws UnsupportedOperationException if file-system does not support this operation
*/
public int getItemCount() throws IOException, UnsupportedOperationException {
return fileSystem.getItemCount();
@ -321,7 +325,7 @@ public class Repository implements FileSystemListener, RepositoryLogger {
/**
* Convenience method for getting list of all "Known" users
* defined to the repository user manager.
* @param currentUser
* @param currentUser user performing request
* @return list of user names.
* @throws IOException
*/
@ -336,9 +340,9 @@ public class Repository implements FileSystemListener, RepositoryLogger {
* Set the user access list.
* @param currentUser user that is setting the access list on this
* repository; the current user must
* @param users
* @param allowAnonymousAccess
* @throws UserAccessException
* @param users user access list
* @param allowAnonymousAccess true if anonymous access should be permitted (assume allowed by server config).
* @throws UserAccessException if currentUser is not a current repository admin
* @throws IOException
*/
public void setUserList(String currentUser, User[] users, boolean allowAnonymousAccess)
@ -382,22 +386,55 @@ public class Repository implements FileSystemListener, RepositoryLogger {
}
/**
* Privileged method for adding a new repository admin
* @param sid user username
* @throws IOException
* Privileged method for setting user access for this repository
* @param username user username
* @param permission access permission ({@link User#READ_ONLY},
* {@link User#WRITE}, or {@link User#ADMIN}).
* @return true if successful, false if user has not been added to server
* @throws IOException failed to update repository access list
*/
void addAdmin(String username) throws IOException {
boolean setUserPermission(String username, int permission) throws IOException {
synchronized (fileSystem) {
userMap.remove(username);
userMap.put(username, new User(username, User.ADMIN));
writeUserList(userMap, anonymousAccessAllowed);
if (permission < User.READ_ONLY || permission > User.ADMIN) {
throw new IllegalArgumentException("Invalid permission: " + permission);
}
if (mgr.getUserManager().isValidUser(username)) {
User newUser = new User(username, permission);
User oldUser = userMap.put(username, newUser);
writeUserList(userMap, anonymousAccessAllowed);
if (oldUser != null) {
log.info("User access to repository '" + name + "' changed: " + newUser);
}
else {
log.info("User access granted to repository '" + name + "': " + newUser);
}
return true;
}
return false;
}
}
/**
* Privileged method for removing user access from this repository
* @param username user username
* @return true if user had access and has been successfully removed, else false
* @throws IOException failed to update repository access list
*/
boolean removeUser(String username) throws IOException {
synchronized (fileSystem) {
if (userMap.remove(username) != null) {
writeUserList(userMap, anonymousAccessAllowed);
log.info("User access d from repository '" + name + "': " + username);
return true;
}
return false;
}
}
/**
* Get the list of known users for this repository.
* @param currentUser user that is requesting the user list.
* @throws UserAccessException
* @throws UserAccessException if currentUser is not a current repository admin
* @throws IOException
*/
public User[] getUserList(String currentUser) throws UserAccessException, IOException {
@ -428,29 +465,18 @@ public class Repository implements FileSystemListener, RepositoryLogger {
* Get the specified user data.
* If the repository's user list if missing or currupt, this user
* will become its administrator.
* @param currentUser
* @param username user name attempting repository access
* @return user data
*/
public User getUser(String currentUser) {
public User getUser(String username) {
synchronized (fileSystem) {
if (anonymousAccessAllowed && UserManager.ANONYMOUS_USERNAME.equals(currentUser)) {
if (anonymousAccessAllowed && UserManager.ANONYMOUS_USERNAME.equals(username)) {
return ANONYMOUS_USER;
}
if (userMap.isEmpty()) {
log.error("Empty repository access list, will attempt repair (" + name + ")");
log.warn("Adding user " + currentUser + " as Admin to repository (" + name + ")");
userMap.put(currentUser, new User(currentUser, User.ADMIN));
try {
writeUserList(currentUser, userMap, anonymousAccessAllowed);
}
catch (Exception e) {
log.error("Failed to repair repository access list: " + e.getMessage());
}
}
User user = userMap.get(currentUser);
User user = userMap.get(username);
if (user == null && anonymousAccessAllowed) {
// allow authenticated user to access repository in read-only mode
return new User(currentUser, User.READ_ONLY);
return new User(username, User.READ_ONLY);
}
return user;
}
@ -460,8 +486,8 @@ public class Repository implements FileSystemListener, RepositoryLogger {
* Write user access list to local file.
* @param currentUser current user
* @param newUserMap user map
* @param allowAnonymous
* @throws UserAccessException
* @param allowAnonymous true if anonymous access is allowed
* @throws UserAccessException if currentUser does not have admin priviledge
* @throws IOException
*/
private void writeUserList(String currentUser, LinkedHashMap<String, User> newUserMap,
@ -472,13 +498,14 @@ public class Repository implements FileSystemListener, RepositoryLogger {
throw new UserAccessException(currentUser + " must have ADMIN privilege!");
}
writeUserList(newUserMap, allowAnonymous);
log.info("User access list for repository '" + name + "' updated by: " + currentUser);
}
/**
* Privileged method for updating user access list.
* @param newUserMap
* @param allowAnonymous
* @throws UserAccessException
* @param newUserMap user map
* @param allowAnonymous true if anonymous access is allowed
* @throws UserAccessException if currentUser does not have admin priviledge
* @throws IOException
*/
private void writeUserList(LinkedHashMap<String, User> newUserMap, boolean allowAnonymous)
@ -533,27 +560,63 @@ public class Repository implements FileSystemListener, RepositoryLogger {
}
/**
* Print to stdout the user access permissions to the specified repository.
* Generate formatted list of user access permissions to the specified repository.
* This is intended to be used with the svrAdmin console command
* @param repositoryDir repository directory
* @param pad padding string to be prefixed to each output line
* @return formatted list of user access permissions
*/
static void listUserPermissions(File repositoryDir, String pad) {
static String getFormattedUserPermissions(File repositoryDir, String pad) {
StringBuilder buf = new StringBuilder();
File userAccessFile = new File(repositoryDir, ACCESS_CONTROL_FILENAME);
try {
ArrayList<User> list = new ArrayList<>();
List<User> list = new ArrayList<>();
boolean anonymousAccessAllowed = readAccessFile(userAccessFile, list);
Collections.sort(list);
if (anonymousAccessAllowed) {
System.out.println(pad + "* Anonymous read-only access permitted *");
buf.append(pad + "* Anonymous read-only access permitted *\n");
}
for (User user : list) {
System.out.println(pad + user);
buf.append(pad + user + "\n");
}
}
catch (IOException e) {
System.out.println(pad + "Failed to read repository access file: " + e.getMessage());
}
return buf.toString();
}
/**
* Generate formatted list of user access permissions to the specified repository
* restricted to user names contained within listUserAccess.
* This is intended to be used with the svrAdmin console command
* @param repositoryDir repository directory
* @param pad padding string to be prefixed to each output line
* @param listUserAccess set of user names of interest
* @return formatted list of user access permissions or null if no users of interest found
*/
static String getFormattedUserPermissions(File repositoryDir, String pad,
Set<String> listUserAccess) {
StringBuilder buf = null;
File userAccessFile = new File(repositoryDir, ACCESS_CONTROL_FILENAME);
try {
ArrayList<User> list = new ArrayList<>();
readAccessFile(userAccessFile, list);
Collections.sort(list);
for (User user : list) {
if (!listUserAccess.contains(user.getName())) {
continue;
}
if (buf == null) {
buf = new StringBuilder();
}
buf.append(pad + user + "\n");
}
}
catch (IOException e) {
System.out.println(pad + "Failed to read repository access file: " + e.getMessage());
}
return buf != null ? buf.toString() : null;
}
/**

View file

@ -31,7 +31,8 @@ import ghidra.framework.store.local.LocalFileSystem;
import ghidra.server.remote.RepositoryServerHandleImpl;
import ghidra.util.NamingUtilities;
import ghidra.util.StringUtilities;
import ghidra.util.exception.*;
import ghidra.util.exception.DuplicateFileException;
import ghidra.util.exception.UserAccessException;
import utilities.util.FileUtilities;
/**
@ -43,6 +44,7 @@ public class RepositoryManager {
private static Map<Thread, String> clientNameMap = new WeakHashMap<>();
private File rootDirFile;
private CommandWatcher commandWatcher;
private HashMap<String, Repository> repositoryMap; // maps name to Repository
private ArrayList<RepositoryServerHandleImpl> handleList = new ArrayList<>();
private UserManager userMgr;
@ -92,6 +94,7 @@ public class RepositoryManager {
* Dispose this repository manager and all repository instances
*/
public synchronized void dispose() {
commandWatcher.dispose();
Iterator<Repository> iter = repositoryMap.values().iterator();
while (iter.hasNext()) {
Repository rep = iter.next();
@ -101,6 +104,7 @@ public class RepositoryManager {
/**
* Return repositories root directory
* @return server root directory
*/
File getRootDir() {
return rootDirFile;
@ -111,14 +115,14 @@ public class RepositoryManager {
* @param currentUser user creating the repository
* @param name name of the repository
* @return a new Repository
* @throws DuplicateNameException if another repository exists with the
* @throws DuplicateFileException if another repository exists with the
* given name
* @throws UserAccessException if the user does not exist in
* the list of known users for this manager
* @throws IOException if there was an error creating the repository
*/
public synchronized Repository createRepository(String currentUser, String name)
throws IOException {
throws IOException, DuplicateFileException {
if (isAnonymousUser(currentUser)) {
throw new UserAccessException("Anonymous user not permitted to create repository");
@ -179,7 +183,7 @@ public class RepositoryManager {
* Delete a specified repository.
* @param currentUser current user
* @param name repository name
* @throws IOException
* @throws IOException if error occurs while removing repository
*/
public synchronized void deleteRepository(String currentUser, String name) throws IOException {
@ -229,6 +233,13 @@ public class RepositoryManager {
return list.toArray(names);
}
private synchronized String[] getRepositoryNames() {
Set<String> nameSet = repositoryMap.keySet();
String[] names = nameSet.toArray(new String[nameSet.size()]);
Arrays.sort(names);
return names;
}
/**
* Get all defined users. If currentUser is an
* Anonymous user an empty array will be returned.
@ -236,17 +247,11 @@ public class RepositoryManager {
* @return array of users known to this manager or empty array if
* we should not reveal to currentUser.
*/
public synchronized String[] getAllUsers(String currentUser) throws IOException {
public synchronized String[] getAllUsers(String currentUser) {
if (isAnonymousUser(currentUser)) {
return new String[0];
}
try {
return userMgr.getUsers();
}
catch (IOException e) {
log.error("Error while accessing user list: " + e.getMessage());
throw new IOException("Failed to read user list");
}
return userMgr.getUsers();
}
public UserManager getUserManager() {
@ -256,7 +261,7 @@ public class RepositoryManager {
/**
* Verify that the specified currentUser is a known user
* @param currentUser current user
* @throws UserAccessException
* @throws UserAccessException specified user is not valid
*/
private void validateUser(String currentUser) throws UserAccessException {
if (!userMgr.isValidUser(currentUser)) {
@ -266,7 +271,7 @@ public class RepositoryManager {
/**
* Scan for existing repositories and build repositoryMap.
* @throws IOException
* @throws IOException if error occurs accessing or writing to server storage directory
*/
private void initialize() throws IOException {
@ -301,7 +306,22 @@ public class RepositoryManager {
}
}
userMgr.updateUserList(true);
// Start command queue watcher
commandWatcher = new CommandWatcher(this);
Thread t = new Thread(commandWatcher, "Server Command Watcher");
t.start();
processCommandQueue(); // process any old commands
}
/**
* Refresh the server's user list and process any pending UserAdmin commands.
* @throws IOException if error occurs processing command files
*/
synchronized void processCommandQueue() throws IOException {
userMgr.readUserListIfNeeded();
userMgr.clearExpiredPasswords();
CommandProcessor.processCommands(this);
}
static String getElapsedTimeSince(long t) {
@ -445,44 +465,84 @@ public class RepositoryManager {
* Print to stdout the set of repository names defined within the specified repositories root.
* This is intended to be used with the svrAdmin console command
* @param repositoriesRootDir repositories root directory
* @param includeUserAccessDetails
* @param includeUserAccessDetails if true additional user access details will displayed
* for each repository
*/
static void listRepositories(File repositoriesRootDir, boolean includeUserAccessDetails) {
String[] names = RepositoryManager.getRepositoryNames(repositoriesRootDir);
System.out.println("\nRepositories:");
if (names.length == 0) {
System.out.println(" <No repositories have been created>");
return;
}
else {
for (String name : names) {
File repoDir = new File(repositoriesRootDir, NamingUtilities.mangle(name));
String rootPath = repoDir.getAbsolutePath();
boolean isIndexed = IndexedLocalFileSystem.isIndexed(rootPath);
String type;
if (isIndexed || IndexedLocalFileSystem.hasIndexedStructure(rootPath)) {
type = "Indexed Filesystem";
try {
int indexVersion = IndexedLocalFileSystem.readIndexVersion(rootPath);
if (indexVersion == IndexedLocalFileSystem.LATEST_INDEX_VERSION) {
type = null;
}
else {
type += " (V" + indexVersion + ")";
}
for (String name : names) {
File repoDir = new File(repositoriesRootDir, NamingUtilities.mangle(name));
String rootPath = repoDir.getAbsolutePath();
boolean isIndexed = IndexedLocalFileSystem.isIndexed(rootPath);
String type;
if (isIndexed || IndexedLocalFileSystem.hasIndexedStructure(rootPath)) {
type = "Indexed Filesystem";
try {
int indexVersion = IndexedLocalFileSystem.readIndexVersion(rootPath);
if (indexVersion == IndexedLocalFileSystem.LATEST_INDEX_VERSION) {
type = null;
}
catch (IOException e) {
type += "(unknown)";
else {
type += " (V" + indexVersion + ")";
}
}
else {
type = "Mangled Filesystem";
catch (IOException e) {
type += "(unknown)";
}
}
else {
type = "Mangled Filesystem";
}
System.out.println(" " + name + (type == null ? "" : (" - uses " + type)));
System.out.println(" " + name + (type == null ? "" : (" - uses " + type)));
if (includeUserAccessDetails) {
Repository.listUserPermissions(repoDir, " ");
if (includeUserAccessDetails) {
System.out.print(Repository.getFormattedUserPermissions(repoDir, " "));
}
}
}
/**
* Print to stdout the repository access permissions for the specified set of users.
* This is intended to be used with the svrAdmin console command
* @param repositoriesRootDir repositories root directory
* @param usernameSet set of users whose details should be displayed
*/
static void listRepositories(File repositoriesRootDir, Set<String> usernameSet) {
String[] names = RepositoryManager.getRepositoryNames(repositoriesRootDir);
if (names.length == 0) {
System.out.println(" <No repositories have been created>");
return;
}
boolean outputHeader = true;
for (String name : names) {
File repoDir = new File(repositoriesRootDir, NamingUtilities.mangle(name));
String formattedAccessList =
Repository.getFormattedUserPermissions(repoDir, " ", usernameSet);
if (formattedAccessList != null) {
if (outputHeader) {
System.out.println("\nRepositories:");
outputHeader = false;
}
System.out.println(" " + name);
System.out.print(formattedAccessList);
}
}
if (outputHeader) {
System.out.println("No repository access found for user(s):");
String[] userNames = usernameSet.toArray(new String[usernameSet.size()]);
Arrays.sort(userNames);
for (String n : userNames) {
System.out.println(" " + n);
}
}
}
@ -504,4 +564,15 @@ public class RepositoryManager {
}
}
/**
* Callback when user removed from server. Remove user from all repository access lists.
* @param username user name
* @throws IOException if error occured while updating repository access lists.
*/
void userRemoved(String username) throws IOException {
for (String repName : getRepositoryNames()) {
getRepository(repName).removeUser(username);
}
}
}

View file

@ -45,8 +45,6 @@ public class ServerAdmin implements GhidraLaunchable {
private static final String MIGRATE_COMMAND = "-migrate";
private static final String MIGRATE_ALL_COMMAND = "-migrate-all";
private boolean propertyUsed = false;
/**
* Main method for launching the ServerAdmin Application via GhidraLauncher.
* The following properties may be set:
@ -86,30 +84,32 @@ public class ServerAdmin implements GhidraLaunchable {
String configFilePath = args.length != 0 && !args[0].startsWith("-") ? args[ix++]
: System.getProperty(CONFIG_FILE_PROPERTY);
File serverDir = getServerDirFromConfig(configFilePath);
if (serverDir == null || (args.length - ix) == 0) {
System.out.println("server.conf: " + configFilePath);
File serverRootDir = getServerDirFromConfig(configFilePath);
if (serverRootDir == null || (args.length - ix) == 0) {
displayUsage("");
System.exit(-1);
return;
}
try {
serverDir = serverDir.getCanonicalFile();
serverRootDir = serverRootDir.getCanonicalFile();
}
catch (IOException e1) {
System.err.println("Failed to resolve server directory: " + serverDir);
System.err.println("Failed to resolve server directory: " + serverRootDir);
System.exit(-1);
}
System.out.println("Using server directory: " + serverDir);
System.out.println("Using server directory: " + serverRootDir);
File userFile = new File(serverDir, UserManager.USER_PASSWORD_FILE);
if (!serverDir.isDirectory() || !userFile.isFile()) {
File userFile = new File(serverRootDir, UserManager.USER_PASSWORD_FILE);
if (!serverRootDir.isDirectory() || !userFile.isFile()) {
System.err.println("Invalid Ghidra server directory!");
System.exit(-1);
}
File cmdDir = new File(serverDir, UserAdmin.ADMIN_CMD_DIR);
File cmdDir = CommandProcessor.getCommandDir(serverRootDir);
if (!cmdDir.isDirectory() || !cmdDir.canWrite()) {
System.err.println("Insufficient privilege or server not started!");
System.exit(-1);
@ -117,6 +117,8 @@ public class ServerAdmin implements GhidraLaunchable {
// Process command line
boolean listRepositories = false;
boolean listAllUserPermissions = false;
Set<String> listUsernameSet = new HashSet<>();
boolean listUsers = false;
boolean migrationConfirmed = false;
boolean migrationAbort = false;
@ -125,98 +127,122 @@ public class ServerAdmin implements GhidraLaunchable {
for (; ix < args.length; ix += cmdLen) {
boolean queueCmd = true;
String pwdHash = null;
if (UserAdmin.ADD_USER_COMMAND.equals(args[ix])) { // add user
cmdLen = 2;
validateSID(args, ix + 1);
if (hasOptionalArg(args, ix + 2, UserAdmin.PASSWORD_OPTION)) {
++cmdLen;
pwdHash = promptForPasswordAndGetSaltedHash(args[ix + 1]);
}
}
else if (UserAdmin.REMOVE_USER_COMMAND.equals(args[ix])) { // remove user
cmdLen = 2;
validateSID(args, ix + 1);
}
else if (UserAdmin.RESET_USER_COMMAND.equals(args[ix])) { // reset user
cmdLen = 2;
validateSID(args, ix + 1);
if (hasOptionalArg(args, ix + 2, UserAdmin.PASSWORD_OPTION)) {
++cmdLen;
pwdHash = promptForPasswordAndGetSaltedHash(args[ix + 1]);
}
}
else if (UserAdmin.SET_USER_DN_COMMAND.equals(args[ix])) { // set/add user with DN for PKI
cmdLen = 3;
validateSID(args, ix + 1);
validateDN(args, ix + 2);
}
else if (UserAdmin.SET_ADMIN_COMMAND.equals(args[ix])) { // set/add repository admin
cmdLen = 3;
validateSID(args, ix + 1);
validateRepName(args, ix + 2, serverDir);
}
else if (LIST_COMMAND.equals(args[ix])) { // list repositories
cmdLen = 1;
queueCmd = false;
listRepositories = true;
}
else if (USERS_COMMAND.equals(args[ix])) { // list users (also affects listRepositories)
cmdLen = 1;
queueCmd = false;
listUsers = true;
}
else if (MIGRATE_ALL_COMMAND.equals(args[ix])) { // list repositories
cmdLen = 1;
queueCmd = false;
if (!migrationConfirmed && !confirmMigration()) {
migrationAbort = true;
}
migrationConfirmed = true;
if (!migrationAbort) {
RepositoryManager.markAllRepositoriesForIndexMigration(serverDir);
}
}
else if (MIGRATE_COMMAND.equals(args[ix])) { // list repositories
cmdLen = 2;
queueCmd = false;
if (ix == (args.length - 1)) {
System.err.println("Missing " + MIGRATE_COMMAND + " repository name argument");
}
else {
String repositoryName = args[ix + 1];
switch (args[ix]) {
case CommandProcessor.ADD_USER_COMMAND: // add user
cmdLen = 2;
validateSID(args, ix + 1);
if (hasOptionalArg(args, ix + 2, CommandProcessor.PASSWORD_OPTION)) {
++cmdLen;
pwdHash = promptForPasswordAndGetSaltedHash(args[ix + 1]);
}
break;
case CommandProcessor.REMOVE_USER_COMMAND: // remove user
cmdLen = 2;
validateSID(args, ix + 1);
break;
case CommandProcessor.RESET_USER_COMMAND: // reset user
cmdLen = 2;
validateSID(args, ix + 1);
if (hasOptionalArg(args, ix + 2, CommandProcessor.PASSWORD_OPTION)) {
++cmdLen;
pwdHash = promptForPasswordAndGetSaltedHash(args[ix + 1]);
}
break;
case CommandProcessor.SET_USER_DN_COMMAND: // set/add user with DN for PKI
cmdLen = 3;
validateSID(args, ix + 1);
validateDN(args, ix + 2);
break;
case CommandProcessor.GRANT_USER_COMMAND: // grant repository permission
cmdLen = 4;
validateSID(args, ix + 1);
validatePermission(args, ix + 2);
validateRepositoryName(args, ix + 3, serverRootDir);
break;
case CommandProcessor.REVOKE_USER_COMMAND: // revoke repository access
cmdLen = 3;
validateSID(args, ix + 1);
validateRepositoryName(args, ix + 2, serverRootDir);
break;
case LIST_COMMAND: // list repositories;
queueCmd = false;
listRepositories = true;
boolean hasUsernames = false;
while ((ix + 1) < args.length && !args[ix + 1].startsWith("-")) {
String sid = args[++ix]; // consume next arg as user sid
validateSID(sid);
listUsernameSet.add(sid);
hasUsernames = true;
}
if (!hasUsernames && (ix + 1) < args.length && "--users".equals(args[ix + 1])) {
listAllUserPermissions = true;
++ix;
}
break;
case USERS_COMMAND: // list users (also affects listRepositories)
queueCmd = false;
listUsers = true;
listUsernameSet.clear();
break;
case MIGRATE_ALL_COMMAND: // list repositories;
queueCmd = false;
if (!migrationConfirmed && !confirmMigration()) {
migrationAbort = true;
}
migrationConfirmed = true;
if (!migrationAbort) {
Repository.markRepositoryForIndexMigration(serverDir, repositoryName,
false);
RepositoryManager.markAllRepositoriesForIndexMigration(serverRootDir);
}
}
}
else {
displayUsage("Invalid usage!");
System.exit(-1);
break;
case MIGRATE_COMMAND: // list repositories
queueCmd = false;
if (ix == (args.length - 1)) {
System.err.println(
"Missing " + MIGRATE_COMMAND + " repository name argument");
}
else {
String repositoryName = args[ix + 1];
if (!migrationConfirmed && !confirmMigration()) {
migrationAbort = true;
}
migrationConfirmed = true;
if (!migrationAbort) {
Repository.markRepositoryForIndexMigration(serverRootDir,
repositoryName,
false);
}
}
break;
default:
displayUsage("Invalid usage!");
System.exit(-1);
}
if (queueCmd) {
addCommand(cmdList, args, ix, cmdLen, pwdHash);
}
}
try {
UserAdmin.writeCommands(cmdList, cmdDir);
if (cmdList.size() != 0) {
try {
CommandProcessor.writeCommands(cmdList, cmdDir);
}
catch (IOException e) {
System.err.println("Failed to queue commands: " + e.toString());
System.exit(-1);
}
System.out.println("Command queued.");
}
catch (IOException e) {
System.err.println("Failed to queue commands: " + e.toString());
System.exit(-1);
}
System.out.println(cmdList.size() + " command(s) queued.");
if (listUsers) {
UserManager.listUsers(serverDir);
UserManager.listUsers(serverRootDir);
}
if (listRepositories) {
RepositoryManager.listRepositories(serverDir, listUsers);
if (listUsernameSet.isEmpty()) {
RepositoryManager.listRepositories(serverRootDir, listAllUserPermissions);
}
else {
RepositoryManager.listRepositories(serverRootDir, listUsernameSet);
}
}
System.out.println();
}
@ -360,9 +386,9 @@ public class ServerAdmin implements GhidraLaunchable {
}
/**
* Validate properly formatted Distinguished Name
* Validate properly formatted Distinguished Name as command arg
* Example: 'CN=Doe John, OU=X, OU=Y, OU=DoD, O=U.S. Government, C=US'
* @param args
* @param args command args
* @param i argument index
*/
private void validateDN(String[] args, int i) {
@ -376,43 +402,58 @@ public class ServerAdmin implements GhidraLaunchable {
args[i] = "\"" + x500User.getName() + "\"";
}
catch (Exception e) {
Msg.error(UserAdmin.class, "Invalid DN: " + dn);
Msg.error(CommandProcessor.class, "Invalid DN: " + dn);
System.exit(-1);
}
}
/**
* Validate username/sid
* @param args
* @param args command args
* @param i argument index
*/
private void validateSID(String[] args, int i) {
if (args.length < (i + 1)) {
displayUsage("Invalid usage!");
displayUsage("Invalid usage, expected username/sid");
System.exit(-1);
}
String sid = args[i];
validateSID(args[i]);
}
private void validateSID(String sid) {
if (!UserManager.isValidUserName(sid)) {
Msg.error(UserAdmin.class, "Invalid username/sid: " + sid);
displayUsage("Invalid username/sid: " + sid);
System.exit(-1);
}
}
/**
* Validate repository name
* @param args
* Validate repository permission arg (repository name to follow)
* @param args command args
* @param i argument index
* @param rootDirFile base repository directory
*/
private void validateRepName(String[] args, int i, File rootDirFile) {
private void validatePermission(String[] args, int i) {
if (args.length < (i + 1) || CommandProcessor.parsePermission(args[i]) < 0) {
displayUsage("Invalid usage, expected grant permission +r, +w or +a");
System.exit(-1);
}
}
/**
* Validate existing repository name as command arg
* @param args command args
* @param i argument index
* @param rootDirFile base repositories directory
*/
private void validateRepositoryName(String[] args, int i, File rootDirFile) {
if (args.length < (i + 1)) {
displayUsage("Invalid usage!");
displayUsage("Invalid usage, expected repository name");
System.exit(-1);
}
String repName = args[i];
File f = new File(rootDirFile, NamingUtilities.mangle(repName));
if (!f.isDirectory()) {
Msg.error(UserAdmin.class, "Repository not found: " + repName);
Msg.error(CommandProcessor.class, "Repository not found: " + repName);
System.exit(-1);
}
}
@ -483,7 +524,7 @@ public class ServerAdmin implements GhidraLaunchable {
/**
* Display an optional message followed by usage syntax.
* @param msg
* @param msg optional error message to proceed usage display
*/
private void displayUsage(String msg) {
if (msg != null) {
@ -496,21 +537,27 @@ public class ServerAdmin implements GhidraLaunchable {
System.err.println("\nSupported commands:");
System.err.println(" -add <sid> [--p]");
System.err.println(
" Add a new user to the server identified by their sid identifier [--p prompt for password]");
" Add a new user to the server identified by their sid identifier [optional --p prompts for password]");
System.err.println(" -grant <sid> [+r|+w|+a] <repository-name>");
System.err.println(
" Grant access permission for a user, identified by sid, to the named repository");
System.err.println(" -revoke <sid> <repository-name>");
System.err.println(
" Revoke access for a user, identified by sid, to a named repository");
System.err.println(" -remove <sid>");
System.err.println(" Remove the specified user from the server's user list");
System.err.println(
" Remove the specified user from the server's user list and revoke all repository access");
System.err.println(" -reset <sid> [--p]");
System.err.println(
" Reset the specified user's server login password [--p prompt for password]");
" Reset the specified user's server login password [optional --p prompts for password]");
System.err.println(" -dn <sid> \"<dname>\"");
System.err.println(
" When PKI authentication is used, add the specified X500 Distinguished Name for a user");
System.err.println(" -admin <sid> \"<repository-name>\"");
System.err.println(
" Grant ADMIN privilege to the specified user with the specified repository");
System.err.println(" -list [-users]");
System.err.println(" -list [--users]");
System.err.println(
" Output list of repositories to the console (user access list will be included with -users)");
System.err.println(" -list <sid> [<sid>...]");
System.err.println(" Output list of repository permissions for each user specified");
System.err.println(" -users");
System.err.println(" Output list of users to console which have server access");
System.err.println(" -migrate \"<repository-name>\"");

View file

@ -1,264 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.server;
import java.io.*;
import java.util.*;
import javax.security.auth.x500.X500Principal;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import ghidra.framework.store.local.LocalFileSystem;
import ghidra.util.exception.DuplicateNameException;
import utilities.util.FileUtilities;
/**
* <code>UserAdmin</code> is an Application for generating administrative
* commands to be processed by the UserManager. Static methods are also
* provided which enable the UserManager to process such commands.
*/
public class UserAdmin {
static final Logger log = LogManager.getLogger(UserAdmin.class);
// Queued commands
static final String ADD_USER_COMMAND = "-add";
static final String REMOVE_USER_COMMAND = "-remove";
static final String RESET_USER_COMMAND = "-reset";
static final String SET_USER_DN_COMMAND = "-dn";
static final String SET_ADMIN_COMMAND = "-admin";
static final String PASSWORD_OPTION = "--p"; // applies to add and reset commands
static final String ADMIN_CMD_DIR = LocalFileSystem.HIDDEN_DIR_PREFIX + "admin";
static final String COMMAND_FILE_EXT = ".cmd";
/**
* Command file filter
*/
static final FileFilter CMD_FILE_FILTER =
f -> f.isFile() && f.getName().endsWith(COMMAND_FILE_EXT);
/**
* File date comparator
*/
static final Comparator<File> FILE_DATE_COMPARATOR = (f1, f2) -> {
long t1 = f1.lastModified();
long t2 = f2.lastModified();
long diff = t1 - t2;
if (diff == 0) {
return 0;
}
return diff < 0 ? -1 : 1;
};
private UserAdmin() {
}
/**
* Split a command string into individual arguments.
* @param cmd command string
* @return array of command arguments
*/
private static String[] splitCommand(String cmd) {
ArrayList<String> argList = new ArrayList<>();
int startIx = 0;
int endIx = 0;
int len = cmd.length();
boolean insideQuote = false;
while (endIx < len) {
char c = cmd.charAt(endIx);
if (!insideQuote && startIx == endIx) {
if (c == ' ' || c == '\"') {
insideQuote = (c == '\"');
startIx = ++endIx;
continue;
}
}
if (c == (insideQuote ? '\"' : ' ')) {
argList.add(cmd.substring(startIx, endIx));
startIx = ++endIx;
insideQuote = false;
}
else {
++endIx;
}
}
if (startIx != endIx) {
argList.add(cmd.substring(startIx, endIx));
}
String[] args = new String[argList.size()];
argList.toArray(args);
return args;
}
/**
* Process the specified command.
* @param repositoryMgr server's repository manager
* @param cmd command string
* @throws IOException
*/
private static void processCommand(RepositoryManager repositoryMgr, String cmd)
throws IOException {
UserManager userMgr = repositoryMgr.getUserManager();
String[] args = splitCommand(cmd);
if (ADD_USER_COMMAND.equals(args[0])) { // add user
String sid = args[1];
char[] pwdHash = null;
if (args.length == 4 && args[2].contentEquals(PASSWORD_OPTION)) {
pwdHash = args[3].toCharArray();
}
try {
userMgr.addUser(sid, pwdHash);
log.info("User '" + sid + "' added");
}
catch (DuplicateNameException e) {
log.error("Add User Failed: user '" + sid + "' already exists");
}
}
else if (REMOVE_USER_COMMAND.equals(args[0])) { // remove user
String sid = args[1];
userMgr.removeUser(sid);
log.info("User '" + sid + "' removed");
}
else if (RESET_USER_COMMAND.equals(args[0])) { // reset user
String sid = args[1];
char[] pwdHash = null;
if (args.length == 4 && args[2].contentEquals(PASSWORD_OPTION)) {
pwdHash = args[3].toCharArray();
}
if (!userMgr.resetPassword(sid, pwdHash)) {
log.info("Failed to reset password for user '" + sid + "'");
}
else if (pwdHash != null) {
log.info("User '" + sid + "' password reset to specified password");
}
else {
log.info("User '" + sid + "' password reset to default password");
}
}
else if (SET_USER_DN_COMMAND.equals(args[0])) { // set/add user with DN for PKI
String sid = args[1];
X500Principal x500User = new X500Principal(args[2]);
if (userMgr.isValidUser(sid)) {
userMgr.setDistinguishedName(sid, x500User);
log.info("User '" + sid + "' DN set (" + x500User.getName() + ")");
}
else {
try {
userMgr.addUser(sid, x500User);
log.info("User '" + sid + "' added with DN (" + x500User.getName() +
") and default password");
}
catch (DuplicateNameException e) {
// should never occur
}
}
}
else if (SET_ADMIN_COMMAND.equals(args[0])) { // set/add repository admin
String sid = args[1];
String repName = args[2];
if (!userMgr.isValidUser(sid)) {
try {
userMgr.addUser(sid);
log.info("User '" + sid + "' added");
}
catch (DuplicateNameException e) {
return; // should never occur
}
}
Repository rep = repositoryMgr.getRepository(repName);
if (rep == null) {
log.error("Failed to add '" + sid + "' as admin, repository '" + repName +
"' not found.");
}
else {
rep.addAdmin(sid);
}
}
}
/**
* Process all queued commands for the specified server.
* @param repositoryMgr server's repository manager
* @param serverDir Ghidra server directory
* @throws IOException
*/
static void processCommands(RepositoryManager repositoryMgr) throws IOException {
File cmdDir = new File(repositoryMgr.getRootDir(), ADMIN_CMD_DIR);
if (!cmdDir.exists()) {
// ensure process owner creates queued command directory
cmdDir.mkdir();
return;
}
File[] files = cmdDir.listFiles(CMD_FILE_FILTER);
if (files == null) {
log.error("Failed to access command queue " + cmdDir.getAbsolutePath() +
": possible permission problem");
return;
}
Arrays.sort(files, FILE_DATE_COMPARATOR);
if (files.length == 0) {
return;
}
log.info("Processing " + files.length + " queued commands");
for (File file : files) {
List<String> cmdList = FileUtilities.getLines(file);
for (String cmdStr : cmdList) {
if (cmdStr.isBlank()) {
continue;
}
processCommand(repositoryMgr, cmdStr.trim());
}
file.delete();
}
}
/**
* Store a list of command strings to a new command file.
* @param cmdList list of command strings
* @param cmdDir command file directory
* @throws IOException
*/
static void writeCommands(ArrayList<String> cmdList, File cmdDir) throws IOException {
File cmdFile = File.createTempFile("adm", ".tmp", cmdDir);
String cmdFilename = cmdFile.getName();
cmdFilename = cmdFilename.substring(0, cmdFilename.length() - 4) + COMMAND_FILE_EXT;
PrintWriter pw = new PrintWriter(new BufferedOutputStream(new FileOutputStream(cmdFile)));
boolean success = false;
try {
Iterator<String> it = cmdList.iterator();
while (it.hasNext()) {
String cmd = it.next();
pw.println(cmd);
}
pw.close();
if (!cmdFile.renameTo(new File(cmdFile.getParentFile(), cmdFilename))) {
throw new IOException("file error");
}
success = true;
}
finally {
if (!success) {
cmdFile.delete();
}
}
}
}

View file

@ -67,11 +67,10 @@ public class UserManager {
private LinkedHashMap<String, UserEntry> userList = new LinkedHashMap<>();
private HashMap<X500Principal, UserEntry> dnLookupMap = new HashMap<>();
private long lastUserListChange;
private boolean userListUpdateInProgress = false;
/**
* Construct server user manager
* @param repositoryMgr repository manager (used for queued command processing)
* @param repositoryMgr repository manager
* @param enableLocalPasswords if true user passwords will be maintained
* within local 'users' file
* @param defaultPasswordExpirationDays password expiration in days when
@ -90,8 +89,8 @@ public class UserManager {
userFile = new File(repositoryMgr.getRootDir(), USER_PASSWORD_FILE);
try {
// everything must be constructed before processing commands
updateUserList(false);
readUserListIfNeeded();
clearExpiredPasswords();
log.info("User file contains " + userList.size() + " entries");
}
catch (FileNotFoundException e) {
@ -145,14 +144,14 @@ public class UserManager {
/**
* Get the SSH public key file for the specified user
* if it exists.
* @param user
* @param username user name/SID
* @return SSH public key file or null if key unavailable
*/
public File getSSHPubKeyFile(String user) {
if (!userList.containsKey(user)) {
public File getSSHPubKeyFile(String username) {
if (!userList.containsKey(username)) {
return null;
}
File f = new File(sshDir, user + SSH_PUBKEY_EXT);
File f = new File(sshDir, username + SSH_PUBKEY_EXT);
if (f.isFile()) {
return f;
}
@ -163,29 +162,31 @@ public class UserManager {
* Add a user.
* @param username user name/SID
* @param passwordHash MD5 hash of initial password or null if explicit password reset required
* @param dn X500 distinguished name for user (may be null)
* @param x500User X500 user name (may be null)
* @throws DuplicateNameException if username already exists
* @throws IOException if IO error occurs
*/
private synchronized void addUser(String username, char[] passwordHash, X500Principal x500User)
private void addUser(String username, char[] passwordHash, X500Principal x500User)
throws DuplicateNameException, IOException {
if (username == null) {
throw new IllegalArgumentException();
}
updateUserList(true);
if (userList.containsKey(username)) {
throw new DuplicateNameException("User " + username + " already exists");
synchronized (repositoryMgr) {
if (userList.containsKey(username)) {
throw new DuplicateNameException("User " + username + " already exists");
}
UserEntry entry = new UserEntry();
entry.username = username;
entry.passwordHash = passwordHash;
entry.passwordTime = (new Date()).getTime();
entry.x500User = x500User;
userList.put(username, entry);
if (x500User != null) {
dnLookupMap.put(x500User, entry);
}
writeUserList();
log.info("User '" + username + "' added");
}
UserEntry entry = new UserEntry();
entry.username = username;
entry.passwordHash = passwordHash;
entry.passwordTime = (new Date()).getTime();
entry.x500User = x500User;
userList.put(username, entry);
if (x500User != null) {
dnLookupMap.put(x500User, entry);
}
writeUserList();
}
/**
@ -230,15 +231,15 @@ public class UserManager {
* Returns the X500 distinguished name for the specified user.
* @param username user name/SID
* @return X500 distinguished name
* @throws IOException
*/
public synchronized X500Principal getDistinguishedName(String username) throws IOException {
updateUserList(true);
UserEntry entry = userList.get(username);
if (entry != null) {
return entry.x500User;
public X500Principal getDistinguishedName(String username) {
synchronized (repositoryMgr) {
UserEntry entry = userList.get(username);
if (entry != null) {
return entry.x500User;
}
return null;
}
return null;
}
/**
@ -246,11 +247,11 @@ public class UserManager {
* @param x500User a user's X500 distinguished name
* @return username or null if not found
*/
public synchronized String getUserByDistinguishedName(X500Principal x500User)
throws IOException {
updateUserList(true);
UserEntry entry = dnLookupMap.get(x500User);
return entry != null ? entry.username : null;
public String getUserByDistinguishedName(X500Principal x500User) {
synchronized (repositoryMgr) {
UserEntry entry = dnLookupMap.get(x500User);
return entry != null ? entry.username : null;
}
}
/**
@ -258,28 +259,29 @@ public class UserManager {
* @param username user name/SID
* @param x500User X500 distinguished name
* @return true if successful, false if user not found
* @throws IOException
* @throws IOException if error occurs while updating user file
*/
public synchronized boolean setDistinguishedName(String username, X500Principal x500User)
public boolean setDistinguishedName(String username, X500Principal x500User)
throws IOException {
updateUserList(true);
UserEntry oldEntry = userList.remove(username);
if (oldEntry != null) {
if (oldEntry.x500User != null) {
dnLookupMap.remove(oldEntry.x500User);
synchronized (repositoryMgr) {
UserEntry oldEntry = userList.remove(username);
if (oldEntry != null) {
if (oldEntry.x500User != null) {
dnLookupMap.remove(oldEntry.x500User);
}
UserEntry entry = new UserEntry();
entry.username = username;
entry.passwordHash = oldEntry.passwordHash;
entry.x500User = x500User;
userList.put(username, entry);
if (x500User != null) {
dnLookupMap.put(x500User, entry);
}
writeUserList();
return true;
}
UserEntry entry = new UserEntry();
entry.username = username;
entry.passwordHash = oldEntry.passwordHash;
entry.x500User = x500User;
userList.put(username, entry);
if (x500User != null) {
dnLookupMap.put(x500User, entry);
}
writeUserList();
return true;
return false;
}
return false;
}
private void checkValidPasswordHash(char[] saltedPasswordHash) throws IOException {
@ -332,9 +334,9 @@ public class UserManager {
* @param saltedSHA256PasswordHash 4-character salt followed by 64-hex digit SHA256 password hash for new password
* @param isTemporary if true password will be set to expire
* @return true if successful, false if user not found
* @throws IOException
* @throws IOException if error occurs while updating user file
*/
public synchronized boolean setPassword(String username, char[] saltedSHA256PasswordHash,
public boolean setPassword(String username, char[] saltedSHA256PasswordHash,
boolean isTemporary) throws IOException {
if (!enableLocalPasswords) {
throw new IOException("Local passwords are not used");
@ -342,31 +344,36 @@ public class UserManager {
checkValidPasswordHash(saltedSHA256PasswordHash);
updateUserList(true);
UserEntry oldEntry = userList.remove(username);
if (oldEntry != null) {
UserEntry entry = new UserEntry();
entry.username = username;
entry.passwordHash = saltedSHA256PasswordHash;
entry.passwordTime = isTemporary ? (new Date()).getTime() : NO_EXPIRATION;
entry.x500User = oldEntry.x500User;
userList.put(username, entry);
if (entry.x500User != null) {
dnLookupMap.put(entry.x500User, entry);
synchronized (repositoryMgr) {
UserEntry oldEntry = userList.remove(username);
if (oldEntry != null) {
UserEntry entry = new UserEntry();
entry.username = username;
entry.passwordHash = saltedSHA256PasswordHash;
entry.passwordTime = isTemporary ? (new Date()).getTime() : NO_EXPIRATION;
entry.x500User = oldEntry.x500User;
userList.put(username, entry);
if (entry.x500User != null) {
dnLookupMap.put(entry.x500User, entry);
}
writeUserList();
return true;
}
writeUserList();
return true;
return false;
}
return false;
}
/**
* Returns true if local passwords are in use and can be changed by the user.
* @see #setPassword(String, char[])
* See {@link #setPassword(String, char[], boolean)}.
* @param username user name/SID
* @return true if password change permitted, else false
*/
public boolean canSetPassword(String username) {
UserEntry userEntry = userList.get(username);
return (enableLocalPasswords && userEntry != null && userEntry.passwordHash != null);
synchronized (repositoryMgr) {
UserEntry userEntry = userList.get(username);
return (enableLocalPasswords && userEntry != null && userEntry.passwordHash != null);
}
}
/**
@ -375,18 +382,17 @@ public class UserManager {
* @param username user name
* @return time until expiration or -1 if it will not expire
*/
public long getPasswordExpiration(String username) throws IOException {
updateUserList(true);
public long getPasswordExpiration(String username) {
synchronized (repositoryMgr) {
UserEntry userEntry = userList.get(username);
UserEntry userEntry = userList.get(username);
// indicate immediate expiration for users with short hash (non salted SHA-256)
if (userEntry != null && userEntry.passwordHash != null &&
userEntry.passwordHash.length != HashUtilities.SHA256_SALTED_HASH_LENGTH) {
return 0;
// indicate immediate expiration for users with short hash (non salted SHA-256)
if (userEntry != null && userEntry.passwordHash != null &&
userEntry.passwordHash.length != HashUtilities.SHA256_SALTED_HASH_LENGTH) {
return 0;
}
return getPasswordExpiration(userEntry);
}
return getPasswordExpiration(userEntry);
}
/**
@ -415,10 +421,10 @@ public class UserManager {
/**
* Reset the local password to the 'changeme' for the specified user.
* @param username
* @param username user name/SID
* @param saltedPasswordHash optional user password hash (may be null)
* @return true if password updated successfully.
* @throws IOException
* @return true if password updated successfully, else false if local passwords are not used.
* @throws IOException if error occurs while updating user file
*/
public boolean resetPassword(String username, char[] saltedPasswordHash) throws IOException {
if (!enableLocalPasswords) {
@ -435,62 +441,46 @@ public class UserManager {
/**
* Remove the specified user from the server access list
* @param username user name/SID
* @throws IOException
* @return true if existing user removed, else false if not found
* @throws IOException if error occurs while updating user file
*/
public synchronized void removeUser(String username) throws IOException {
updateUserList(true);
UserEntry oldEntry = userList.remove(username);
if (oldEntry != null) {
if (oldEntry.x500User != null) {
dnLookupMap.remove(oldEntry.x500User);
public boolean removeUser(String username) throws IOException {
synchronized (repositoryMgr) {
UserEntry oldEntry = userList.remove(username);
if (oldEntry != null) {
if (oldEntry.x500User != null) {
dnLookupMap.remove(oldEntry.x500User);
}
writeUserList();
repositoryMgr.userRemoved(username);
log.info("User removed from server: " + username);
return true;
}
writeUserList();
return false;
}
}
/**
* Get list of all users known to server.
* @return list of known users
* @throws IOException
*/
public synchronized String[] getUsers() throws IOException {
updateUserList(true);
String[] names = new String[userList.size()];
Iterator<String> iter = userList.keySet().iterator();
int i = 0;
while (iter.hasNext()) {
names[i++] = iter.next();
}
return names;
}
/**
* Refresh the server's user list and process any pending UserAdmin commands.
* @param processCmds TODO
* @throws IOException
*/
synchronized void updateUserList(boolean processCmds) throws IOException {
if (userListUpdateInProgress) {
return;
}
userListUpdateInProgress = true;
try {
readUserListIfNeeded();
clearExpiredPasswords();
if (processCmds) {
UserAdmin.processCommands(repositoryMgr);
public String[] getUsers() {
synchronized (repositoryMgr) {
String[] names = new String[userList.size()];
Iterator<String> iter = userList.keySet().iterator();
int i = 0;
while (iter.hasNext()) {
names[i++] = iter.next();
}
}
finally {
userListUpdateInProgress = false;
return names;
}
}
/**
* Clear all local user passwords which have expired.
* @throws IOException
* @throws IOException if error occurs while updating user file
*/
private void clearExpiredPasswords() throws IOException {
void clearExpiredPasswords() throws IOException {
if (defaultPasswordExpirationMS == 0) {
return;
}
@ -498,7 +488,8 @@ public class UserManager {
Iterator<UserEntry> it = userList.values().iterator();
while (it.hasNext()) {
UserEntry entry = it.next();
if (enableLocalPasswords && getPasswordExpiration(entry) == 0) {
if (entry.passwordHash != null && enableLocalPasswords &&
getPasswordExpiration(entry) == 0) {
entry.passwordHash = null;
entry.passwordTime = 0;
dataChanged = true;
@ -513,9 +504,9 @@ public class UserManager {
/**
* Read user data from file if the timestamp on the file has changed.
*
* @throws IOException
* @throws IOException if error occurs while updating user file
*/
private void readUserListIfNeeded() throws IOException {
void readUserListIfNeeded() throws IOException {
long lastMod = userFile.lastModified();
if (lastUserListChange == lastMod) {
@ -627,7 +618,7 @@ public class UserManager {
/**
* Write user data to file.
* @throws IOException
* @throws IOException if error occurs while updating user file
*/
private void writeUserList() throws IOException {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(userFile))) {
@ -660,16 +651,12 @@ public class UserManager {
/**
* Returns true if the specified user is known to server.
* @param username user name/SID
* @return
* @return true if user is known to server
*/
public synchronized boolean isValidUser(String username) {
try {
updateUserList(true);
public boolean isValidUser(String username) {
synchronized (repositoryMgr) {
return userList.containsKey(username);
}
catch (IOException e) {
// ignore
}
return userList.containsKey(username);
}
/**
@ -677,50 +664,54 @@ public class UserManager {
* password set for the specified user.
* @param username user name/SID
* @param password password data
* @throws IOException
* @throws IOException if error occurs while updating user file
* @throws FailedLoginException if authentication fails
*/
public synchronized void authenticateUser(String username, char[] password)
public void authenticateUser(String username, char[] password)
throws IOException, FailedLoginException {
if (username == null || password == null) {
throw new FailedLoginException("Invalid authentication data");
}
updateUserList(true);
UserEntry entry = userList.get(username);
if (entry == null) {
throw new FailedLoginException("Unknown user: " + username);
}
if (entry.passwordHash == null ||
entry.passwordHash.length < HashUtilities.MD5_UNSALTED_HASH_LENGTH) {
throw new FailedLoginException("User password not set, must be reset");
}
// Support deprecated unsalted hash
if (entry.passwordHash.length == HashUtilities.MD5_UNSALTED_HASH_LENGTH && Arrays.equals(
HashUtilities.getHash(HashUtilities.MD5_ALGORITHM, password), entry.passwordHash)) {
return;
}
char[] salt = new char[HashUtilities.SALT_LENGTH];
System.arraycopy(entry.passwordHash, 0, salt, 0, HashUtilities.SALT_LENGTH);
if (entry.passwordHash.length == HashUtilities.MD5_SALTED_HASH_LENGTH) {
if (!Arrays.equals(
HashUtilities.getSaltedHash(HashUtilities.MD5_ALGORITHM, salt, password),
entry.passwordHash)) {
throw new FailedLoginException("Incorrect password");
synchronized (repositoryMgr) {
clearExpiredPasswords();
UserEntry entry = userList.get(username);
if (entry == null) {
throw new FailedLoginException("Unknown user: " + username);
}
}
else if (entry.passwordHash.length == HashUtilities.SHA256_SALTED_HASH_LENGTH) {
if (!Arrays.equals(
HashUtilities.getSaltedHash(HashUtilities.SHA256_ALGORITHM, salt, password),
entry.passwordHash)) {
throw new FailedLoginException("Incorrect password");
if (entry.passwordHash == null ||
entry.passwordHash.length < HashUtilities.MD5_UNSALTED_HASH_LENGTH) {
throw new FailedLoginException("User password not set, must be reset");
}
// Support deprecated unsalted hash
if (entry.passwordHash.length == HashUtilities.MD5_UNSALTED_HASH_LENGTH &&
Arrays.equals(
HashUtilities.getHash(HashUtilities.MD5_ALGORITHM, password),
entry.passwordHash)) {
return;
}
char[] salt = new char[HashUtilities.SALT_LENGTH];
System.arraycopy(entry.passwordHash, 0, salt, 0, HashUtilities.SALT_LENGTH);
if (entry.passwordHash.length == HashUtilities.MD5_SALTED_HASH_LENGTH) {
if (!Arrays.equals(
HashUtilities.getSaltedHash(HashUtilities.MD5_ALGORITHM, salt, password),
entry.passwordHash)) {
throw new FailedLoginException("Incorrect password");
}
}
else if (entry.passwordHash.length == HashUtilities.SHA256_SALTED_HASH_LENGTH) {
if (!Arrays.equals(
HashUtilities.getSaltedHash(HashUtilities.SHA256_ALGORITHM, salt, password),
entry.passwordHash)) {
throw new FailedLoginException("Incorrect password");
}
}
else {
throw new FailedLoginException("User password not set, must be reset");
}
}
else {
throw new FailedLoginException("User password not set, must be reset");
}
}
@ -760,7 +751,8 @@ public class UserManager {
/*
* Regex: matches if the entire string is alpha, digit, ".", "-", "_", fwd or back slash.
*/
private static final Pattern VALID_USERNAME_REGEX = Pattern.compile("[a-zA-Z0-9.\\-_/\\\\]+");
private static final Pattern VALID_USERNAME_REGEX =
Pattern.compile("[a-zA-Z][a-zA-Z0-9.\\-_/\\\\]*");
/**
* Ensures a name only contains valid characters and meets length limitations.

View file

@ -30,6 +30,7 @@ typewriter {
<LI><a href="#introduction">Introduction</a></LI>
<LI><a href="#javaRuntime">Java Runtime Environment</a></LI>
<LI><a href="#serverConfig">Server Configuration</a></LI>
<LI><a href="#serverLogs">Server Logs</a></LI>
<LI><a href="#serverMemory">Server Memory Considerations</a></LI>
<LI><a href="#dnsNote">Note regarding use of DNS (name lookup service)</a></LI>
<LI><a href="#userAuthentication">User Authentication</a></LI>
@ -42,7 +43,7 @@ typewriter {
<LI><a href="#windows_install">Install as Automatic Service</a></LI>
<LI><a href="#windows_uninstall">Uninstall Service</a></LI>
</UL>
<LI><a href="#running_linux_mac">Running Ghidra Server on Linux or Mac-OSX</a></LI>
<LI><a href="#running_linux_mac">Running Ghidra Server on Linux or Mac OS</a></LI>
<UL>
<LI><a href="#linux_mac_scripts">Server Scripts</a></LI>
<LI><a href="#linux_mac_console">Running Server in Console Window</a></LI>
@ -55,7 +56,7 @@ typewriter {
<LI><a href="#pkiCertificates">PKI Certificates</a></LI>
<LI><a href="#pkiCertificateAuthorities">Managing PKI Certificate Authorities</a></LI>
<LI><a href="#upgradeServer">Upgrading the Ghidra Server Installation</a></LI>
<LI><a href="#troubleshooting">Troubleshooting</a></LI>
<LI><a href="#troubleshooting">Troubleshooting / Known Issues</a></LI>
<UL>
<LI><a href="#checkinFailures">Failures Creating Repository Folders / Checking in Files</a></LI>
<LI><a href="#connectErrors">Client/Server connection errors</a></LI>
@ -64,6 +65,7 @@ typewriter {
or svrUninstall.bat Error</a></LI>
<LI><a href="#selinuxDisabled">Linux - SELinux must be disabled</a></LI>
<LI><a href="#randomHang">Linux - Potential hang from /dev/random depletion</a></LI>
<LI><a href="#macDiskAccess">Mac OS - Service fails to start (macOS 10.14 Mojave and later)</a></LI>
</UL>
</UL>
@ -149,6 +151,16 @@ new installation. Using a non-default repositories directory outside your Ghidr
will simplify the migration process.
</P>
(<a href="#top">Back to Top</a>)
<div style="border-top: 4px double; margin-top: 1em; padding-top: 1em;"> </div>
<h2><a name="serverLog">Server Logs</a></h2>
<P>The Ghidra Server produces two log files, which for the most part have the same content.
The service <i>wrapper.log</i> file generally resides within the Ghidra installation root
directory, while the <i>server.log</i> file resides within the configured <i>repositories</i>
directory. When running the server in console mode all <i>wrapper.log</i> output is directed
to the console.
(<a href="#top">Back to Top</a>)
<div style="border-top: 4px double; margin-top: 1em; padding-top: 1em;"> </div>
<h2><a name="serverMemory">Server Memory Considerations</a></h2>
@ -490,7 +502,10 @@ are not currently supported.
(<a href="#top">Back to Top</a>)
<div style="border-top: 4px double; margin-top: 1em; padding-top: 1em;"> </div>
<h2><a name="running_linux_mac">Running Ghidra Server on Linux or Mac-OSX</a></h2>
<h2><a name="running_linux_mac">Running Ghidra Server on Linux or Mac OS</a></h2>
<B>NOTE:</B> Mac OS has limited support. The latest supported version is macOS 10.13.x High Sierra
(see <a href="#macDiskAccess">Mac OS - Service fails to start</a>).</u>
<a name="linux_mac_scripts"><h3><u>Server Scripts (located within the server subdirectory)</u></h3></a>
@ -584,7 +599,15 @@ to run as <i>root</i> and monitor/manage the Java process.
<P>
The script <typewriter>svrAdmin</typewriter>, or <typewriter>svrAdmin.bat</typewriter>, provides
the ability to manage Ghidra Server users and repositories. This script must be run from a
command shell so that the proper command line arguments may be specified.
command shell so that the proper command line arguments may be specified. This command
should only be used after the corresponding Ghidra installation has been properly
configured via modification of the <typewriter>server/server.conf</typewriter> file
(see <a href="#serverConfig">Server Configuration</a>) and installed and/or started.
</P><P>
Many of the commands are queued for subsequent execution by the Ghidra Server process.
Due to this queing, there may be a delay between the invocation of a <typewriter>svrAdmin</typewriter>
command and its desired affect. The Ghidra log file(s) may be examined for feedback on
queued command execution (see <a href="#serverLogs">Server Logs</a>).
</P>
<P>
@ -592,12 +615,14 @@ to run as <i>root</i> and monitor/manage the Java process.
<PRE>
svrAdmin [&lt;server-root-path&gt;]
[-add &lt;user_sid&gt; [--p]]
[-add &lt;user_sid&gt; [--p]]
[-grant &lt;user_sid&gt; &lt;"+r"|"+w"|"+a"&gt; &lt;repository_name&gt;]
[-revoke &lt;user_sid&gt; &lt;repository_name&gt;]
[-remove &lt;user_sid&gt;]
[-reset &lt;user_sid&gt; [--p]]
[-dn &lt;user_sid&gt; &quot;&lt;user_dn&gt;&quot;]
[-admin &lt;user_sid&gt; &quot;&lt;repository_name&gt;&quot;]
[-list]
[-list &lt;user_sid&gt; [&lt;user_sid&gt;...]]
[-list [--users]]
[-users]
[-migrate-all]
[-migrate &quot;&lt;repository_name&gt;&quot;]
@ -626,11 +651,29 @@ to run as <i>root</i> and monitor/manage the Java process.
svrAdmin -add mySID --p
</PRE>
</LI>
<LI><typewriter>-grant</typewriter>&nbsp;&nbsp;<b>(Grant Repository Access for User)</b><br>
Grant access for a specified user and repository where both must be known to the server.
Repository access permission must be specified as +r for READ_ONLY, +w for WRITE or +a for ADMIN.
Examples:
<PRE>
svrAdmin -grant mySID +a myRepo
svrAdmin -grant mySID +w myRepo
</PRE>
</LI>
<LI><typewriter>-revoke</typewriter>&nbsp;&nbsp;<b>(Revoke Repository Access for User)</b><br>
Revoke the access for a specified user and named repository. Currently, revoking access for a
user does not disconnect them if currently connected.
Examples:
<PRE>
svrAdmin -revoke mySID myRepo
</PRE>
</LI>
<LI><typewriter>-remove</typewriter>&nbsp;&nbsp;<b>(Removing a User)</b><br>
A user may be removed from the server with this command form. This will only prevent the
specified user from connecting to the server and will have no effect on the state or history
A user may be removed from the Ghidra Server and all repositories with this command form. This will only prevent the
specified user from connecting to the server in the future and will have no effect on the state or history
of repository files. If a repository admin wishes to clear a user&apos;s checkouts, this is
a separate task which may be performed from an admin&apos;s Ghidra client.
a separate task which may be performed from an admin&apos;s Ghidra client. Currently, removing a
user does not disconnect them if currently connected.
<br><br>
Example:
<PRE>
@ -661,26 +704,19 @@ to run as <i>root</i> and monitor/manage the Java process.
<typewriter>UnknownDN.log</typewriter> file following an attempted connection with their PKCS
certificate.
</LI>
<br>
<LI><typewriter>-admin</typewriter>&nbsp;&nbsp;<b>(Adding a Repository Administrator)</b><br>
If an existing repository administrator is unable to add another user as administrator, the
server administrator may use this command to specify a new repository administrator.
<br><br>
Example:
<PRE>
svrAdmin -admin mySID "myProject"
</PRE>
</LI>
<LI><typewriter>-list</typewriter>&nbsp;&nbsp;<b>(List All Repositories)</b><br>
Lists all repositories. If the <i>-users</i> option is also present, the user access
list will be included for each repository.
<LI><typewriter>-list</typewriter>&nbsp;&nbsp;<b>(List All Repositories and/or User Permissions)</b><br>
If the <i>--users</i> option is also present, the complete user access
list will be included for each repository. Otherwise, command may be followed by one or user SIDs (separated by a space)
which will limit the displayed repository list and access permissions to those users specified.
<br><br>
Example:
<PRE>
svrAdmin -list
svrAdmin -list --users
svrAdmin -list mySID
</PRE>
<LI><typewriter>-users</typewriter>&nbsp;&nbsp;<b>(List All Users)</b><br>
Lists all users with server access. May also be coupled with the <i>-list</i> option.
Lists all users with server access.
<br><br>
Example:
<PRE>
@ -894,7 +930,7 @@ Please note that the Ghidra Server does not currently support Certificate Revoca
<br>
<LI>Uninstall an installed Ghidra Server Service by following the <typewriter>Uninstall Service</typewriter>
instructions corresponding to your operating system (<a href="#windows_uninstall">Windows</a>
or <a href="#linux_mac_uninstall">Linux/Mac-OSX</a>).</LI>
or <a href="#linux_mac_uninstall">Linux/Mac OS</a>).</LI>
<br>
<LI>Unzip the new Ghidra distribution to a new installation directory (general unpacking and installation
guidelines may be found in <typewriter>ghidra_<I>x.x</I>/docs/InstallationGuide.html</typewriter>).</LI>
@ -953,7 +989,7 @@ backup of your project or server repositories directory is highly recommended be
(<a href="#top">Back to Top</a>)
<div style="border-top: 4px double; margin-top: 1em; padding-top: 1em;"> </div>
<h2><a name="troubleshooting">Troubleshooting</a></h2>
<h2><a name="troubleshooting">Troubleshooting / Known Issues</a></h2>
<a name="checkinFailures"><h3><u>Failures Creating Repository Folders / Checking in Files</u></h3></a>
<P>
@ -1028,7 +1064,7 @@ Expansion Daemon) which will satisfy the entropy demand needed by /dev/random.
</P>
<br>
<a name="macDiskAccess"><h3><u>Mac OS - Service fails to start</u></h3></a>
<a name="macDiskAccess"><h3><u>Mac OS - Service fails to start (macOS 10.14 Mojave and later)</u></h3></a>
<P>
The installed service may fail to start with Mac OS Majave (10.14) and later due
to changes in the Mac OS system protection feature. When the service fails to start it does not