#!/usr/local/bin/wish -f
#			groupie - Version 1.1
#
# Overview
#    This program provides a handy dandy front-end for group-based
#    permission schemes.  By setting the permission bits on "important" 
#    executables to 750 (-rwxr-x---), and setting the group ownership to 
#    a reasonable groupname... it becomes possible to control access to 
#    important programs by controlling which users are allowed into which
#    groups.  This program helps manage the users and groups.
#
#    Not all groups need to be managed by groupie.  The original intention for
#    this program was to limit access to licensed and potentially sensitive
#    software (e.g.  database query software, etc.)  Note that the group-id
#    assigned via the passwd file cannot be revoked or changed by groupie.
#
#    When users are granted (or revoked) access to new groups, the changes
#    become effective for their NEXT login session.  Active login sessions
#    will not be affected by changes in the group file (from a permissions
#    perspective.) 
#
#    Groupie can be used to make modifications directly to the /etc/group
#    file, or to a group file located elsewhere - if you're concerned about
#    having a program named "groupie" massage your /etc/group.  Groupie does
#    not make any provisions for NIS/YP.  Groupie users will have to perform
#    the appropriate yppush or "/var/yp/make group" commands manually.
#
#    If group ownership and permissions are set properly, you'll no longer
#    have to go shoot the weenie who checked out all your Framemaker licenses 
#    when he shouldn't even have access to Framemaker.  The janitor won't be 
#    able to fiddle with his stock portfolio using Lotus and Sybase, either.
#
#    Groupie is written in Tcl/Tk.  Version 7.3 of Tcl and version 3.6 of Tk
#    are the current releases... get them if you don't already have them.
#    The best site for Tcl/Tk stuff is harbor.ecn.purdue.edu, in /pub/tcl.
#    No Tcl extensions are required for groupie, and as far as I can tell,
#    there are no conflicts between any Tcl extensions (TclX, blt, itcl, etc.)
#    and groupie.  Please let me know if you find any.
#
#    Tcl/Tk was written by John Ousterhout while at UC/Berkeley.  If you don't
#    know Tcl, then buy his book, read his book.  Do not pass GO, do not
#    collect $200 until you are done with his book.  The title is "Tcl and
#    the Tk Toolkit" and it was published by Addison-Wesley.  It is as good
#    as an O'Reilly book.
#
# Functional Overview
#    This is a simple program.  If you're getting confused, it's not because
#    it's complicated... it's because my explanation stinks!  Please send me
#    a note pointing out the confusion so that I can save the next person 
#    from suffering through my lousy explanation.
#
#    There are two cross-reference files that determine which users belong to
#    which groups.
#
#      The first file, function2group, maps job functions to UNIX group names. 
#      For example, a "stocktrader" job function needs access to sybase, 
#      lotus, reuters, and marketvision.  A "programmer" job function needs 
#      access to compiler and guibuilder.  A "secretary" needs frame and fax.
#
#      The second file, users2group, maps users to their job functions.  Users
#      may have more than one function.  User "jones" has both "stocktrader" 
#      and "backoffice" responsibilities, so he is allowed into the union of 
#      groups defined by "stocktrader" and "backoffice."
#
#   The file $HOME/.groupie, if it exists, is used to hold the location of the
#   configuration files used by groupie.  Absolute pathnames are highly
#   recommended when defining these files.  If $HOME/.groupie does not exist,
#   then an information-gathering screen will pop up and force the user to
#   enter the pathnames of the configuration files.
#
#   The groupie program reads these two files and it displays the users and the
#   groups that they are allowed into.  The program then looks for a file 
#   containing custom changes.  This "custom changes" file is used to permit 
#   or deny access to particular groups in spite of what the two cross-
#   reference files contain. The custom changes file merely contains a bunch 
#   of commands that invoke user checkbuttons.  Custom changes are immediately
#   obvious on the main screen since the foreground color of the corresponding
#   button is changed to orange.  
#
#   Groupie permits modifications to the function2group and user2function 
#   files.  There are buttons on the main screen to bring up a modification 
#   screen.  There are two other buttons on the main screen:  one is to erase
#   all the custom changes (i.e. users' permissions are based solely on the 
#   cross-reference files), and the other is to save the current state of the 
#   main window to the group file.  The group file can be either /etc/group 
#   (i.e. the real thing) or a copy of it (i.e. like the one I provided in the
#   SampleFiles directory.)
#
# Usage
#   It doesn't get any simpler than this... the command is "groupie" with no
#   command line arguments.
#
# Installation
#    1)  Install Tcl/Tk if you don't already have it.
#    2)  Make sure that groupie has execute permission (chmod +x groupie.)
#    3)  Make sure that the first line of groupie points to the "wish"
#        executable.
#    4)  enter the command "./groupie"
#    5)  If you still have the "SampleFiles" directory supplied with the
#        groupie distribution, then you can try out the sample files.
#    6)  If you are not using the real /etc/group file, then you will have
#        to manually move your group file to /etc/group after you save your
#        changes.
#    7)  Edit your group (whether /etc/group or whatever) to include group
#        names that refer to the software packages you want to control.
#        The SampleFiles directory gives some reasonable examples.
#    8)  Groupie does not do yppush, so if you run NIS, then you have to 
#        yppush (i.e. "/var/yp/make group") manually.  
#    9)  Groupie is intended for system administrators, but it is possible
#        to create a new group that has permission to modify the config
#        files (including /etc/group) and permission to run groupie if it
#        makes sense for your permission-control to be performed by a non-
#        privileged user.
#          
# Limitations
#    1)  Refer to items 6 and 7 in the Installation section above.
#    2)  Groupie will never modify permissions or ownership of any files.
#    3)  Groupie does not create new groups in the group file.  It merely
#        adds usernames to the existing groups.
#    4)  There is no history file or mechanism to track changes.
#    5)  There is no nifty front-end to see which files are executable by
#        which group.  I thought of two ways to do this, but I'm not too
#        excited about either one: 
#              a)  using "find", but it won't work in a network environment
#                  and it would kill disk performance whenever it ran
#              b)  using another configuration file... but that's just 
#                  another headache to keep track of
#        My hope is that your group names make it clear exactly which files 
#        are accessible by each group.
#    6)  If you are running an old version of NFS (i.e. before 4.0), then
#        users are limited to a maximum of 8 groups.  Newer NFS implementations
#        support 16 groups.  If you get "NFS getattr failed/RPC Authentication"
#        errors, then you may be over the limit.
#    7)  Group entries are limited to 1024 characters if you run NIS.
#
# History
#    10/06/94  Originally designed and cranked out by Peter Grina while his
#              family was traipsing around in sunny Italy.
#    10/27/94  Version 1.0 released
#    11/08/94  Version 1.1 - catch error if the initial touch command fails on
#              a configuration file.  Added some more pearls of wisdom to this
#              fascinating documentation section.
#
# Support and Bug Fixes
#    This version of groupie is distributed as free software.  Support will
#    be provided via EMAIL (consult@grina.com, or grina@cnj.digex.net) on a
#    whenever-I-have-time basis.  I plan to provide bug fixes (at the very
#    minimum) for this program.
#
#    Please contact me if you need a guaranteed support contract, if you need
#    custom modifications, etc.
#                     
#                     Peter A. Grina - Consulting
#                     456 South Horizon Way
#                     Neshanic Station, NJ  08853
#
#                     email:  consult@grina.com
#                             -or-  grina@cnj.digex.net
#                     fax, answering machine:  (908) 369-6852
#
#    Once again, PLEASE USE EMAIL FOR SUPPORT QUESTIONS AND BUG REPORTS (unless
#    you have a support contract.)
#
# Copyright Notices
#  The groupie distribution is covered by the following copyright:
#     Copyright(c) 1994, Peter A. Grina.  All rights reserved.
#
#     The permission to use and the disclaimer for groupie are the same
#     as those for Tcl/Tk, but substitute "Peter A. Grina" in place of
#     "University of California," and take note that I don't have any Regents.
#
#  The Tcl/Tk distribution is covered by the following copyright:
#     Copyright (c) 1991-1993 The Regents of the University of California.
#     All rights reserved.
#
#     Permission is hereby granted, without written agreement and without
#     license or royalty fees, to use, copy, modify, and distribute this
#     software and its documentation for any purpose, provided that the
#     above copyright notice and the following two paragraphs appear in
#     all copies of this software.
# 
#     IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR
#     DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING
#     OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE 
#     UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 
#     DAMAGE.
# 
#     THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
#     INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
#     AND FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS
#     ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATION
#     TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
# 
#EOF (end of fluff)


