Friday, February 28, 2014

TCL Command Wrapper:

In my opinion there are some commands that should not be readily accessible to most users. Speaking from experience, I have seen users create a new temporary file pointer to the current directory (a copy of &UFD&) and then later run CLEAR.FILE on the temporary file (clearing the current directory (the UniVerse account))! This example is an unusual case which is unlikely to be repeated (especially by the programmer I saw do it). However I have also seen users run CLEAR.FILE on an important application file by mistake. I have never personally seen anyone delete or clear a file accidentally when they forgot there was an active select list but I can certainly see where this could happen.

Another example of misusing a command is someone changing data with the editor, essentially circumventing the application. Depending on the application this could have unintended consequences (for example, possibly bypassing security and auditing, or programmatic relational cross references, etc.)

One way to avoid potential problems is to restrict access to certain commands. Or at the very least record individual use of certain commands. This article suggests employing a "wrapper" to restrict access to and record usage of certain commands.


  • Note: If the goal is strictly to restrict access then I suggest setting up important commands as VOC (R)emote items, with a program (defined in field four of the VOC remote record) that returns a one in the 'flag' (the last) argument in the security subroutine if access is granted. Review "UniVerse System Description", pg 3-15 (v11.2.5, July 2015, UNV-1125-SYS-1) for more information regarding the Remote command VOC type.


The least innocuous step is to audit the use of commands. I propose creating a cataloged program that would replace the VOC verb (or sentence, paragraph, etc.) of commands that should be audited. This replacement program would determine the "native" command to run by using the command name from the sentence that invoked the wrapper. This native command, if only for delivered verbs, could read NEWACC for the verb contents and reconstruct a new verb to run. However, the idea is for the wrapper to also work for non-delivered, custom sentences, paragraphs, and verbs. With this in mind I propose copying the verb to-be-wrapped to the VOC but with a unique and well known name. In this example the program assumes the native VOC item has .CMDWRPR appended to it and is in the VOC.

Note: All programming displayed on this site is for illustrative purposes only. Use at your own risk.

program CMD.WRAPPER

*

*******************************************************************
*
* Put a wrapper around specific Verbs/programs.
*
*******************************************************************
*
* If the command being executed calls this program, then the verb (or program) is meant
* to be "wrapped". This means that an audit is to be made and/or a security check is to
* be performed to ensure the user is authorized to run the command.
*
* For example, The following shows that the CLEAR.FILE and DELETE.FILE verbs are to be
* wrapped (the records just have to exist, there is no sentinel that is required).
*     >SORT CMDWRPR_AUDIT COL.SUP
*     CLEAR.FILE
*     DELETE.FILE
*
*     2 records listed.
*
*     >ED CMDWRPR_AUDIT *
*     SELECTing all records in the file.
*
*     SELECTed record name = "DELETE.FILE".
*     0 lines long.
*
*     ----: N
*
*     SELECTed record name = "CLEAR.FILE".
*     0 lines long.
*
*     ----: N
*
* If auditing of the command is specified then a simple date/timestamp and user information
* (ID and DBMS#) is stored in the CMDWRPR_AUDIT file. (I had thought about simply starting
* a COMO log but that may not survive long term (due to house cleaning, etc.))
*
* If the command EXISTS in the CMDWRPR_SECURITY file then the list of valid userIDs is read
* from the CMDWRPR_SECURITY file (with command as the key).
*
* For example, the following two records (CLEAR.FILE and ED) have no users and three
* users that are able to execute these commands:
*
*     >SORT CMDWRPR_SECURITY @ID FMT "18L" ID.SUP EVAL "@RECORD" FMT "99L"
*
*     CMDWRPR_SECURITY. CLEAR.FILE
*     @RECORD.......... * Add users to this record to be able to run this command
*
*     CMDWRPR_SECURITY. ED
*     @RECORD.......... * Add users to this record to be able to run this command
*                     . brianp
*                     . bsmith
*                     . johnd
*
*     2 records listed.
*
*******************************************************************
*
* Notes:
*   The "real" command is stored in the VOC with a different ID (an option could be to read
*   NEWACC for the delivered command but that won't work because this program will wrap user
*   custom paragraphs and sentences as well).
*
*   There are on-error catches and else clauses for the audit file update. Depending on a
*   flag that is hard coded in this program, the command being audited will either NOT
*   be executed, or the errors will be informational only (output to the user) and the
*   command will still run.
*
*   NOWRAPPER on the command line will drop through and run the command without the wrapper.
*
*******************************************************************
*

