[comp.os.vms] Managing cluster users with identifiers.

CHRIS@YMIR.BITNET (Chris Yoder) (04/26/88)

[bug food]

     Well, here I go again, trying to submit to INFO-VAX from BITNET.  I'm
beginning to think that this is a fruitless exercise because I never see
anything come back...

     Included is NODECHECK, a utility that allows cluster managers to control
who logs onto what nodes in the cluster by means of identifiers.  Have fun, and
send me any comments or bug fixes.

-- Chris Yoder
     Harvey Mudd College        Bitnet ----- Chris@Ymir.Bitnet

$!---------------------------- Cut here ----------------------------------------
$ show default
$ check_sum = 958622119
$ write sys$output "Creating 000README.1ST"
$ create 000README.1ST
$ DECK/DOLLARS="$*$*EOD*$*$"
     NODECHECK is a utility which gives VAX Cluster managers the ability to
allow and disallow the users of that cluster to log into a given node.
NODECHECK is entirely driven by the identifiers that the user holds and needs
no privileges.  Some system setup is required.

     NODECHECK unconditionally allows users onto a node if they hold an
identifier of the form ``<node>_ACCESS'' where the string <node> is equal to
the SYSGEN parameter SCSNODE (this should also be the DECnet node name).  It
will conditionally allow users holding an identifier in the list of identifiers
specified by the /ALLOW qualifier to log in.  Finally, users holding
identifiers in the list of identifiers specified by the /DISALLOW qualifier
will not be allowed to log in.

     The /ALLOW and /DISALLOW qualifiers are conditional in that the user is
allowed to log in if they have an identifier on the /ALLOW list and they don't
have an identifier on the /DISALLOW list.  If they hold an identifier on both
lists, then the conflict is resolved by whether the user holds the
``<node>_ACCESS'' identifier.  The precedence of identifiers is:

1) Users holding ``<node>_ACCESS'' identifiers always get on.

2) Users only holding one or more identifiers on the /ALLOW list get on iff
        they do not hold any identifiers on the /DISALLOW list.

3) Users only holding one or more identifiers on the /DISALLOW list are not
        allowed to log on.

4) Users who do not hold either the ``<node>_ACCESS'' identifier or an
        identifier on the /ALLOW list are not allowed to log on.


SYSTEM SETUP: (The utterly simplified version.)

1) PAScal and LINk NODE_ACCESS

2) Move NODE_ACCESS.EXE to wherever you want it to live.

        (I use the system logical PRIV: to point to a directory where all
        system-type public-domain and locally written software live).

3) Edit NODECHECK.CLD so that the image line of the verb NODECHECK points to
        that directory.

4) Add the NODECHECK CLD file into the appropriate DCLTABLES.

        (In an environment where all systems share the same DCLTABLES, the
        command for this is:

        $ SET COMMAND /TABLE=SYS$LIBRARY:DCLTABLES -
                /OUTPUT=SYS$COMMON:[SYSLIB]DCLTABLES NODECHECK
        )

5) Reinstall DCLTABLES on each node in the cluster.

        (On each node execute the command:

        $ MCR INSTALL
        INSTALL> SYS$LIBRARY:DCLTABLES/REPLACE
        )

6) Add the appropriate NODECHECK command to SYLOGIN.COM.


EXAMPLES:

1)   Say Joe Random Prof owned a workstation named RANDOM and he didn't want
        any STUDENTS to log on to his workstation but would allow all
        SYSTEM_MANAGEMENT and FACULTY on.  Assuming that all students hold the
        identifier STUDENTS, all faculty hold the identifier FACULTY, and all
        system management types hold the identifier SYSTEM_MANAGEMENT, the
        command in SYLOGIN.COM would be:

$ NODECHECK/OWNER="Joe Random Prof"/ALLOW=(SYSTEM_MANAGEMENT,FACULTY)-
        /DISALLOW=STUDENTS

     If user held both the STUDENT and SYSTEM_MANAGEMENT identifier, but not
        the RANDOM_ACCESS identifier, that student could not log on.

