Writing Priv Commands
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:
- The user is running as root.
- There is no root password on the system.
- There is an file in the defaultPrivileges(4) directory granting the
privilege to all users.
- There is an entry in the privilegedUsers(4) database granting the user
all privileges.
- There is an entry in the privilege(4) database granting the user the
requested privilege, and the user is not an NIS user.
- 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.
There are several restrictions placed on priv commands by the runpriv
(1M) program for security reasons:
- The priv command must be installed in /usr/sysadm/privbin
- The environment is cleansed of all but the most basic environment
variables (see /var/sysadm/privenviron)
- The home directory is set to /var/sysadm/home
- The priv command runs with the effective uid set to 0 (root) and the
real uid set to the uid of the user that logged into sysadmd.
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);
}
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.
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.
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:
- 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.
- 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.
- Any other output that the GUI needs should be sent to stdout.
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:
- The priv command may have been invoked from a shell or script and
not from the GUI
- The GUI could have a bug
- The GUI may not be able to validate all inputs
- Due to timing issues, the GUI might not know the correct current
state of the system for validation.
For these reasons, it is the responsibility of the priv command to
verify that all the inputs are valid before performing any
operation.
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.
All priv commands executed by runpriv are logged in
/var/sysadm/salog, and salog is world-readable.
This has several implications:
- 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.
- 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.
- 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).
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:
Character | Escape |
= | %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:
- One key specifies the number of values
- 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:
- An 8 character hexadecimal ASCII representation of the number
of bytes taken up by the key, the value, an equal sign, and a newline.
- A space
- The key, quoted as described above
- An equal sign
- The value, quoted as described above
- 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);
}