######################################################################
#                           Function2Group
#
#  This procedure parses up the information in the function2group file.
#
######################################################################
proc Function2Group {} {
  global globals gfunction group

  if {[file writable $globals(function2group)] == 0} {
    Error fatal "Cannot update the file \"$globals(function2group)\""
  }

  exec sort -o $globals(function2group) $globals(function2group)
  set F2G [open $globals(function2group) r]

  # generate the complete list of groups mentioned in function2group
  set globals(etcgroups) {}

  set i 0
  while {[gets $F2G line] != -1} {
    if {[llength $line] == 2} {
      set gfunction($i) [lindex $line 0]
      set group($i) [lindex $line 1]
      foreach j $group($i) {
        if {[lsearch -exact $globals(etcgroups) $j] == -1} {
          lappend globals(etcgroups) $j
          if {[catch {exec grep $j $globals(etcgroup)}] == 1} {
            Error warning "Group \"$j\" exists in $globals(function2group) but not in \"$globals(etcgroup)\"\n\"$j\" will not be written out to \"$globals(etcgroup)\""
          }
        }
      }
      incr i
    } else {
      if {[llength $line] != 0} {
        Error warning "Ignoring line in \"$globals(function2group)\" file... line is\n$line"
      }
    }
  }
  close $F2G
  set globals(numfunctions) $i

  # the variable globals(etcgroups) contains the names of all groups referenced
}

######################################################################
#                           User2Function
#
#  This procedure parses up the information in the user2function file.
#
######################################################################
proc User2Function {} {
  global globals ufunction user gfunction

  if {[file writable $globals(user2function)] == 0} {
    Error fatal "Cannot update the file \"$globals(user2function)\""
  }

  exec sort -o $globals(user2function) $globals(user2function)
  set U2F [open $globals(user2function) r]

  set i 0
  while {[gets $U2F line] != -1} {
    if {[llength $line] == 2} {
      set user($i) [lindex $line 0]
      set ufunction($i) [lindex $line 1]

      # check if these are all valid functions
      foreach k $ufunction($i) {
        set validfunction 0
        for {set j 0} {$j < $globals(numfunctions)} {incr j} {
          if {$k == $gfunction($j)} {
            set validfunction 1
            break
          }
        }
      }
      if {$validfunction == 0} {
        Error warning "Ignoring unknown function $k found in \"$globals(user2function)\""
      }
      incr i
    } else {
      if {[llength $line] != 0} {
        Error warning "Ignoring line in \"$globals(user2function)\" file... line is\n$line"
      }
    }
  }
  close $U2F

  set globals(numusers) $i
}