2)   If it is desired to allow most users onto the system and only exclude
        those who hold identifiers on the /DISALLOW list from a given node,
        then the system manager may grant an identifier to all users, say
        "USER" and include the USER identifier in the /ALLOW list.  The above
        command would change to:

$ NODECHECK/OWNER="Joe Random Prof"/ALLOW=(SYSTEM_MANAGEMENT,FACULTY,USER)-
        /DISALLOW=STUDENTS

     This would allow all users who do not hold the STUDENT identifier to log
        onto the node RANDOM.  Of course, users with the STUDENT identifier who
        also hold the RANDOM_ACCESS identifier are allowed to log in.
$*$*EOD*$*$
$ checksum 000README.1ST
$ if checksum$checksum .ne. check_sum then -
$   write sys$output "Checksum failed, file probably corrupted"
$ check_sum = 1265006991
$ write sys$output "Creating NODECHECK.CLD"
$ create NODECHECK.CLD
$ DECK/DOLLARS="$*$*EOD*$*$"
!
! Command definition for NODECHECK utility
!
define syntax DEBUG
  image pub$root:[source.nodecheck]node_access
  qualifier OWNER
    value (REQUIRED)
  qualifier ALLOW
    value (LIST,REQUIRED)
  qualifier DISALLOW
    value (LIST,REQUIRED)

define verb NODECHECK
  image priv:node_access
  qualifier DEBUG
    syntax = DEBUG
  qualifier OWNER
    value (REQUIRED)
  qualifier ALLOW
    value (LIST,REQUIRED)
  qualifier DISALLOW
    value (LIST,REQUIRED)
$*$*EOD*$*$
$ checksum NODECHECK.CLD
$ if checksum$checksum .ne. check_sum then -
$   write sys$output "Checksum failed, file probably corrupted"
$ check_sum = 994520805
$ write sys$output "Creating NODE_ACCESS.PAS"
$ create NODE_ACCESS.PAS
$ DECK/DOLLARS="$*$*EOD*$*$"
[Ident ('1.0'),
 Inherit ('SYS$Library:Starlet')]
Program Node_Access (Output);

{
           This program will check to see if the user has the identifier
        <node-name>_ACCESS.  If the user has it, nothing is output.  If the
        user does not have this identifier, then the user is informed that
        he does not have access to the particular node and the process is
        deleted.  This program is intended to be run from SYLOGIN.COM.

                Written by:     Chris Yoder
                        for:    Harvey Mudd College
                                Mathematics Department Computing Laboratory
                        on:     March 14, 1988
}

CONST

{
 CLI constants.
}
        CLI$_ABSENT     = %x'000381F0';
        CLI$_COMMA      = %x'0003FD39';
        CLI$_CONCAT     = %x'0003FD29';
        CLI$_DEFAULTED  = %x'0003FD21';
        CLI$_LOCNEG     = %x'00038230';
        CLI$_LOCPRES    = %x'0003FD31';
        CLI$_NEGATED    = %x'000381F8';
        CLI$_PRESENT    = %x'0003FD19';

TYPE
        Word                    = [WORD] -32768..32767;

        UnsignedWord            = [WORD] 0..65535;

        String80                = Varying[80] of CHAR;

{TYPE Definitions
        Word ------------------- A type to hold a word.
        UnsignedWord ----------- An unsigned word type.
        String80 --------------- A string 80 characters long.
}

VAR
        Node                    : String80;

        Identifier              : String80 := '_ACCESS';

        Owner                   : String80 := 'your system management';

        Explicit_Access         : BOOLEAN := FALSE;

        User_Has_Access         : BOOLEAN := FALSE;

{VAR Definitions
        Node ------------------- The node that we are running on.
        Identifier ------------- The default identifier to check on.
        Owner ------------------ The owner of the machine.
        Explicit_Access -------- True for users with the node_ACCESS identifier.
        User_Has_Access -------- True if the user can log on to this node.
}

