Writing Priv Commands


Introduction and Background

As described by the Rhino Architecture document, all communication between the client GUI and the server is handled on the server end by a daemon called sysadmd. This daemon runs as the user who logged into the GUI, not necessarily as root. When a user wants to perform some administrative function that requires root access, a command must be run on the server as root. There are several components of the Rhino architecture which support this:
priv command
Also known as privileged command. A priv command is a command line program that requires root level access to run successfully. These commands reside in the /usr/sysadm/privbin directory, and are the commands that actually perform the changes to the system when the user runs a Task from the GUI. The priv commands are not setuid root, but must usually be run as root to be effective, since they perform operations that will fail if they are not running as root. There are two ways to run priv commands: via the runpriv command, or the root user can invoke them directly.
runpriv
A setuid program that takes the name of a priv command as an argument. It allows a non-root user to run a priv command as root if any of the following are true:
  1. The user is running as root.
  2. There is no root password on the system.
  3. There is an file in the defaultPrivileges(4) directory granting the privilege to all users.
  4. There is an entry in the privilegedUsers(4) database granting the user all privileges.
  5. There is an entry in the privilege(4) database granting the user the requested privilege, and the user is not an NIS user.
  6. The -auth auth-scheme arguments are provided, and the user passes the authentication test. If auth-scheme is unix, then the user must type the root password when prompted in order to pass.
Privilege Broker Service
One of the services provided by sysadmd. It allows the GUI to pass a request to run a priv command to the server. The Privilege Broker Service currently always uses the "unix" style of authentication. The GUI can use the runPriv family of Java methods (See the Task documentation for more info) to pass commands to the priv broker service.
The Privilege Broker Service is currently the only method for the GUI to run a command on the server. For some products, it may be desirable to have a more general way to run arbitrary commands on the server. These commands would run as the user that logged into the GUI, not necessarily as root, and would therefore have many fewer restrictions. This general "command" service is not currently implemented, but could be written if it is deemed necessary.

Environment of Priv Commands Run Via Runpriv

There are several restrictions placed on priv commands by the runpriv (1M) program for security reasons: This last restriction makes it impossible to use a script as a priv command, because by default IRIX systems are configured to refuse to run shell scripts if effective uid != real uid. In this case, the recommend solution is to write a C wrapper that sets the uid to the effective uid and then calls the script. For example:
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>

#ident "$Revision: 1.2 $"

#define SCRIPT "/usr/bin/script.pl"

/*
 * A C front end to a Perl script.
 * This is required so that runpriv (1M) can
 * execute the script.
 */
void main( int argc, char *argv[]) 
{
    
    int   status;
    /* Set the uid to the effective uid 
     */
    if (setuid(geteuid()) < 0) {
        perror("setuid");
        exit(1);
    }
    status=execv(SCRIPT,argv);
    exit(status);
}

Dividing Functionality Among Priv Commands

Depending on the product, it may be desirable to write one priv command that provides all of the functionality necessary, or several priv commands that are specialized. For example, a product that manages user accounts could have one userAccount priv command that could add, remove, and modify accounts based on the arguments, or the product could have addUserAccount, removeUserAccount, and modifyUserAccount priv commands.

When deciding on how to break the functionality into priv commands, remember that the runpriv command gives an administrator the ability to grant privileges to users on a per priv command basis. Therefore, if you feel that an administrator would benefit from being able to grant privileges to a subset of the functionality of your product, you should divide the priv commands appropriately. For example, in the userAccount case above, if there is only the userAccount priv command, then the administrator must grant a user permission to perform all user account functions, or grant no permission. In the case where three priv commands were provided, the administrator could give a particular user permission to add user accounts without giving that user permission to modify or delete existing accounts.


Naming Priv Commands

Because all priv commands reside in a common directory, give priv commands names that will not collide with the priv commands written for other products. It is recommended that you prefix the name of your product's priv commands with a prefix representing the product. For example, fsmgrAddMachine would perform the "add machine" functionality for the Failsafe Manager (fsmgr) product.

Returning Status from Priv Commands

While the Rhino infrastructure gives programmers direct access to the return codes and output streams of priv commands, there are some conventions that make things easier:
  1. The priv command should return 0 if it was successful. Otherwise return a non-zero error code. This error code can be used by the GUI to present a user-friendly, localized error message, so make sure that the error code is specific enough for the GUI to display a useful message.
  2. If the priv command is not successful, then the priv command may send any error output to stderr. By default, the GUI will display this text if the return value is not 0.
  3. Any other output that the GUI needs should be sent to stdout.

Validating Input

GUIs written with the Rhino infrastructure often verify that all the arguments to a priv command are valid. This does not mean, however, that the priv command can assume that the arguments are valid. There are several reasons for this: For these reasons, it is the responsibility of the priv command to verify that all the inputs are valid before performing any operation.

Priv Commands Should be Atomic

From the user's point of view, a priv command should be an atomic operation that either succeeds completely or fails without making any changes to the system. This is because a half-completed priv command will often leave the system in an inconsistent state that is difficult to diagnose and fix. While this is not always practical, it should be a goal for any priv command.

On a related note, there is nothing to prevent two GUIs from calling the same priv command at the same time. If the system could be corrupted as a result of two or more simultaneous priv commands running, the it is the responsibility of the writer of the priv command to provide some kind of locking mechanism to prevent corruption.


Priv Commands are Logged

All priv commands executed by runpriv are logged in /var/sysadm/salog, and salog is world-readable. This has several implications:
  1. Make sure that no private or secret data is revealed by the priv command on the command line. A way to pass data to a priv command in a secure fashion is discussed below.
  2. An user can see a list of all the priv commands that were run. This let users create scripts that call the priv commands simply by cutting and pasting from the log file. If the user is not root, then they will have to use the runpriv command to run the priv commands.
  3. The log can often be useful while debugging the GUI and the priv commands to see exactly what commands the GUI ran (or tried to run).