######################################################################
#                           ShowUser
#
#  This procedure adds one user (passed by the username argument) to the
#  bottom of the scrollable list of users. 
#
######################################################################
proc ShowUser { username grouplist userfunctions } {
  global globals gfunction

  frame .u.sers.u$username -bd 2 -relief sunken
    frame .u.sers.u$username.func
      label .u.sers.u$username.func.name -text $username -width 10 -anchor w -bg white -bd 2 -relief sunken
      pack .u.sers.u$username.func.name -side left -ipadx 5

      label .u.sers.u$username.func.label -text "Functions: "
      label .u.sers.u$username.func.functions -text $userfunctions -bd 2 -relief sunken 
      pack .u.sers.u$username.func.label .u.sers.u$username.func.functions -side left
    pack .u.sers.u$username.func -anchor w -ipady 3

    frame .u.sers.u$username.group
#      label .u.sers.u$username.group.name -text "groups:" 
#      pack .u.sers.u$username.group.name -side left -ipadx 5

      foreach i $globals(etcgroups) {
        # this is necessary so re-displays do not unset all previous sets
        set globals($username$i) 0

        checkbutton .u.sers.u$username.group.g$i -text $i -variable globals($username$i)
        pack .u.sers.u$username.group.g$i -side left -padx 5

        if {[lsearch -exact $grouplist $i] != -1} {
          .u.sers.u$username.group.g$i invoke
        }
      }
    pack .u.sers.u$username.group -ipady 3 -anchor w
  pack .u.sers.u$username -ipady 2
}

######################################################################
#                           Restore
#
#  This procedure sets all the group access back to those defined by
#  the function2group and user2functions tables.  It accomplishes this
#  by deleting the CUSTOMCHANGES and LOCKFILE files.
#
######################################################################
proc Restore {} {
  global globals

  foreach win [winfo children .u.sers] {
    destroy $win
  }
  # delete the lock file since we are restoring to original state
  exec rm -f $globals(LOCKFILE)
  exec rm -f $globals(CUSTOMCHANGES)
  exec touch $globals(CUSTOMCHANGES)

  ConfigureUsers
}

######################################################################
#                           Edit
#
#  Edit is the callback for the two edit buttons on the main screen.
#  This brings up a cheesy interface to allow input to function2group and
#  and user2function.  The "type" parameter is either f2g or u2f.
#
######################################################################
proc Edit { type file } {
  global globals

  exec touch $file
  if {[file writable $file] == 1} {
    if {[winfo exists .edit$type] == 1} {
      catch "destroy .edit$type"
    }

    # create cheesy interface to edit the file
    toplevel .edit$type
    wm title .edit$type "$globals(title) - Update $file"
    wm minsize .edit$type 0 0

    label .edit$type.label -text "$globals(title) - Update $file" -bg orange -font  -Adobe-Helvetica-Medium-R-Normal--*-180-*
    pack .edit$type.label -pady 10

    frame .edit$type.bot
      button .edit$type.bot.add -text "Add Entry" -command "AddScreen $type $file"
      menubutton .edit$type.bot.del -text "Delete Entry"  -bd 2 -relief raised -menu .edit$type.bot.del.menu
        menu .edit$type.bot.del.menu
      button .edit$type.bot.save -text Save -command "WriteFile $type $file; destroy .edit$type"
      button .edit$type.bot.quit -text Quit -command "destroy .edit$type" -cursor pirate
      pack .edit$type.bot.add .edit$type.bot.del .edit$type.bot.save .edit$type.bot.quit -side left -padx 10 -fill x
    pack .edit$type.bot -ipady 10 -side bottom

    # read the file and add a line on the screen for each line of the file
    scrollbar .edit$type.yscroll -command ".edit$type.item yview" -relief sunken -orient vert
    pack .edit$type.yscroll -side right -fill y
    canvas .edit$type.item  -yscroll ".edit$type.yscroll set" 
      frame .edit$type.item.f  -bd 2 -relief sunken
        set FILE [open $file r]
        set i 0
        while {[gets $FILE line] != -1} {
          frame .edit$type.item.f.line$i
            entry .edit$type.item.f.line$i.label -width 10 -selectbackground bisque -selectborder 0
            entry .edit$type.item.f.line$i.entry -bd 2 -relief sunken -bg white -width 66  -selectbackground white -selectborder 0
            .edit$type.item.f.line$i.label insert 0 [lindex $line 0]
            .edit$type.bot.del.menu add command -label [lindex $line 0] -command "wm withdraw .edit$type; WriteFile $type $file delete [lindex $line 0]; destroy .edit$type"
            .edit$type.item.f.line$i.label configure -state disabled
            .edit$type.item.f.line$i.entry insert 0 [lindex $line 1]
            bind .edit$type.item.f.line$i.entry <Enter> "focus .edit$type.item.f.line$i.entry"
            bind .edit$type.item.f.line$i.entry <Return> "focus .edit$type.item.f.line$i.entry"
            pack .edit$type.item.f.line$i.label .edit$type.item.f.line$i.entry -side left -ipadx 10
          pack .edit$type.item.f.line$i -ipady 3
          incr i
        }
        close $FILE
        set globals(number_of_$type) $i 
      .edit$type.item create window 0 0 -anchor nw -window .edit$type.item.f
    pack .edit$type.item -fill both -expand true

    # flush event queue
    update idletasks

    # set the canvas size
    .edit$type.item configure -width [winfo reqwidth .edit$type.item.f] -scrollregion "0 0 [winfo reqwidth .edit$type.item.f] [winfo reqheight .edit$type.item.f]"
    .edit$type.yscroll set [winfo reqheight .edit$type.item.f] [winfo height .edit$type.item] 0 [winfo height .edit$type.item]

  } else {
    Error warning "Cannot open \"$file\" for writing... probably incorrect permissions set"
  } 

}