{******************************************************************************}

[EXTERNAL, UNBOUND]
FUNCTION CLI$Present (
        EntityDesc : [CLASS_S] Packed Array [ $l1..$u1 : Integer ] Of Char
        ) : Integer;    External;

{******************************************************************************}

[EXTERNAL, UNBOUND]
FUNCTION CLI$Get_Value (
        EntityDesc : [CLASS_S] Packed Array [ $l1..$u1 : Integer ] Of Char;
        RetDesc : [CLASS_S] Packed Array [ $l2..$u2 : Integer ] Of Char;
        VAR RetLength : [UNSAFE] UnsignedWord := %Immed 0
        ) : Integer;    External;

{******************************************************************************}

FUNCTION LIB$Stop
  (%immed   condition_value1 : [LIST, UNSAFE] UNSIGNED )
  : UNSIGNED;

  EXTERNAL;

{******************************************************************************}

FUNCTION LIB$GetJPI
  (         item_code : INTEGER;
   %ref     process_id : UNSIGNED := %immed 0;
            process_name : VARYING [$len3] OF CHAR := %immed 0;
   %ref     out_value : UNSIGNED := %immed 0;
   VAR      out_string : VARYING [$len5] OF CHAR := %immed 0;
   VAR      out_len : Word := %immed 0 )
  : UNSIGNED;

  EXTERNAL;

{******************************************************************************}

Procedure Check (Status_Code : Unsigned);
  {
           This procedure will check the status code returned from a system
        call.  If it is OK, then we continue on, otherwise we signal the error
        and abort the program.

  }

  BEGIN { Procedure Check }
    IF NOT ODD (Status_Code)
      THEN LIB$Stop (Status_Code);
  END; { Procedure Check }

{******************************************************************************}