Passing Arguments to Priv Commands

This section describes the parameter passing conventions used by Rhino applications and the priv commands that they use. The conventions described below are guidelines only - the Rhino infrastructure will allow complete control of the arguments used to start a priv command - but following the conventions will make writing both the GUI Tasks and the priv commands easier because you can take advantage of existing infrastructure for passing the arguments from the client to the server.

The following sections have a lot of detail so that the reader will understand the implications of the way that Rhino passes arguments. Most of the details are taken care of by the infrastructure and libraries supplied with Rhino.

Basic Arguments

Arguments should be passed as key/value pairs, where both the key and value are strings, and the key is separated from the value by a "=" character. The order of the pairs is not important. The following "escapes" are used:
CharacterEscape
=%3d
\n%0a
%%25
For example, the priv command to add a user might look like:
/usr/sysadm/privbin/addUser username=guest uid=123 homedir=/usr/people/guest "realname=Guest User"
This is the way that the client side "runPriv" Java method sends the arguments by default. The key/value style of argument matches the structure of TaskData (the data structure that the Tasks use to store the input collected from users). This makes is so that the Task can automatically convert the data entered by users in a Task to a command line. It also makes the log file easier to understand than the traditional "flag" style of argument specification.

Passing Arrays

There are times when a priv command needs to accept an array of arguments. For example, to add a host to the /etc/hosts file, the priv command might take as many "alias" fields as necessary. In this case, the preferred method is:

  1. One key specifies the number of values
  2. Each value is passed with a separate key, where the key is formed by appending a number to a prefix.

For example:

addHost numAliases=3 alias0=bonnie alias1=bonnie.engr alias2=bonnie.engr.sgi.com

This approach obviates the selection of a delimiter character (required in the case of passing an array as, for example, Item=value0,value1,value2) and allows a consistent approach across CLIs.

Passing Args on Stdin

There are some situations where there are too many arguments to fit on the command line, or it's not desirable (perhaps for security reasons) to put a particular argument on the command line. In this case, the priv command can take some arguments on the command line, followed by the special argument "-input". This is a signal to the priv command that it should read the remaining arguments from file descriptor 0 (stdin). The arguments specified on file descriptor 0 are specified as key/value pairs similar to command line arguments, but there are a few differences. Each arguments sent to stdin follows the following format:
  1. An 8 character hexadecimal ASCII representation of the number of bytes taken up by the key, the value, an equal sign, and a newline.
  2. A space
  3. The key, quoted as described above
  4. An equal sign
  5. The value, quoted as described above
  6. The newline character
The GUI automatically sends any arguments that don't fit on the command line to stdin. It also sends any piece of TaskData that have been marked as hidden (via setAttrVisible method of TaskData or AttrBundle) to stdin. The C API (described below) makes reading all of the arguments, both from the command line and stdin, as easy as a few function calls.

C API

To make the writing of priv commands easier, a C API and library are provided that make the reading of arguments a trivial process. The function calls are described first, followed by example code that illustrates how the API is intended to be used.

This is not meant to be a complete description of the SaParam API. See SaParam.h for complete documentation.

Access

Headers for this API are obtained from the header file sysadm/SaParam.h (in sysadm_root.sw.hdr). The library that implements the API is /usr/lib32/libsysadmParam.so. (in sysadm_root.sw.lib)

Types

The types defined by this API are opaque:


# SaParam is a structure that holds all of the parameters, not a single param
typedef struct _SaParam SaParam;
typedef struct _SaParamIter SaParamIter;

Create and Destroy

extern SaParam *SaParamCreate(void);
extern void SaParamDestroy(SaParam *param);

SaParamCreate() returns NULL if malloc() fails.

Set and Get

extern int SaParamSet(SaParam *param, const char *key, const char *value);
extern const char *SaParamGet(SaParam *param, const char *key);

SaParamSet() returns 0 if successful, -1 if malloc fails.
SaParamGet() returns NULL if there is no value for "key".

The pointer returned by SaParamGet is owned by "param", and will remain valid as long as SaParamDestroy() or SaParamSet() for this key are not called.

Argument Parsing

#define SaPARAM_INPUT_ARG "-input"
extern int SaParamParseArgs(SaParam *param, int argc, char *argv[]);

Parse command line arguments of the form "key=value" into key/value pairs. If the argument SaPARAM_INPUT_ARG ("-input") is encountered, read key value pairs from file descriptor 0 as well. Returns 0 if successful, -1 if memory is exhausted, if read fails, or if an unrecognized argument is encountered.

Enumerating the Keys

extern SaParamIter *SaParamIterCreate(SaParam *param);
extern void SaParamIterDestroy(SaParamIter *iter);
extern const char *SaParamIterGetKey(SaParamIter *iter);
extern const char *SaParamIterGetValue(SaParamIter *iter);
extern void SaParamIterReset(SaParamIter *iter);

To iterate over all of the keys in an SaParam object, create an SaParamIter using the SaParamIterCreate function, and call SaParamIterGetKey repeatedly until it returns NULL. At any point in the iteration, SaParamIterGetValue can be called to get the value corresponding to the last key returned by SaParamIterGetKey. This is more efficient than calling SaParamGet with each key.

Priv command Sample Code

    int main(int argc, char *argv[])
    {
        const char *name = NULL;
        const char *uid = NULL;
        
        // Create param object
        SaParam param = SaParamCreate();
        
        // Parse the command line.
        SaParamParseArgs(param, argc, arg);
        
        name = SaParamGet(param, "name");
        uid = SaParamGet(param, "uid");
            
        ...
	SaParamDestroy(param);
    }