######################################################################
#                           AddScreen
#
#  AddScreen pops up a simple dialog box that allows the input of a new user
#  into user2function, or a new function into function2group.  AddScreen gets
#  called by the "Add Entry" button on the "Edit <user2function | function2group>"
#  screen.  
#
#  AddScreen calls AddEntry when the "OK to Add" button is pressed.
#
######################################################################
proc AddScreen { type file } {
  global globals

  if {[winfo exists .add$type] == 1} {
    catch "destroy .add$type"
  }
  toplevel .add$type
  wm title .add$type "$globals(title) - Add $type"

  if {$type == "u2f"} {
    set realtype User
  } else {
    set realtype Function
  }

  label .add$type.label -text "$globals(title) - Add $realtype" -bg orange -font -Adobe-Helvetica-Medium-R-Normal--*-180-*
  pack .add$type.label -pady 10

  frame .add$type.l
    label .add$type.l.variable -text $realtype -width 10
    label .add$type.l.value -text "Value" -width 50
    pack .add$type.l.variable .add$type.l.value -side left -padx 10 -fill x
  pack .add$type.l

  frame .add$type.f
    entry .add$type.f.variable -bg white -bd 2 -relief sunken -width 10  -selectbackground white -selectborder 0
    bind .add$type.f.variable <Enter> "focus .add$type.f.variable"
    bind .add$type.f.variable <Return> "focus .add$type.f.value"
    entry .add$type.f.value -bg white -bd 2 -relief sunken -width 50 -selectbackground white -selectborder 0
    bind .add$type.f.value <Enter> "focus .add$type.f.value"
    bind .add$type.f.value <Return> ".add$type.bot.add invoke"
    pack .add$type.f.variable .add$type.f.value -side left -padx 10 -fill x 
  pack .add$type.f

  frame .add$type.bot
    button .add$type.bot.add -text "OK to Add" -command "wm withdraw .add$type;WriteFile $type $file add; destroy .add$type; destroy .edit$type"

    button .add$type.bot.quit -text Quit -command "destroy .add$type"
    pack .add$type.bot.add .add$type.bot.quit -side left -padx 10 -fill x
  pack .add$type.bot -ipady 10
}

######################################################################
#                           WriteFile
#
#  WriteFile is the callback of the "Save" button on the "Edit user2function"
#  or "Edit function2group" screens.  It saves the screen information to the
#  appropriate file, and then it forces groupie to re-read its startup files.
#
#  The add_del argument is used only when entries are being added or deleted.
#
######################################################################
proc WriteFile { type file { add_del {} } { item {} } } {
  global globals gfunction group

# commit the changes in the LOCKFILE
  if {[file exists $globals(LOCKFILE)]} {
    exec cat $globals(LOCKFILE) >> $globals(CUSTOMCHANGES)
    exec rm -f $globals(LOCKFILE)
    exec touch $globals(LOCKFILE)
  }

  exec touch $file.new
  if {[file writable $file.new] == 0} {
    Error fatal "Cannot write to the file \"$file.new\""
  }

  set FILE [open $file.new w]

  # update the file... delete an entry, if necessary
  set i 0

  if {$type == "f2g"} {
    # this is for function2group
    # make sure that we handle additions/deletions of groups properly
    while {[winfo exists .edit$type.item.f.line$i.label]} {
      set label [.edit$type.item.f.line$i.label get]
      if {$add_del == "delete" && $item == $label} {
        Error information "\"$item\" deleted from \"$file\""
      } else {
        set entry [.edit$type.item.f.line$i.entry get]
        puts $FILE "$label\t\{$entry\}"

        # determine if this function has the same groups as previously defined
        for {set g 0} {$g < $globals(numfunctions)} {incr g} {
          if {$gfunction($g) == $label} {
            foreach func $entry {
              if {[lsearch -exact $group($g) $func] == -1} {
                # new function $func added to function gfunction($g)
                ToggleAllUsers $gfunction($g) $func add
              }
            }
            foreach func $group($g) {
              if {[lsearch -exact $entry $func] == -1} {
                # function $func removed from function gfunction($g)
                ToggleAllUsers $gfunction($g) $func del
              }
            }
            break
          }
        }
      }
      incr i
    }
  } else {
    # this is for user2function
    while {[winfo exists .edit$type.item.f.line$i.label]} {
      if {$add_del == "delete" && $item == [.edit$type.item.f.line$i.label get]} {
        Error information "\"$item\" deleted from \"$file\""
      } else {
        puts $FILE "[.edit$type.item.f.line$i.label get]\t\{[.edit$type.item.f.line$i.entry get]\}"
      }
      incr i
    }
  }

  # add new entry, if necessary
  if {$add_del == "add"} {
    puts $FILE "[.add$type.f.variable get]\t\{[.add$type.f.value get]\}"
  }

  close $FILE

  exec cp $file $file.bak
  exec mv $file.new $file
  
  Error information "Done updating \"$file\""

  # remove all the users from the main screen
  foreach win [winfo children .u.sers] {
    catch "destroy $win"
  }

  # re-read config files and remap the users on the main screen
  StartUp

}

######################################################################
#                           ToggleAllUsers
#
#  This procedure gets invoked when any function changes its groups.
#  It cycles through all the users, and if any user has the function name
#  passed, then that user no longer gets toggled from the custom changes
#  file.
#
######################################################################
proc ToggleAllUsers { function groupname add_del} {
  global globals ufunction user gfunction group
  for {set i 0} {$i < $globals(numusers)} {incr i} {
    if {[lsearch -exact $ufunction($i) $function] != -1} {

      # Wipe out the invoke command(s) from CUSTOMCHANGES file for the user.
      # Also, if the group is deleted and the user would have access via another 
      # group, then retain the invoke command in the CUSTOMCHANGES file.

      set NEWFILE [open "$globals(CUSTOMCHANGES).new" w]
      set FILE [open $globals(CUSTOMCHANGES) r]

      while {[gets $FILE line] != -1} {
        if {[string match .u.sers.u$user($i).group.g$groupname [lindex $line 0]] == 0} {
          puts $NEWFILE $line
        } else {
          if {$add_del == "del"} {
            # check if the user has other functions would have given him access
            # because that would mean that this group is really off-limits
            foreach func $ufunction($i) {
              set found 0
              if {$func != $function} {
                for {set f 0} {$f < $globals(numfunctions)} {incr f} {
                  if {$func == $gfunction($f)} {
                    if {[lsearch -exact $group($f) $groupname] != -1} {
                      set found 1
                      break
                    }
                  }
                }
                if {$found == 1} {
                  puts $NEWFILE $line
                  break
                }
              }
            }
          } else {
            # this is for add_del = "add"

            # If the user has been denied access to this group, then this
            # procedure should preserve that denial.  If the user has been
            # granted access to this group already, then he'll have it via
            # the normal function/group/user cross reference.
            foreach func $ufunction($i) {
              set found 0
              for {set f 0} {$f < $globals(numfunctions)} {incr f} {
                if {$func == $gfunction($f)} {
                  if {[lsearch -exact $group($f) $groupname] != -1} {
                    set found 1
                    break
                  }
                }
              }
              if {$found == 1} {
                puts $NEWFILE $line
                break
              }
            }
          }
        }
      } 
      close $FILE
      close $NEWFILE

      exec mv $globals(CUSTOMCHANGES) $globals(CUSTOMCHANGES).bak
      exec mv $globals(CUSTOMCHANGES).new $globals(CUSTOMCHANGES)
    }
  }
}