Function User_Has_Identifier (Identifier : String80) : BOOLEAN;

  TYPE
        Holder_Type     = [QUAD,UNSAFE] RECORD
                            UIC         : Unsigned;
                            Reserved    : Unsigned;
                          END;

  { TYPE Definitions
        Holder_Type --- Put the UIC into a Quadword to pass to $Find_Held
  }

  VAR
        Hold_Result     : Boolean;
        UIC             : Unsigned;
        Holder          : Holder_Type;

        Bin_Id          : [Volatile] Unsigned;

        Check_Id        : [Volatile] Unsigned;
        Context         : [Volatile] Unsigned;

        Ret_Status      : Unsigned;

  { VAR Definitions
        Identifier ---- The identifier that we want to check on.
        Hold_Result --- Hold the result of the function before we return.
        UIC ----------- The UIC of the user running the program.
        Holder -------- Who's UIC we're checking for.
        Bin_Id -------- The Binary form of the ASCII identifier that we were
                                passed.
        Check_Id ------ The current id that we are checking.
        Context ------- The context variable for $Find_Held
        Ret_Status ---- Hold the return status of a call to a system routine.
  }

  BEGIN { Function User_Has_Identifier }
    { We haven't found the identifier, so this function is set to FALSE until
        we do. }
    Hold_Result := FALSE;

    { Convert the identifier that has been passed here from ASCii format TO
        binary IDentifier format. }
    Ret_Status := $AscToId (Identifier,Bin_Id,);

    { Be graceful about an identifier not existing and don't bomb out. }
    IF (Ret_Status = SS$_NoSuchId)
      THEN Writeln ('Identifier ',Identifier,' does not exist.')
      ELSE
        BEGIN
          { Do the check here so that we don't blow the user out of the program
                if the identifier doesn't exist. }
          Check (Ret_Status);

          { Initialize a couple of things before we start calling $Find_Held. }

          { Initialize the context variable for $Find_Held }
          Context := 0;

          { Get the UIC of the current process and convert it into something
                that $Find_Held will like. }
          Check (LIB$GetJPI (JPI$_UIC,,,UIC,,));
          Holder.UIC := UIC;
          Holder.Reserved := 0;

          { Call $Find_Held repeatedly.  Each time returns an Identifier found
                in the rights_list database until we run out of identifiers for
                the current UIC.  When this happens, the status SS$_NoSuchId
                is returned. }

          REPEAT { UNTIL we find the identifier or we run out of identifiers
                        to try. }
            Ret_Status := $Find_Held (Holder,Check_Id,,Context);

            { Be graceful about a status that we know will come up. }
            IF (Ret_Status <> SS$_NoSuchId)
              THEN Check (Ret_Status);

            { Test to see if the two binary formats of the identifier are the
                same.  If we match, then we've found it.  Otherwise continue. }
            Hold_Result := (Check_Id = Bin_Id);
          UNTIL Hold_Result OR (Ret_Status = SS$_NoSuchId);

          { Clean up any channels assigned to the rightslist database only if
                we find the identifier. When SS$_NoSuchId is returned
                $Find_Held does this automatically. }
          IF Hold_Result
            THEN Check ($Finish_RDB (Context));

      END; { IF (Ret_Status = SS$_NoSuchId) ... ELSE (i.e. the identifier
                exists.) }

    { Send back the result. }
    User_Has_Identifier := Hold_Result;
  END; { Function User_Has_Identifier }

{******************************************************************************}

Function User_On_List (Qualifier : String80) : BOOLEAN;
  {
           This Function will check to see if the user is on the qualifier
        list.  Since right now both an ALLOW and a DISALLOW set may be
        specified, just return whether or not the user holds one of the
        Identifiers on the list.
  }

  TYPE
        Access_List_Ptr         = ^Access_List_Record;

        Access_List_Record      = RECORD
                                    Identifier  : String80;
                                    Next        : Access_List_Ptr;
                                  END; { RECORD Access_List_Record;

  {TYPE Definitions
        Access_List_Ptr ------ A pointer to a list of access identifiers
        Access_List_Record --- A record for a linked list of access id's
  }

  VAR
        Access_Identifiers      : Access_List_Ptr;
        Hold_Ptr                : Access_List_Ptr;

        Hold_Result             : BOOLEAN;

        Ret_Status              : INTEGER;

  {VAR Definitions
        Access_Identifiers --- Identifiers that allow access.
        Hold_Ptr ------------- A temporary pointer.
        Hold_Result ---------- The result of the function 'till it gets passed.
        Ret_Status ----------- Hold a return status.
  }

  BEGIN { Function User_On_List }
    Hold_Result := False;

    { Build the list of identifiers. }
    Hold_Ptr := NIL;
    Access_Identifiers := NIL;

    NEW (Hold_Ptr);
    Hold_Ptr^.Next := NIL;
    Hold_Ptr^.Identifier := '';

    IF ((CLI$Present (Qualifier) = CLI$_Present) OR
        (CLI$Present (Qualifier) = CLI$_Defaulted))
      THEN
        REPEAT
          Ret_Status := CLI$Get_Value ( Qualifier,
                                        Hold_Ptr^.Identifier.Body,
                                        Hold_Ptr^.Identifier.Length);

          IF (Access_Identifiers = NIL)
            THEN Access_Identifiers := Hold_Ptr
            ELSE
              BEGIN
                Hold_Ptr^.Next := Access_Identifiers;
                Access_Identifiers := Hold_Ptr;
              END;

          Hold_Ptr := NIL;
          NEW (Hold_Ptr);
          Hold_Ptr^.Next := NIL;
          Hold_Ptr^.Identifier := '';

        UNTIL (Ret_Status = SS$_Normal);

    WHILE NOT Hold_Result AND (Access_Identifiers <> NIL) DO
      BEGIN
        Hold_Result := User_Has_Identifier (Access_Identifiers^.Identifier);

        Hold_Ptr := Access_Identifiers;
        Access_Identifiers := Access_Identifiers^.Next;
        Hold_Ptr^.Next := NIL;
        DISPOSE (Hold_Ptr);
      END;

    User_On_List := Hold_Result;
  END; { Function User_On_List }

{******************************************************************************}

Procedure Initialize;
  {
           Initialize the Node name and the Identifier that goes with it.
  }

  TYPE
        IO_Status_Block_Type    = RECORD
                                    IO_Stat, Count      : UnsignedWord;
                                    Dev_Info            : INTEGER;
                                  END; { RECORD IO_Status_Block_Type }

        SYI_Info_Block_Type     = RECORD
                                    Buffer_Length       : UnsignedWord;
                                    Item_Code           : UnsignedWord;
                                    Buffer_Address      : Unsigned;
                                    Ret_Length_Address  : Unsigned;
                                  END; { RECORD SYI_Info_Block_Type }

  {TYPE Definitions
        IO_Status_Block_Type --- A standard IO status block.
        SYI_Info_Block_Type ---- Hold a block to pass to $GETSYI.
  }

  VAR
        First_Space             : INTEGER;

        SYI_Info                : ARRAY [1..2] OF SYI_Info_Block_Type;
        IO_Stat                 : IO_Status_Block_Type;

  {VAR Definitions
        First_Space ------------ The first space in the node string.
        SYI_Info --------------- The item list that we pass to $GETSYI
        IO_Stat ---------------- Hold the IO status of a call
  }

  BEGIN { Procedure Initialize }
    Node := '';
    Node := PAD (Node,' ',80);

    { Get the node name. }
    SYI_Info[1].Buffer_Length := 80;
    SYI_Info[1].Item_Code := SYI$_SCSNODE;
    SYI_Info[1].Buffer_Address := (iaddress (Node.Body)) :: Integer;
    SYI_Info[1].Ret_Length_Address := (iaddress (Node.Length)) :: Integer;

    SYI_Info[2].Buffer_Length := 0;
    SYI_Info[2].Item_Code := 0;
    SYI_Info[2].Buffer_Address := 0;
    SYI_Info[2].Ret_Length_Address := 0;

    IO_Stat.IO_Stat := 0;
    IO_Stat.Count := 0;
    IO_Stat.Dev_Info := 0;

    Check ($GetSyIW (,,,SYI_Info,IO_Stat,,));
    Check (IO_Stat.IO_Stat);

    First_Space := INDEX (Node,' ');
    Node := SUBSTR (Node,1,First_Space-1);

    { Make up the Identifier. }
    Identifier := Node + Identifier;

  END; { Procedure Initialize }

{******************************************************************************}

BEGIN { Program Node_Access }
  Initialize;

  { Check for Explicit Access to this node. }
  Explicit_Access := User_Has_Identifier (Identifier);
  User_Has_Access := Explicit_Access;

  { Check to see if the user has one of the Identifiers on the ALLOW list.
        For the sake of being efficient, we only check if we have to. }
  IF NOT User_Has_Access
    THEN User_Has_Access := User_On_List ('ALLOW');

  { Check to see if the user has one of the Identifiers on the DISALLOW list.
        For the sake of being efficient, we only check if we have to. }
  IF User_Has_Access AND NOT Explicit_Access
    THEN User_Has_Access := NOT User_On_List ('DISALLOW');

  { Kick the user off of the system if they aren't allowed on. }
  IF NOT User_Has_Access AND NOT Explicit_Access
    THEN
      BEGIN
        { Grab the owner information (if present) }
        IF ((CLI$Present ('OWNER') = CLI$_Present) OR
            (CLI$Present ('OWNER') = CLI$_Defaulted))
          THEN Check (CLI$Get_Value ('OWNER',Owner.Body,Owner.Length));

        Writeln;
        Writeln ('You are not authorized to use node ',Node,'.');
        Writeln ('Please see ',Owner,' for authorization.');
        Writeln;
        $DelPrc(,);
      END;
END. { Program Node_Access }
$*$*EOD*$*$
$ checksum NODE_ACCESS.PAS
$ if checksum$checksum .ne. check_sum then -
$   write sys$output "Checksum failed, file probably corrupted"
$ exit
-------