How to write a privileged command procedure that can be run by a relatively unprivileged user. by M. Erik Husby Manager, Systems and Operations Project Software and Development, Inc. 14 Story Street Cambridge, Ma. 02138 617-661-1444 For many years now, I have been hearing others ask for away to protect command procedures from prying eyes and for a way to install procedures with privileges. Being a system manager, I can emphasize with these requests. I have a very definite need for a privileged command procedure. As part of the software system in development at PSDI, there is a dataset to which write access needed to be limited to one person at a time. The problem I faced was how to do this in a controlled manner. ( We can not use RMS's record locking since the dataset is not read or written using RMS). My initial solution was to set up a privileged captive account which only allowed certain operations to be performed. This was not statisfactory because one had to logout and log into the privileged one. Once we had DECnet, we discovered we could do a "SET HOST" to the local node thus eliminating one login. However we were still faced with the problem that one could log in only to discover that someone else was already using the account. Since the account only allowed one user, one then had to try again sometime in the future. I kept saying to myself, if there was only a way to for a user to submit a batch job to be run by another account my problem would be solved. Well it turns out that DECnet provides the solution. And since DECnet is now part of VMS, the solution is available to all even if you only have a one node network. The solution lies in DECnet's task-to-task communication capablities and in the undocumented contents of a logical name. So beware if you implement this technique that it may break at any time in a future version of VMS. It works for VMS 3.6. When a network job is created, a process logical name SYS$NET is setup which contains the information needed to do network I/O. This logical name also contains the name of the node orignating the network job, the process id of the orginating process, and most importantly, the name of the task being requested. Thus the login procedure of an account can examine this logical name and reject or accept network tasks based on the task name. To set up a privileged command procedure, you will need to do several things. First, one must setup an account with appropriate privileges. Then the login procedure for that account must be written to filter out the desired tasks. Then the privileged procedure must be written in two parts; the first part is visible to the unprivileged users (I said relativly unprivileged users since the user of this procedure must be able to use DECnet), and the second part is hidden in the directory of the privileged account. I will illustrate these steps using my task as an example. Account Parameters. I set my account up with the following parameters. You will notice that VMS through the "DISUSER" option prevents interactive logins. The login procedure will control the network jobs. This account has a null password (i.e. it was created with /PASSWORD=""). For my purposes, it was better to turn on privileges as I needed them hence I gave the account SETPRV. Username: NETCDL Owner: PROJECT 2 Account: TECHAPPL UIC: [301,002] CLI: DCL LGICMD: LOGNETCDL.COM Default Device: PROJECT2: Default Directory: [NETCDL] Login Flags: DEFCLI LOCKPWD CAPTIVE DISWELCOME DISUSER Primary days: Mon Tue Wed Thu Fri Sat Sun DISDIALUP Secondary days: No hourly restrictions PRIO: 4 BYTLM: 30000 BIOLM: 20 PRCLM: 15 PBYTLM: 0 DIOLM: 20 ASTLM: 25 WSDEFAULT: 512 FILLM: 125 ENQLM: 20 WSQUOTA: 768 SHRFILLM: 0 TQELM: 40 WSEXTENT: 1024 CPU: no limit MAXJOBS: 0 MAXACCTJOBS: 0 PGFLQUOTA: 10000 Privileges: GRPNAM GROUP SETPRV TMPMBX NETMBX The directory for this account was created with the following command $ create/directory- /owner=[301,2]- /protection=(system:rwe,owner:rwe,group,world) - project2:[netcdl] This prevents the prying eyes from seeing the command procedures or any of the files in the directory. The LOGNETCDL.COM procedure filters out the network tasks, logs out any interactive users just in case VMS failed, and allows batch jobs through. It has the following structure. $ goto 'f$mode()' $ interactive: $ logout/brief $ batch: $ exit $ network: $ set noon $ @rejectnet 'valid_tasks' $ if .not. $status then logout/brief $ exit The RejectNet procedure is used to examine the logical name SYS$NET for valid tasks. As a by-product of its actions, it leaves the global DCL symbols Requestor_Node and Requestor_ID as the node and process id of the process making the request. RejectNet.Com is listed at the end of the article along with the other command procedures mentioned. The authorization flag DISUSER prevents anyone from logging into the account. The RejectNet procedure prevents any unauthorized network jobs from being run by the account. And the directory protection prevents anyone except the system from adding or modifing any of the files in the directory. The account seems to be secure. I have not found a way around these protections; but there still may be a loophole. I would be very interested in hearing from anyone that discovers a loophole. The Privileged Command procedure Once the account is setup, the privileged command procedure can be written. As was stated above, the procedure is written in two parts. The unprivileged part will perform the following tasks: verify parameters, invoke the privileged portion by using TASK-TO-TASK communication, and pass the appropriate information to the privileged portion. The privileged part will perform the following tasks: verify that the requestor is authorized, pick up the data from the requestor, and perform the privileged operation. When I say that the privileged part verifies that the requestor is authorized I mean that the procedure can reject requests from other nodes or it can check to see if the requestor is one from a list of authorized accounts. In the procedure I wrote, it rejected requests from other nodes and if a certain operation was requested it had to come from a particular account. The privileged procedure is started with the following command by the unprivileged procedure. $ OPEN/READ/WRITE sysnet node"account"::"TASK=privileged_task" -- "SYSNET" is the logical name to be used for subsiquent reads and writes. -- "NODE" is the name of the node on which the privileged procedure resides. -- "ACCOUNT" is the name of the account which will execute the privileged procedure. -- "PRIVILEGED_TASK" is the name of the privileged command procedure. In my case, the unprivileged procedure SUBMITCDL called CDLSUBMIT with an open statement that looked like this: $ OPEN/READ/WRITE SYSNET TERRA"NETCDL"::"TASK=CDLSUBMIT" The privileged procedure uses an open statement to complete the connection. It looks like THIS: $ OPEN/READ/WRITE SYSNET SYS$NET Where SYS$NET is the logical name that VMS sets up for network jobs. By specifing /READ/WRITE on the open statements, the procedures can pass data both ways. In my case, the SUBMITCDL procedure passes parameters to the CDLSUBMIT procedure which submits a batch job. The CDLSUBMIT passes back information showing that the batch job was submitted. In summary, the flow of control between the two procedures will look like this: Unprivileged Procedure Privileged Procedure ---------------------- -------------------- Verifies parameters. Creates the privileged procedure with the OPEN statement. Checks out the requestor, aborts if not valid. Passes information to the privileged procedure. Based on data received from the unprivileged procedure, performs its function. Passes results back to the unprivileged procedure. Receives results of the privileged procedure. Closes the network connection. Closes the network connection. Important Programming Consideration Because DCL does not provide all the error control features that are available in a language like BLISS or FORTRAN such as timeouts on I/O operations, it is very important that the command procedures keep a tight control over CONTROL-Y's and other errors. If the procedures get out of synch with reads and writes over the network connection, you can not use CONTROL-Y to abort an unsatistfied read or write. The only way is to abort the network job which causes the read or write to complete with an error. Invoking the procedure. We have a symbol defined that invokes the SUBMITCDL command procedure. Thus it ends up looking like a normal DCL command. NOTE While developing the procedure, I found it very handy to use a logical name for the node and account portion of the OPEN statement. This allowed me to do the development from my own account where I could easily look at log files to see what went wrong. Then when I put things into actual use, I simply added a system logical name for the correct node and account so that I did not have to change the procedures. Conclusions I have presented a technique that I have used to implement privileged command procedures. This technique should be applicable to other procedures, in fact I have plans for several more. Our programmers are using this procedure daily with no problems. They are very happy that they do not have to log into an account to get this task accomplished. I hope that I have stimulated some thinking about other ways to use DECnet. I will be willing to talk with anyone that has questions about this technique, please feel free to call or write. Command Procedures $ save_verify = 'f$verify(1)' ! Make the 1 a 0 when procedure debugged. $ goto skip_header $ ! $ ! rejectnet.com $ ! $ ! Procedure to: Reject unauthorize network access $ ! $ ! Author: M. Erik Husby, on Wed May 9 17:43:07 1984 $ ! $ ! Modifications: $ ! $ ! Calling sequence: @rejectnet.com valid_procedures local $ ! Where 'valid_procedures' is a list of the valid procedures to be $ ! executed by a network job. Entries are separated by slashes. $ ! If a valid procedure is found, return $STATUS = SS$_NORMAL $ ! otherwise return SS$_NOPRIV (%x22). $ ! Where 'local' is an optional boolean value which indicates wether $ ! a request from a truely remote node should be rejected. If true $ ! then the request must be from the local node. Defaults to FALSE. $ ! $ ! Returns the global symbols $ ! Requestor_Node as the node name of the requestor $ ! Requestor_Pid as the process id of the requestor $ ! $ skip_header: $ exit_status=1 ! SS$_NORMAL $ ss$_nopriv = %x22 $ ! $ ! Get logical link information from SYS$NET logical name $ sysnet = f$logical("SYS$NET") $ local_node = f$logical("SYS$NODE") - "_" $ ! $ ! Define global symbols for our caller $ requestor_node == f$extract(0,f$locate("""",sysnet),sysnet) $ requestor_pid == f$extract(f$locate("=",sysnet)+1,8,sysnet) $ ! $ ! First check the LOCAL switch $ local = p2 $ if local .eqs. "" then local = "FALSE" $ if local .and. (local_node .nes. requestor_node) then goto not_valid $ ! $ ! If no valid procedures, exit with ss$_nopriv $ valid_procs = p1 $ len_sysnet = f$length(sysnet) $ next_procedure: ! Get next procedure name $ valid_proc = f$extract(0,f$locate("/",valid_procs),valid_procs) $ if valid_proc .eqs. "" then goto not_valid! End of list? $ valid_procs = valid_procs - valid_proc - "/"! Remove current from list $ len_vp = f$length(valid_proc) ! Determine how much to compare $ j = len_sysnet - 1 - len_vp ! in sysnet $ if valid_proc .eqs. f$extract(j,len_vp,sysnet) then goto valid $ goto next_procedure ! Not found, try next in list $ valid: ! A valid procedure name found $ exit_status = 1 $ goto exit_com $ not_valid: $ exit_status = ss$_nopriv $ ! $ ! all done: $ ! $ exit_com: $ exit 'exit_status' ! 'f$verify(save_verify)' Restore verify status $ save_verify = 'f$verify(0)' ! Make the 1 a 0 when procedure debugged. $ goto skip_header $ ! $ ! submitcdl.com $ ! $ ! Procedure to: Requests, through DECnet, that a CDL job be submitted $ ! to a batch queue. By going through DECnet, we can run a priviledged $ ! command procedure for a non-privileged user. $ ! $ ! For jobs doing a PRODUCTION level compile, we need a password. $ ! We get the password in one of two ways, either the global symbol $ ! 'cdl_prod_password' is defined in which case we use its value, or $ ! we prompt for it. $ ! $ ! NOTE: Requires the logical name NETCDL to be defined with a node $ ! and account name. An example definition would be $ ! $ DEFINE/SYSTEM NETCDL "TERRA""NETCDL""::" $ ! $ ! Author: M. Erik Husby, on Tue May 15 11:37:39 1984 $ ! $ ! Modifications: $ ! MEH on 25-May-1984 -- To allow a NoReplace option $ ! MEH on 30-May-1984 -- Corrected defaulting of NoReplace option $ ! $ ! Calling sequence: @submitcdl.com [input_file] [system_name] [NOReplace] $ ! Where 'input_file' is the name of the CDL commands. Defaults to the $ ! type ".CDL". Prompt if not given. $ ! $ ! 'system_name' is the system into which the compilation is to be done. $ ! Defaults to 'PWORK' $ ! $ ! $ skip_header: $ exit_status=1 ! SS$_NORMAL $ ! $ ! Get name of the input file, verify existance $ input_file = p1 $ if input_file .eqs. "" then inquire input_file "_CDL input file" $ if input_file .eqs. "" then goto exit_com! Exit if no name given $ ! $ ! Expand name and verify existance $ input_file = f$parse(input_file,".CDL") $ full_name = f$search(input_file) $ if full_name .eqs. "" then goto not_found $ ! $ ! Set value of no_replace option -- Defaults to FALSE i.e. replace the $ ! CDBLIB. If parameter is NOReplace, then set value to TRUE i.e. do not $ ! replace the CDBLIB. $ no_replace = p3 $ if no_replace .eqs. "" then no_replace = "TRUE" $ no_replace = .not. no_replace $ ! $ ! Check out system_name, default to PWORK $ system_name = p2 $ if system_name .eqs. "," then system_name = "PWORK" $ if system_name .eqs. "" then system_name = "PWORK" $ if system_name .nes. "PROJ" then goto submit_job $ username := 'f$getjpi("","USERNAME") ! Trims blanks $ if username .eqs. "PSDICES" then goto get_password $ exit_status = %x22 ! No Privilege $ goto exit_com $ get_password: ! For production runs, we need a password $ if "''cdl_proj_password'" .eqs. "" then - inquire cdl_proj_password "_Project/2 Production password" $ ! $ ! Got an input file, lets start up things on the other side $ submit_job: $ on error then goto error_exit $ open/read/write sysnet NETCDL::"TASK=CDLSUBMIT" $ ! $ ! Write to our partner the file name and system_name and the replace option $ write sysnet full_name $ write sysnet system_name $ write sysnet no_replace $ ! $ ! And if system name if PROJ, send the password as well $ if system_name .eqs. "PROJ" then write sysnet cdl_proj_password $ ! $ ! Now read the response, echo it as well $ response_loop: $ read/end=response_done/error=error_exit sysnet line $ write sys$output line $ goto response_loop $ response_done: $ close sysnet $ ! $ ! All done $ goto exit_com $ ! $ ! Error handlers $ not_found: ! Input file not found $ exit_status = %x10031098 ! Open error (with message given bit set) $ write sys$error "''f$fao(f$message(%x31098),input_file)'"! Error opening $ write sys$error "''f$message(%x18292)'" ! Rms$_FNF - file not found $ goto exit_com $ error_exit: $ exit_status = $status $ set noon $ close sysnet $ ! $ ! all done: $ ! $ exit_com: $ exit 'exit_status' ! 'f$verify(save_verify)' Restore verify status $ save_verify = 'f$verify(1)' ! Make the 1 a 0 when procedure debugged. $ goto skip_header $ ! $ ! cdlsubmit.com $ ! $ ! Procedure to: Submit a CDL job to the P2BATCH job for a non-privileged $ ! user. The calling procedure SUBMITCDL invokes us as a network jbo. $ ! The login procedure for this account calls REJECTNET.com to reject $ ! all unauthorized network access. As a by-product, the global symbols $ ! Requestor_Node and Requestor_Pid are set. Since we only run on the $ ! node that invoked us, we can use F$GETJPI to find out more about our $ ! caller. $ ! $ ! Author: M. Erik Husby, on Tue May 15 14:01:01 1984 $ ! $ ! Modifications: $ ! MEH on 25-May-1984 -- To allow a NOReplace option that eliminates $ ! the replacement of CDBLIB $ ! MEH on 30-May-1984 -- To submit jobs at priority 5 if we have the $ ! privilege. $ ! $ ! Calling sequence: @cdlsubmit.com $ ! $ skip_header: $ exit_status=1 ! SS$_NORMAL $ ! $ if f$mode() .nes. "NETWORK" then goto exit_com $ ! Open the network connection to get the filename and level number of $ ! our requestor. $ on error then goto error_exit $ open/read/write sysnet sys$net $ read sysnet cdl_file $ read sysnet cdl_level $ read sysnet no_replace $ ! $ ! Now get some info on our caller $ old_privs = f$setprv("WORLD") $ requestor_username := 'f$getjpi(requestor_pid,"USERNAME") $ old_privs = f$setprv(old_privs) $ ! $ ! We will only allow PRODUCTION levels from a username of PSDICES $ if cdl_level .nes. "PROJ" then goto level_ok $ if requestor_username .eqs. "PSDICES" then goto level_ok $ ! Invalid level requested, return a no-privilege error message $ write sysnet "''f$message(%x22)'" $ goto exit_com $ level_ok: $ ! $ ! Build the cdl system command from the level $ ! We know the passwords on WORK/TEST $ ! Also build a name for the output file that includes a directory name $ if cdl_level .nes. "PWORK" then goto not_pwork $ password = "'PWORK' 'PWORK'" $ output_dir = f$logical("PSDICES_WORK") - "]" + ".LIST]" $ popts = "NONE" $ goto level_done $ not_pwork: if cdl_level .nes. "PTEST" then goto not_ptest $ password = "'PTEST' 'TESTP'" $ output_dir = f$logical("PSDICES_TEST") - "]" + ".LIST]" $ popts = "NONE" $ goto level_done $ not_ptest: if cdl_level .nes. "PROJ" then goto not_proj $ ! $ ! If level is PROJ then we need a password from the requestor $ read sysnet proj_password $ password = "'PROJ' '" + proj_password + "'" $ ! $ ! Proj listings are always printed, not kept online $ popts = "PRINT/DELETE" $ output_dir = "PSDICES_PRODUCTION:" $ goto level_done $ ! $ ! Not a standard level. $ not_proj: ! Not one of the standards, use the $ password = "'" + cdl_level + "'"! the level name $ popts = "NONE" $ ! $ ! See if this unknown level has a LIST subdirectory, if so use it for the $ ! output file. Otherwise stick the output into PSDICES_WORK: $ set noon ! Incase directory does not exist $ list_dir = "" ! list_dir was not being defined. $ list_dir = f$search("PSDICES_''cdl_level':LIST.DIR") $ set on $ if list_dir .nes. "" then - output_dir = f$logical("PSDICES_''cdl_level'") - "]" + ".LIST]" $ if list_dir .eqs. "" then - output_dir = F$logical("PSDICES_WORK") - "]" + ".LIST]" $ level_done: $ ! $ ! Things look ok, lets build an input stream to do the compile $ job_name = f$parse(cdl_file,,,"NAME") $ output = output_dir + job_name + ".lcd" $ open/write bj 'job_name'.dat $ input = f$search("''job_name'.dat") $ write bj "cdl;**SING;System ",password $ write bj "*input file ",cdl_file $ write bj "finish" $ close bj $ ! $ ! Now write an parameter file incase the file names are too long for the $ ! submit command $ open/write opt 'job_name'.par $ option_file = f$search("''job_name'.par") $ write opt "$ input_file :== ",input $ write opt "$ output_file :== ",output $ write opt "$ popts :== ",popts $ write opt "$ job_name :== ",job_name $ write opt "$ no_replace :== ",no_replace $ close opt $ ! $ ! Let the submittor know the parameters $ write sysnet "System : ",cdl_level $ write sysnet "Input file : ",cdl_file $ write sysnet "Output file : ",output $ write sysnet "Print Options : ",popts $ if no_replace then write sysnet "CDBLIB : will not be replaced" $ ! $ ! See if we have privilege to submit at priority 5 $ old_privs = f$setprv("ALTPRI") $ submit_priority = 5 ! Default to priority 5 $ if .not. f$privilege("ALTPRI") then submit_priority = 4 $ ! $ ! Done building the batch job, submit it for processing. $ open/write sj 'job_name'.tmp $ write sj "$ submit-" $ write sj " /queue=p2batch-" $ write sj " /name=''job_name'-" $ write sj " /printer=black_hole-" $ write sj " /priority=''submit_priority'-" $ write sj " /parameter=(''requestor_username',''option_file') -" $ write sj " batchcdl" $ close sj $ @'job_name'.tmp/output='job_name'.tmp $ ! $ ! Restore old privileges $ old_privs = f$setprv(old_privs) $ ! $ ! Send back the submit response $ open/read jt 'job_name'.tmp $ response_loop: $ read/end=response_done jt line $ write sysnet line $ goto response_loop $ response_done: $ close jt $ delete/log 'job_name'.tmp;0 ! The temporary output $ delete/log 'job_name'.tmp;0 ! and the temporary command procedure $ ! $ ! Seem to be all done, break the connection $ goto exit_com $ ! $ ! Error handlers $ error_exit: $ set noon $ write sysnet "''f$message($status)'" $ ! $ ! all done: $ ! $ exit_com: $ close sysnet $ logout/full $ exit 'exit_status' ! 'f$verify(save_verify)' Restore verify status