######################################################################
#                           HandleChange
#
#  HandleChange is the callback for the checkbuttons.  It toggles the
#  foreground between black and orange, and it puts a "checkbutton invoke"
#  command in the LOCKFILE, if it exists.
#
######################################################################
proc HandleChange { user group } {
  global globals

  # make an entry in the LOCKFILE if it exists... it does not exist until after
  # the custom configuration file is processed at startup time
  if {[file exists $globals(LOCKFILE)] ==1 } {
    exec echo ".u.sers.u$user.group.g$group invoke" >> $globals(LOCKFILE)
  }

  # toggle the foreground color between orange and black
  if {[lindex [.u.sers.u$user.group.g$group configure -fg] 4] == "orange"} {
    .u.sers.u$user.group.g$group configure -fg black -activeforeground black
  } else {
    .u.sers.u$user.group.g$group configure -fg orange -activeforeground orange
  }
}

######################################################################
#                           MainScreen
#
#  MainScreen creates the label at the top and the buttons on the bottom
#  of the main screen.  It also creates an empty frame that eventually
#  gets filled up by ShowUsers.
#
######################################################################
proc MainScreen {} {
  global globals

  frame .top
    label .top.label -text "$globals(title) - Version 1.1" -bg orange -font -Adobe-Helvetica-Medium-R-Normal--*-180-*
    pack .top.label -fill x
  pack .top -pady 10

  frame .bot 
    button .bot.restore -text "Remove Custom Changes" -command Restore
    button .bot.f2g -text "Edit Function2Group" -command "Edit f2g $globals(function2group)"
    button .bot.u2f -text "Edit User2Function" -command "Edit u2f $globals(user2function)"
    button .bot.save -text "Update $globals(etcgroup)" -command UpdateEtcGroup
    button .bot.quit -text Quit -cursor pirate -command QuitDialog
    pack .bot.restore .bot.f2g .bot.u2f .bot.save .bot.quit -padx 7 -side left -fill x
  pack .bot -side bottom -ipady 10
    
  scrollbar .yscroll -command ".u yview" -relief sunken -orient vert
  pack .yscroll -fill y -side right

  canvas .u -bd 2 -relief sunken -yscroll ".yscroll set" -xscroll ".xscroll set" 
  pack .u -side top -expand true -fill both

  scrollbar .xscroll -command ".u xview" -relief sunken -orient horiz
  pack .xscroll -fill x -side bottom

  frame .u.sers
  .u create window 0 0 -anchor nw -window .u.sers

}

######################################################################
#                           QuitDialog
#
#  This is the callback of the Quit button on the main screen.  It checks
#  for any uncommitted changes and it gives the option of committing them
#  before exiting.
#
######################################################################
proc QuitDialog {} {
  global globals

  if {[file exists $globals(LOCKFILE)] == 1} {
    if {[file size $globals(LOCKFILE)] > 0} {
      # yep, there are uncommitted changes... bring up the dialog box
      if {[winfo exists .quit] == 1} {
        catch "destroy .quit"
      }
      toplevel .quit
      wm title .quit "$globals(title) - Confirm Quit"

      label .quit.label -text "There are Uncommitted Changes" -bg bisque2 -font -Adobe-Helvetica-Bold-R-Normal--*-180-*
      pack .quit.label -pady 10

      frame .quit.bot
        button .quit.bot.quit -text "Quit Anyway" -command "exec rm -f $globals(LOCKFILE); destroy .; exit"
        button .quit.bot.commit -text "Commit Changes, then Quit" -command "exec cat $globals(LOCKFILE) >> $globals(CUSTOMCHANGES); .quit.bot.quit invoke"
        button .quit.bot.cancel -text "Cancel" -command "destroy .quit"
        pack .quit.bot.quit .quit.bot.commit .quit.bot.cancel -side left -padx 10 -fill x
      pack .quit.bot -ipady 10
    } else {
      exec rm -f $globals(LOCKFILE)
      destroy .
      exit
    }
  } else {
    destroy .
    exit
  }
}
######################################################################
#                           CustomChanges
#
#  This procedure is invoked after the users are displayed.  If a custom
#  changes file exists, then it runs the commands (i.e. checkbutton invoke)  
#  in the file.
#
######################################################################
proc CustomChanges {} {
  global globals

  if {[file exists $globals(CUSTOMCHANGES)] == 1} {
    set FILE [open $globals(CUSTOMCHANGES) r]
    while {[gets $FILE line] != -1} {
      if {[catch "eval $line"] == 1} {
        Error warning "Cannot execute the custom change command from file \"$globals(CUSTOMCHANGES)\"\nCommand is \"$line\"\nSelect the \"Update /etc/group\" button to clear the problem."
      }
    }
    close $FILE
  }
}