equ true to 1

equ false to 0
equ IK$SLACTIVE TO 1

audit.write.failure.will.abort = true


*

* Parse command-line for the command...
*
sentence = upcase(trim(@SENTENCE))
convert " " to @FM in sentence
command = sentence<1>
sentence = delete(sentence,1,0,0) ;* remove the verb
original.sentence = sentence      ;* Need this for later

locate "NOWRAPPER" in sentence<1> setting dummy then

   *
   * This provides for a failsafe in case the program does something unexpected
   * and support staff really needs to perform what was typed/entered at TCL.
   *
   command := ".CMDWRPR"                        ;* original verb, sentence or paragraph
   execute command:" ":original.sentence
end else
   open '','CMDWRPR_AUDIT' to F.AUDIT then
      call file.io.sub("R", F.AUDIT, command, dummy, errcode)
      if not(error code) and len(dummy) then ;* Audit this command...
         *
         * Check for active select list...
         *
         audit.info = @DATE
         audit.info<-1> = @TIME
         audit.info<-1> = @LOGNAME
         audit.info<-1> = @USERNO
         audit.info<-1> = @TTY
         audit.info<-1> = @LEVEL
         audit.info<-1> = command:" ":original.sentence      ;* This command
         audit.info<-1> = @COMMAND.STACK<2>                  ;* Previous command
         if selectinfo(0,IK$SLACTIVE) then
            asl = 1
         end else asl = 0
         audit.info<-1> = asl                                ;* indicate if there is an active select list

         tmp = @DATE:".":@TIME:".":@USERNO

         audit.key = tmp
         loop.cnt = 0
         audit.updated = false
         loop
            loop.cnt += 1
            readu dummy from F.AUDIT, audit.key locked
               audit.key = tmp:".":loop.cnt
            end else
               write audit.info to F.AUDIT, audit.key on error
                  crt "Write to audit failed!"
                  if audit.write.failure.will.abort then stop  ;* stop immediately
               end else
                  crt "Unable to write to audit!"
                  if audit.write.failure.will.abort then stop  ;* stop immediately
               end
               audit.updated = true                            ;* Even if there is some type of error just continue
            end
            until audit.updated or loop.cnt >= 100
         repeat
         if not(audit.updated) then
            crt "Command not audited!"
            if audit.write.failure.will.abort then stop        ;* stop immediately
         end
      end else null                                            ;* Command is NOT to be audited
   end else
      crt "Unable to open AUDIT! Command not audited!"
      if audit.write.failure.will.abort then stop              ;* stop immediately
   end
end

okay.to.run = true

open '','CMDWRPR_SECURITY' to F.SECURITY then
   okay.to.run = false                                         ;* Reset to false
   read security.rec from F.SECURITY, command
      on error print "Unknown error reading CMDWRPR_SECURITY!"
      then                                                     ;* Command IS locked down
         locate @LOGNAME in security.rec<1> setting dummy then
            *
            * User is in the list to be granted access...
            *
            okay.ro.run = true
         end else
            *
            * User is NOT in the list to be granted access...
            *
            okay.to.run = false
            crt "Access to ":command:" is restricted."
         end
      end else null                                            ;* Command is NOT locked down
end else null                                                  ;* No commands are locked down

if okay.to.run then

   *
   * Run the command (that is stored with the .CMDWRPR suffix)...
   *
   *
   newcmd = trim(command:".CMDWRPR ":original.sentence)
   execute newcmd
end

end





I just wrote this routine and have not extensively tested it. Please use at your own risk!

No comments:

Post a Comment