######################################################################
#                           UpdateEtcGroup
#
#  This procedure creates a new /etc/group (or temporary group file) file
#  based on the information displayed on the screen.  It is invoked by the
#  "Write /etc/group File" on the main screen.
#
#  This procedure also updates the CUSTOMCHANGES file.
#
######################################################################
proc UpdateEtcGroup {} {
  global globals user

# NEVER MIND THIS COMMENT BLOCK... THIS IS THE OLD WAY TO DO IT
#  # append the lock file (which contains the new custom changes) to the 
#  # existing "custom changes", if any
#  exec touch $globals(CUSTOMCHANGES)
#  exec touch $globals(LOCKFILE)
#  exec cat $globals(LOCKFILE) >> $globals(CUSTOMCHANGES)
#  exec sort -o $globals(CUSTOMCHANGES) $globals(CUSTOMCHANGES)
#
#  # scan the CUSTOMCHANGES file and delete duplicate entries two by two
#  # (i.e.  if there are three indentical "invoke" commands, then delete two
#  #  of them... if there are five, then delete four.)
#  set CUSTFILE [open $globals(CUSTOMCHANGES) r]
#  set NEWCUST [open $globals(CUSTOMCHANGES).new w]
#  set previous_line ""
#  while {[gets $CUSTFILE line] != -1} {
#    if {$line == $previous_line} {
#      set previous_line ""
#    } else {
#      if {$previous_line != ""} {
#        puts $NEWCUST $previous_line
#      }
#      set previous_line $line
#    }
#  }
#  if {$previous_line != ""} {
#    puts $NEWCUST $previous_line
#  }
#  close $CUSTFILE
#  close $NEWCUST
#  exec mv -f $globals(CUSTOMCHANGES) $globals(CUSTOMCHANGES).bak
#  exec mv -f $globals(CUSTOMCHANGES).new $globals(CUSTOMCHANGES)
#  # done updating CUSTOMCHANGES file
#


  # create the new CUSTOMCHANGES file by noting all checkbuttons that have
  # an orange foreground color (i.e. highlighted)
  set FILE [open $globals(CUSTOMCHANGES).new w]
  for {set j 0} {$j < $globals(numusers)} {incr j} {
    foreach g $globals(etcgroups) {
      if {[string match [lindex [.u.sers.u$user($j).group.g$g configure -fg] 4] "orange"] == 1} {
        puts $FILE ".u.sers.u$user($j).group.g$g invoke"
      }
    }
  }
  close $FILE      
  exec mv -f $globals(CUSTOMCHANGES) $globals(CUSTOMCHANGES).bak
  exec mv -f $globals(CUSTOMCHANGES).new $globals(CUSTOMCHANGES)
  # done updating CUSTOMCHANGES file

  # open the group file and add users to appropriate groups
  set GROUP [open $globals(etcgroup) r]
  set NEWGROUP [open $globals(etcgroup).new w]
  while {[gets $GROUP line] != -1} {
    set newline $line
    set gname [lindex [split $line :] 0]
    set foundgroup 0
    foreach i $globals(etcgroups) {
      if {$i == $gname} {
        set foundgroup 1
        # modify this group file entry to include all appropriate users
        set newline "$gname:[lindex [split $line :] 1]:[lindex [split $line :] 2]:"
        set firstuser 0
        for {set j 0} {$j < $globals(numusers)} {incr j} {
          if {$globals($user($j)$gname) == 1} {
            if {$firstuser == 0} {
              set firstuser 1
              append newline $user($j)
            } else {
              append newline ",$user($j)"
            }
          }
        }
#        append newline ":"
      }
    }
    puts $NEWGROUP $newline
  }
  close $GROUP
  close $NEWGROUP
  exec mv -f $globals(etcgroup) $globals(etcgroup).bak
  exec mv -f $globals(etcgroup).new $globals(etcgroup)

  # delete the lock file since we are writing the changes to /etc/group
  exec rm -f $globals(LOCKFILE)

  Error information "Done updating \"$globals(etcgroup)\" file"
}

######################################################################
#                           ConfigureUsers
#
#  This procedure cycles through all the users, determines which groups
#  they belong to (based on function2group and user2function files), and
#  then calls ShowUser to display the user on the main screen.
#
######################################################################
proc ConfigureUsers {} {
  global globals gfunction group ufunction user

  for {set j 0} { $j < $globals(numusers) } {incr j} {

    # groupnames are the /etc/group entries that the user will be a member of
    set groupnames {}

    for { set i 0 } { $i < $globals(numfunctions)} {incr i} {
      # cycle through all the user's job functions
      foreach userfunc $ufunction($j) {
        if {$gfunction($i) == $userfunc} {
           # test if /etc/group entries for this job function are already defined
           foreach entry $group($i) {
             if {[string match "* $entry*" $groupnames] == 0} {
               # add this /etc/group entry to the list
               append groupnames "$entry "
             }
           }
           # go to the user's next job function
           break
        }
      }
    }
    # show this user on the main screen
    ShowUser $user($j) $groupnames $ufunction($j)

    foreach g $globals(etcgroups) {
      .u.sers.u$user($j).group.g$g configure -command "HandleChange $user($j) $g"
    }
  }
}

######################################################################
#                           Error
#
#  Exception handling routine.  It distinguishes messages based on level
#  (i.e. severity level) and it combines all similar-level messages into
#  one error box.  In other words, if two "warning" messages are noted,
#  then this procedure will bring up one warning box with both messages
#  included.
#
#  If the severity level is fatal, then Error causes the program to exit.
#
#  This should be called something other than "Error", but I'm too lazy
#  to think of a better name.
#
######################################################################
proc Error { level message } {
  global globals

  set LEVEL [string toupper $level]

  if {[winfo exists .message$level] == 0} {
    toplevel .message$level
    wm title .message$level "$globals(title) - $LEVEL Message"

      message .message$level.text -aspect 1000 -font -Adobe-Helvetica-Bold-R-Normal--*-180-* -text $message
      button .message$level.dismiss -cursor pirate -command "destroy .message$level" -text Dismiss
      pack .message$level.text .message$level.dismiss -ipady 10
 
    if {$level == "fatal"} {
      .message$level.dismiss configure -command "destroy .message$level; destroy .;exit 1"
      wm withdraw .
      tkwait visibility .message$level
      grab .message$level
      tkwait window .message$level
    }    
  } else {
    set wholemessage "[lindex [.message$level.text configure -text] 4]\n\n$message"
    .message$level.text configure -text $wholemessage
  }
}

######################################################################
#                           StartUp
#
#  StartUp calls procedures to read in the two cross-reference files,
#  cycles through the users (displaying them on the screen), and 
#  sizing and calibrating the windows and scrollbars.
#
######################################################################
proc StartUp {} {
global globals 

# read in function2group database file
Function2Group

# read in user2function database file
User2Function

# display all users and the groups that they belong to (this calls ShowUser)
ConfigureUsers

# this sets/unsets group access for particular users (i.e. custom changes)
CustomChanges

# create the LOCKFILE... notice that this affects the HandleChange procedure
catch "exec rm -rf $globals(LOCKFILE)"
exec touch $globals(LOCKFILE)
# nobody else can run the program now that the LOCKFILE exists

# make sure we don't ask for a screen bigger than the whole display

# flush the event queue before we start calculating screen sizes
update idletasks

# calculate the height... leave room for the header and footer stuff
set screenheight [expr [winfo vrootheight .]-150]

set maxheight [winfo reqheight .u.sers]
if {$maxheight > $screenheight} {
  set height $screenheight
  Error information "trimming height $maxheight to $screenheight"
} else {
  set height $maxheight
}

# calculate the width... leave room for the scrollbar and window decorations 
set screenwidth [expr [winfo vrootwidth .]-40]
set maxwidth [winfo reqwidth .u.sers]
if {$maxwidth > $screenwidth} {
  set width $screenwidth
  Error information "trimming width $maxwidth to $screenwidth"
} else {
  set width $maxwidth
}

if {$globals(numusers) != 0} {
  # set the size of the scrolling area, and set the scroll increment
  .u configure -height $height -width $width -scrollregion "0 0 $maxwidth $maxheight" -scrollincrement [expr $maxheight/$globals(numusers)]

  # calibrate the scrollbars
  .xscroll set $maxwidth $width 0 $width
  .yscroll set $maxheight $height 0 $height

  # flush queue to draw the new size(s) and update calibrations
  update idletasks
  }
}

######################################################################
#                           CreateInfo
#
#  CreateInfo is the procedure that creates the ".groupie" startup
#  file in the $HOME directory.
#
######################################################################
proc CreateInfo { {sample {} } } {
  global globals env

  set FILE [open $env(HOME)/.groupie w]

  # if the sample button was pressed, then load in the sample config files
  if {$sample == "sample"} {
    .info.f.group.path delete 0 end
    .info.f.group.path insert 0 SampleFiles/group
    .info.f.f2g.path delete 0 end
    .info.f.f2g.path insert 0 SampleFiles/function2group
    .info.f.u2f.path delete 0 end
    .info.f.u2f.path insert 0 SampleFiles/user2function
    .info.f.cust.path delete 0 end
    .info.f.cust.path insert 0 SampleFiles/groupie.custom
  }

  set globals(etcgroup) [.info.f.group.path get]
  puts $FILE "set globals(etcgroup) [.info.f.group.path get]"
  puts $FILE "set globals(function2group) [.info.f.f2g.path get]"
  puts $FILE "set globals(user2function) [.info.f.u2f.path get]"
  puts $FILE "set globals(CUSTOMCHANGES) [.info.f.cust.path get]"

  close $FILE

  # by setting this variable, the main program can continue
  # because the "tkwait" statement will be satisfied
  set globals(ok_to_proceed) 1
}

######################################################################
#                           ReadDotFile
#
#  ReadDotFile reads and executes the commands in the .groupie file that
#  exists (maybe) on the $HOME directory.  This procedure will whine if
#  the .groupie contains information that is not useable.
#
######################################################################
proc ReadDotFile {} {
  global globals env

  # try executing each line the in the .groupie file
  set FILE [open $env(HOME)/.groupie r]
  while {[gets $FILE line] != -1} {
    if {[catch "eval $line"] == 1} {
      Error fatal "The \"$env(HOME).groupie\" file is corrupt.  Please delete it and restart groupie"
    }
  }
  close $FILE

  # make sure that each of the files specified is writable... don't bother
  # whining if the filenames have not been set yet
  if {$globals(etcgroup) != "NotSetYet"} {
    catch "exec touch $globals(etcgroup)"
    if {[file writable $globals(etcgroup)] == 0} {
      Error warning "Cannot update group file \"$globals(etcgroup)\""
      set globals(etcgroup) NotSetYet
    }
  }
  if {$globals(function2group) != "NotSetYet"} {
    catch "exec touch $globals(function2group)"
    if {[file writable $globals(function2group)] == 0} {
      Error warning "Cannot update function2group file \"$globals(function2group)\""
      set globals(function2group) NotSetYet
    }
  }
  if {$globals(user2function) != "NotSetYet"} {
    catch "exec touch $globals(user2function)"
    if {[file writable $globals(user2function)] == 0} {
      Error warning "Cannot update user2function file \"$globals(user2function)\""
      set globals(user2function) NotSetYet
    } 
  }
  if {$globals(CUSTOMCHANGES) != "NotSetYet"} {
    catch "exec touch $globals(CUSTOMCHANGES)"
    if {[file writable $globals(CUSTOMCHANGES)] == 0} {
      Error warning "Cannot update custom changes file \"$globals(CUSTOMCHANGES)\""
      set globals(CUSTOMCHANGES) NotSetYet
    }
  }
}

######################################################################
#                           main program
#
#  The main programs starts here.  All procedures are defined above this
#  comment block.
#
######################################################################
global env globals user group ufunction gfunction
set globals(title) groupie
wm title . $globals(title)
wm minsize . 0 0

# default values that are guaranteed to fail
set globals(etcgroup) NotSetYet
set globals(function2group) NotSetYet
set globals(user2function) NotSetYet
set globals(CUSTOMCHANGES) NotSetYet

# check if the user has run groupie before
if {[file exists $env(HOME)/.groupie] == 1} {
  ReadDotFile
} else {
  exec touch $env(HOME)/.groupie
}

# if any of the configuration files are not set yet, then bring up an
# information-gathering screen
while {$globals(etcgroup) == "NotSetYet" || $globals(function2group) == "NotSetYet" || $globals(user2function) == "NotSetYet" || $globals(CUSTOMCHANGES) == "NotSetYet" } {
  frame .info
    label .info.label -text "$globals(title) - Get Info" -font -Adobe-Helvetica-Medium-R-Normal--*-180-* -bg orange
    pack .info.label -pady 10

    frame .info.f -bd 2 -relief sunken
      frame .info.f.labels
        label .info.f.labels.file -text "Configuration File" -width 20 -bg bisque2
        label .info.f.labels.path -text "Path to Configuration File" -width 50 -bg bisque2
        pack .info.f.labels.file .info.f.labels.path -padx 5 -side left
      pack .info.f.labels -ipady 5
      frame .info.f.group
        label .info.f.group.file -text "group file" -width 20
        entry .info.f.group.path -width 50 -bg white -bd 2 -relief sunken -selectbackground white -selectborder 0
        bind .info.f.group.path <Enter> "focus .info.f.group.path"
        bind .info.f.group.path <Return> "focus .info.f.f2g.path"
        .info.f.group.path insert 0 $globals(etcgroup)
        pack .info.f.group.file .info.f.group.path -padx 5 -side left
      pack .info.f.group -ipady 5
      frame .info.f.f2g
        label .info.f.f2g.file -text "function2group file" -width 20
        entry .info.f.f2g.path -width 50 -bg white -bd 2 -relief sunken -selectbackground white -selectborder 0
        bind .info.f.f2g.path <Enter> "focus .info.f.f2g.path"
        bind .info.f.f2g.path <Return> "focus .info.f.u2f.path"
        .info.f.f2g.path insert 0 $globals(function2group)
        pack .info.f.f2g.file .info.f.f2g.path -padx 5 -side left
      pack .info.f.f2g -ipady 5
      frame .info.f.u2f
        label .info.f.u2f.file -text "user2function file" -width 20
        entry .info.f.u2f.path -width 50 -bg white -bd 2 -relief sunken -selectbackground white -selectborder 0
        bind .info.f.u2f.path <Enter> "focus .info.f.u2f.path"
        bind .info.f.u2f.path <Return> "focus .info.f.cust.path"
        .info.f.u2f.path insert 0 $globals(user2function)
        pack .info.f.u2f.file .info.f.u2f.path -padx 5 -side left
      pack .info.f.u2f -ipady 5
      frame .info.f.cust
        label .info.f.cust.file -text "custom changes file" -width 20
        entry .info.f.cust.path -width 50 -bg white -bd 2 -relief sunken -selectbackground white -selectborder 0
        bind .info.f.cust.path <Enter> "focus .info.f.cust.path"
        bind .info.f.cust.path <Return> ".info.bot.ok invoke"
        .info.f.cust.path insert 0 $globals(CUSTOMCHANGES)
        pack .info.f.cust.file .info.f.cust.path -padx 5 -side left
      pack .info.f.cust -ipady 5

      frame .info.f.message
        label .info.f.message.dummy -text "" -width 20
        label .info.f.message.l -text "NOTE:  Absolute pathnames are HIGHLY recommended" -bg orange -width 50
        pack .info.f.message.dummy .info.f.message.l -padx 5 -side left
      pack .info.f.message

    pack .info.f -ipady 10


    frame .info.bot
      button .info.bot.ok -text "OK" -command "CreateInfo"
      button .info.bot.sample -text "Use Sample Files" -command "CreateInfo sample"
      button .info.bot.quit -text "Cancel" -command "destroy .;exit" -cursor pirate
      pack .info.bot.ok .info.bot.sample .info.bot.quit -side left -padx 10 
    pack .info.bot -ipady 5
  pack .info


  update idletasks
  tkwait variable globals(ok_to_proceed)
  set globals(ok_to_proceed) 0
  catch "destroy .info"
  ReadDotFile
}



# the LOCKFILE ensures that only one instance of this program is running
set globals(LOCKFILE) /tmp/$globals(title).LOCK
if {[file exists $globals(LOCKFILE)] == 1} {
  Error fatal "$globals(LOCKFILE) exists.  Delete this file and try again\nif you are positive that nobody else is running \"$globals(title)\" now."
}


# scan through the /etc/group file (or whatever file was selected) for correctness
set GROUP [open $globals(etcgroup) r]
while {[gets $GROUP line] != -1} {
  set entry [split $line :]

  # whine about group entries that don't have exactly four fields
  if {[llength $entry] != 4} {
    # don't whine about blank lines
    if {[llength $line] != 0} {
      # ignore entries beginning with a plus sign
      if {[lindex $entry 0] != "+"} {
        Error fatal "Cannot understand entry in \"$globals(etcgroup)\" file... entry is\n$line"
      }
    }
  }
}
close $GROUP
# if we made it this far, then the group file must be OK

# draw main screen sans users (they come later, via ShowUser)
MainScreen

# read config files and map the users on the main screen
StartUp
