	!=====================================================================+
	! MASTER - program for allowing "proj_MASTER" holders to grant and    |
	!          revoke identifiers for their project.                      |
	!=====================================================================+
	! Author:  Harry Flowers
	!
	!  Command syntax:
	!
	!    $ MASTER GRANT[/SURE][/NOTNOW] identifier username
	!    $ MASTER REVOKE[/SURE][/NOW]   identifier username
	!    $ MASTER LIST                  identifier
	!
	!  /SURE suppresses the "Are you SURE" question for granting/revoking
	!        the proj_MASTER id itself (an unusual thing to do)
	!  /NOTNOW does not grant the identifier to existing processes; the
	!          default is to grant the identifier to existing processes
	!  /NOW revokes the identifier from existing processes; the default
	!       is not to revoke the identifier from existing processes
	!  Note: Granting and revoking to existing processes only takes affect
	!        on the node from which the MASTER command was issued; this is
	!        due to a node restriction in $GRANTID and $REVOKID (VMS V5.4).
	!
	!======================================================================
	!
	! Set up system services
	OPTION TYPE = EXPLICIT
	EXTERNAL LONG FUNCTION	LIB$GET_FOREIGN,	&
				LIB$STOP,		&
				LIB$SIGNAL,		&
				SYS$GETJPIW,		&
				SYS$GETUAI,		&
				SYS$ASCTOID,		&
				SYS$IDTOASC,		&
				SYS$FIND_HOLDER,	&
				SYS$ADD_HOLDER,		&
				SYS$REM_HOLDER,		&
				SYS$PROCESS_SCAN,	&
				SYS$GRANTID,		&
				SYS$REVOKID,		&
				SYS$FAO,		&
				SOR$BEGIN_SORT,		&
				SOR$RELEASE_REC,	&
				SOR$SORT_MERGE,		&
				SOR$RETURN_REC,		&
				SOR$END_SORT
	!
	%INCLUDE "$SSDEF" %FROM %LIBRARY "SYS$LIBRARY:BASIC$STARLET.TLB"
	%INCLUDE "$JPIDEF" %FROM %LIBRARY "SYS$LIBRARY:BASIC$STARLET.TLB"
	%INCLUDE "$KGBDEF" %FROM %LIBRARY "SYS$LIBRARY:BASIC$STARLET.TLB"
	%INCLUDE "$UAIDEF" %FROM %LIBRARY "SYS$LIBRARY:BASIC$STARLET.TLB"
	%INCLUDE "$PSCANDEF" %FROM %LIBRARY "SYS$LIBRARY:BASIC$STARLET.TLB"
	%INCLUDE "$SORDEF" %FROM %LIBRARY "SYS$LIBRARY:BASIC$STARLET.TLB"
	%INCLUDE "$DSCDEF" %FROM %LIBRARY "SYS$LIBRARY:BASIC$STARLET.TLB"
	!
	DECLARE LONG	STAT, RET_LENGTH
	DECLARE LONG CONSTANT BUF_LENGTH = 255%
	MAP(FIXED_STRING) STRING RET_STRING = BUF_LENGTH
	DECLARE LONG CONSTANT FATAL = 268435460%
	DECLARE STRING NODES
	!
	RECORD ITMLST
	    GROUP ITEM(2)
		VARIANT
		    CASE
			WORD	BUFFER_LEN
			WORD	ITEM_CODE
			LONG	BUFFER_ADDR
			LONG	LENGTH_ADDR
		    CASE
			LONG	TERMINATOR
		END VARIANT
	    END GROUP
	END RECORD
	!
	DECLARE STRING	COMMAND_LINE,		&
			PROMPT_STR,		&
		WORD	OUT_LEN
	!
	DECLARE STRING	ID_NAME,		&
		LONG	RIGHTS_ID,		&
			ID_ATTRIB
	!
	DECLARE	LONG	PROCESS_ID
	DECLARE ITMLST	ITEM_LIST
	DECLARE	LONG	IOSB(1%)
	DECLARE LONG	PROC_RIGHTS(128%)
	!
	MAP(RIGHTS_HOLDER)			&
		LONG	UIC,			&
		LONG	ZERO
	DECLARE	LONG	CONTEXT
	!
	DECLARE	STRING	USERNAME	
	!
	DECLARE	WORD	KEYBUFFER(4%),		&
			RECLENGTH,		&
		BYTE	WORKFILES		
	!
	!======================================================================
	! Misc declarations
	DECLARE STRING	QUALIFIER,		&
			COMMAND,		&
			PROJECT,		&
			PROJ_MASTER,		&
			MASTER_USER,		&
			PID,			&
			ANS,			&
		LONG	NOW,			&
			NOTNOW,			&
			SURE,			&
			BOUNCER,		&			
			NUMRECORDS,		&
			X, Y, Z
	!
	!======================================================================
	! Initialize some variables
	ZERO = 0%
	NOW = 0%
	NOTNOW = 0%
	SURE = 0%
	!NODES = "ADMIN*"
	!
	! Parse command line.  Find identifier name to grant/revoke, etc.
	!
	PROMPT_STR = "_Command: "
	!
	STAT = LIB$GET_FOREIGN(COMMAND_LINE,PROMPT_STR,OUT_LEN,)
	CALL LIB$STOP(STAT BY VALUE) IF (STAT AND 1%) = 0%
	!
	IF OUT_LEN = 0%
	    THEN
		PRINT "%MASTER-F-NOCMD, no command"
		CALL LIB$STOP(FATAL BY VALUE)
	END IF
	COMMAND_LINE = EDIT$(COMMAND_LINE,4%+8%+16%+32%+128%)
	!
	! Start our primitive parse of the command line
	Y = POS(COMMAND_LINE,"/",0%)		! First slash (/)
	WHILE Y <> 0%				! While there are /'s
	Z = POS(COMMAND_LINE,"/",Y+1%)		! Next / after Y
	X = POS(COMMAND_LINE," ",Y+1%)		! Next space after Y
	IF (Z = 0%) THEN Z = X \ END IF		! If no /, end @ space
	IF (X <> 0%) AND (X < Z) THEN Z = X \ END IF ! Space before /, end@space
	Z = LEN(COMMAND_LINE) + 1% IF Z = 0%	! No space or slash, end @ end+1
	QUALIFIER = SEG$(COMMAND_LINE,Y+1%,Z-1%) ! Extract qualifier
	COMMAND_LINE = LEFT$(COMMAND_LINE,Y-1%) + RIGHT$(COMMAND_LINE,Z)
	SELECT QUALIFIER
		CASE "NOW"
		    NOW = -1%
		    NOTNOW = 0%
		CASE "NOT" TO "NOTNOW"
		    NOTNOW = -1%
		    NOW = 0%
		CASE "SU" TO "SURE"
		    SURE = -1%
		CASE ELSE
		    PRINT "%MASTER-W-UNK, unknown qualifier: ";QUALIFIER
		END SELECT		
	Y = POS(COMMAND_LINE,"/",0%)		! Find remaining /'s
	NEXT 					! Y <> 0%; /'s to parse
	!
	COMMAND_LINE = EDIT$(COMMAND_LINE,8%+16%+128%)
	IF LEN(COMMAND_LINE) = 0%
	    THEN
		PRINT "%MASTER-F-NOCMD, no command"
		CALL LIB$STOP(FATAL BY VALUE)
	END IF
	X = POS(COMMAND_LINE," ",1%)		! First space
	X = LEN(COMMAND_LINE) + 1% IF X = 0%	! Only thing left
	COMMAND = LEFT$(COMMAND_LINE,X-1%)	! Command
	COMMAND_LINE = RIGHT$(COMMAND_LINE,X+1%)
	X = POS(COMMAND_LINE," ",1%)		! Next space
	X = LEN(COMMAND_LINE) + 1% IF X = 0%	! Only thing left
	ID_NAME = LEFT$(COMMAND_LINE,X-1%)	! Identifier name
	COMMAND_LINE = RIGHT$(COMMAND_LINE,X+1%)
	X = POS(COMMAND_LINE," ",1%)		! Next space
	X = LEN(COMMAND_LINE) + 1% IF X = 0%	! Only thing left
	USERNAME = LEFT$(COMMAND_LINE,X-1%)	! Username
	!
	!======================================================================
	! Construct the proj_MASTER identifier and use SYS$ASCTOID to convert
	! to ID.
	X = POS(ID_NAME,"_",1%)			! Underscore
	PROJECT = LEFT$(ID_NAME,X-1%)		! Project
	IF PROJECT = ""
	    THEN
		PRINT "%MASTER-F-NOID, no valid identifier specified"
		CALL LIB$STOP(FATAL BY VALUE)
	END IF
	PROJ_MASTER = PROJECT + "_MASTER"	! Project master
	STAT = SYS$ASCTOID(PROJ_MASTER BY DESC,	&
	                   RIGHTS_ID BY REF,	&
	                   ID_ATTRIB BY REF)
	CALL LIB$STOP(STAT BY VALUE) IF (STAT AND 1%) = 0%
	!
	! Use SYS$GETJPIW to get the invoker's username and process rights.
	ITEM_LIST::ITEM(0)::BUFFER_LEN	= BUF_LENGTH
	ITEM_LIST::ITEM(0)::ITEM_CODE	= JPI$_USERNAME
	ITEM_LIST::ITEM(0)::BUFFER_ADDR = LOC(RET_STRING)
	ITEM_LIST::ITEM(0)::LENGTH_ADDR = LOC(RET_LENGTH)
	ITEM_LIST::ITEM(1)::BUFFER_LEN	= 512%
	ITEM_LIST::ITEM(1)::ITEM_CODE	= JPI$_PROCESS_RIGHTS
	ITEM_LIST::ITEM(1)::BUFFER_ADDR	= LOC(PROC_RIGHTS(1%))
	ITEM_LIST::ITEM(1)::LENGTH_ADDR = LOC(PROC_RIGHTS(0%))
	ITEM_LIST::ITEM(2)::TERMINATOR  = 0%
	PROCESS_ID = 0%
	STAT = SYS$GETJPIW(,PROCESS_ID BY REF,,	&
	                   ITEM_LIST BY REF,	&
	                   IOSB(0%) BY REF,,)
	CALL LIB$STOP(STAT BY VALUE) IF (STAT AND 1%) = 0%
	CALL LIB$STOP(IOSB(0%) BY VALUE) IF (IOSB(0%) AND 1%) = 0%
	MASTER_USER = LEFT$(RET_STRING,RET_LENGTH)
	PROC_RIGHTS(0%) = PROC_RIGHTS(0%)/8%
	BOUNCER = -1%
	CHECK_ID:
	FOR X = 1 TO PROC_RIGHTS(0%)
	Y = 2%*X - 1%
	IF PROC_RIGHTS(Y) = RIGHTS_ID
	    THEN
		BOUNCER = 0%
		EXIT CHECK_ID
	END IF
	NEXT X
	IF BOUNCER
	    THEN
		PRINT "%MASTER-F-NOTMASTER, you don't hold " + PROJ_MASTER
		CALL LIB$STOP(FATAL BY VALUE)
	END IF
 ! Log the following information:
 ! date and time, MASTER username, grant/revoke, identifier, username
 ! DATE$          MASTER_USER      COMMAND       ID_NAME     USERNAME
	OPEN "MASTER_LOG:" AS FILE #1, ACCESS APPEND, ALLOW MODIFY
	PRINT #1, DATE$(0%) + " " + TIME$(0%) + " " + MASTER_USER + " " + &
		COMMAND + " " + ID_NAME + " " + USERNAME
	CLOSE #1
	!PRINT "Invoker's username: " + MASTER_USER
	!
	!======================================================================
	! Convert parsed identifier to ID
	STAT = SYS$ASCTOID(ID_NAME BY DESC,	&
	                   RIGHTS_ID BY REF,	&
	                   ID_ATTRIB BY REF)
	CALL LIB$STOP(STAT BY VALUE) IF (STAT AND 1%) = 0%
	!
	! Get parsed username's UIC.
	IF USERNAME = "" THEN GOTO DO_COMMAND \ END IF
	ITEM_LIST::ITEM(0)::BUFFER_LEN	= 4%
	ITEM_LIST::ITEM(0)::ITEM_CODE	= UAI$_UIC
	ITEM_LIST::ITEM(0)::BUFFER_ADDR = LOC(UIC)
	ITEM_LIST::ITEM(0)::LENGTH_ADDR = LOC(RET_LENGTH)
	ITEM_LIST::ITEM(1)::TERMINATOR  = 0%
	STAT = SYS$GETUAI(,,USERNAME BY DESC,ITEM_LIST BY REF,,,)
	CALL LIB$STOP(STAT BY VALUE) IF (STAT AND 1%) = 0%
	!
	DO_COMMAND:
	SELECT COMMAND
	    CASE "L" TO "LIST"
		! We will get and sort the list of usernames holding the id
		CONTEXT = 0%
		STAT = 0%
		NUMRECORDS = 0%
		KEYBUFFER(0%) = 1%		! One key
		KEYBUFFER(1%) = DSC$K_DTYPE_T	! Text key
		KEYBUFFER(2%) = 0%		! Ascending order
		KEYBUFFER(3%) = 0%		! Offset in record
		KEYBUFFER(4%) = 32%		! Key size
		RECLENGTH = 32%			! Record size
		WORKFILES = 0%			! Sort in memory
		! Set up the sort
		STAT = SOR$BEGIN_SORT(KEYBUFFER(0%) BY REF,	&
		                      RECLENGTH BY REF,,,,,,	&
		                      WORKFILES BY REF,)
		CALL LIB$STOP(STAT BY VALUE) IF (STAT AND 1%) = 0%
		! Find the holders
		WHILE STAT <> SS$_NOSUCHID
		STAT = SYS$FIND_HOLDER(RIGHTS_ID BY VALUE,	&
		                       UIC BY REF,		&
		                       ID_ATTRIB BY REF,	&
		                       CONTEXT BY REF)
		IF STAT = SS$_NOSUCHID THEN ITERATE \ END IF
		CALL LIB$STOP(STAT BY VALUE) IF (STAT AND 1%) = 0%
		! Translate the UIC to ASCII
		STAT = SYS$IDTOASC(UIC BY VALUE,		&
		                   RET_LENGTH BY REF,		&
		                   RET_STRING BY DESC,		&
		                   ,,,)
		CALL LIB$STOP(STAT BY VALUE) IF (STAT AND 1%) = 0%
		USERNAME = LEFT$(RET_STRING,RET_LENGTH) + SPACE$(32%-RET_LENGTH)
		NUMRECORDS = NUMRECORDS + 1%
		! Pass the username to the sort routine
		STAT = SOR$RELEASE_REC(USERNAME BY DESC,)
		CALL LIB$STOP(STAT BY VALUE) IF (STAT AND 1%) = 0%
		NEXT	! STAT <> SS$_NOSUCHID
		PRINT "%MASTER-I-LIST, listing"; NUMRECORDS;	&
			"users holding " + ID_NAME
		! Actually do the sort
		STAT = SOR$SORT_MERGE()
		CALL LIB$STOP(STAT BY VALUE) IF (STAT AND 1%) = 0%
		! Print the list
		FOR X = 1 TO NUMRECORDS
		STAT = SOR$RETURN_REC(RET_STRING BY DESC,	&
		                      RECLENGTH BY REF,)
		USERNAME = EDIT$(LEFT$(RET_STRING,RECLENGTH),2%)
		PRINT USERNAME
		NEXT X
		STAT = SOR$END_SORT()
		CALL LIB$STOP(STAT BY VALUE) IF (STAT AND 1%) = 0%
	    CASE "G" TO "GRANT"
		!
		! If the ID is the MASTER one, make sure of the change
		IF ID_NAME = PROJ_MASTER
		    THEN
			IF NOT SURE
			    THEN
				PRINT "Are you SURE you wish to grant " + &
					PROJ_MASTER + " to " + USERNAME;
				INPUT ANS
				ANS = EDIT$(ANS,2%+4%+32%)
				IF ANS <> "Y" AND ANS <> "YES"
				    THEN
					PRINT "%MASTER-F-NOTSURE, aborting"
					CALL LIB$STOP(FATAL BY VALUE)
				END IF
			END IF
		END IF
		! Use SYS$ADD_HOLDER to grant the identifier.
		STAT = SYS$ADD_HOLDER(RIGHTS_ID BY VALUE,	&
		                      UIC BY REF,		&
		                      ID_ATTRIB BY VALUE)
		CALL LIB$STOP(STAT BY VALUE) IF (STAT AND 1%) = 0%
		PRINT "%MASTER-I-GRANT, granted " + ID_NAME + " to " + USERNAME
		IF NOTNOW THEN GOTO THE_END \ END IF
		ITEM_LIST::ITEM(0)::BUFFER_LEN	= 0%
		ITEM_LIST::ITEM(0)::ITEM_CODE	= PSCAN$_UIC
		ITEM_LIST::ITEM(0)::BUFFER_ADDR = UIC
		ITEM_LIST::ITEM(0)::LENGTH_ADDR = PSCAN$M_EQL
		!Commented out because $GRANTID doesn't work cluster-wide
		!RET_STRING = NODES
		!ITEM_LIST::ITEM(1)::BUFFER_LEN	= LEN(NODES)
		!ITEM_LIST::ITEM(1)::ITEM_CODE	= PSCAN$_NODENAME
		!ITEM_LIST::ITEM(1)::BUFFER_ADDR = LOC(RET_STRING)
		!ITEM_LIST::ITEM(1)::LENGTH_ADDR = PSCAN$M_WILDCARD
		!ITEM_LIST::ITEM(2)::TERMINATOR  = 0%
		ITEM_LIST::ITEM(1)::TERMINATOR  = 0%
		STAT = SYS$PROCESS_SCAN(CONTEXT BY REF,	&
		                        ITEM_LIST BY REF)
		CALL LIB$SIGNAL(STAT BY VALUE) IF (STAT AND 1%) = 0%
		ITEM_LIST::ITEM(0)::BUFFER_LEN	= 4%
		ITEM_LIST::ITEM(0)::ITEM_CODE	= JPI$_PID
		ITEM_LIST::ITEM(0)::BUFFER_ADDR = LOC(PROCESS_ID)
		ITEM_LIST::ITEM(0)::LENGTH_ADDR = LOC(RET_LENGTH)
		ITEM_LIST::ITEM(1)::TERMINATOR  = 0%
		WHILE STAT <> SS$_NOMOREPROC
		STAT = SYS$GETJPIW(,CONTEXT BY REF,,	&
		                   ITEM_LIST BY REF,	&
		                   IOSB(0%) BY REF,,)
		IF STAT = SS$_NOMOREPROC THEN ITERATE \ END IF
		CALL LIB$STOP(STAT BY VALUE) IF (STAT AND 1%) = 0%
		CALL LIB$STOP(IOSB(0%) BY VALUE) IF (IOSB(0%) AND 1%) = 0%
		STAT = SYS$GRANTID(PROCESS_ID BY REF,,	&
		                   RIGHTS_ID BY REF,,)
		CALL LIB$SIGNAL(STAT BY VALUE) IF (STAT AND 1%) = 0%
		STAT = SYS$FAO("!XL",RET_LENGTH BY REF,	&
		               RET_STRING BY DESC,	&
		               PROCESS_ID BY VALUE)
		CALL LIB$SIGNAL(STAT BY VALUE) IF (STAT AND 1%) = 0%
		PID = LEFT$(RET_STRING,RET_LENGTH)
		PRINT "%MASTER-I-GRANT, updated process ";PID
		NEXT ! STAT <> SS$_NOMOREPROC
	    CASE "R" TO "REVOKE"	
		!
		! If the ID is the MASTER one, make sure of the change
		IF ID_NAME = PROJ_MASTER
		    THEN
			IF NOT SURE
			    THEN
				PRINT "Are you SURE you wish to revoke " + &
					PROJ_MASTER + " from " + USERNAME;
				INPUT ANS
				ANS = EDIT$(ANS,2%+4%+32%)
				IF ANS <> "Y" AND ANS <> "YES"
				    THEN
					PRINT "%MASTER-F-NOTSURE, aborting"
					CALL LIB$STOP(FATAL BY VALUE)
				END IF
			END IF
		END IF
		! Use SYS$REM_HOLDER to revoke the identifier.
		STAT = SYS$REM_HOLDER(RIGHTS_ID BY VALUE,	&
		                      UIC BY REF)
		CALL LIB$STOP(STAT BY VALUE) IF (STAT AND 1%) = 0%
		PRINT "%MASTER-I-REVOKE, revoked " + ID_NAME + " from " + &
			USERNAME
		IF NOT NOW THEN GOTO THE_END \ END IF
		ITEM_LIST::ITEM(0)::BUFFER_LEN	= 0%
		ITEM_LIST::ITEM(0)::ITEM_CODE	= PSCAN$_UIC
		ITEM_LIST::ITEM(0)::BUFFER_ADDR = UIC
		ITEM_LIST::ITEM(0)::LENGTH_ADDR = PSCAN$M_EQL
		!Commented out because $REVOKID doesn't work cluster-wide
		!RET_STRING = NODES
		!ITEM_LIST::ITEM(1)::BUFFER_LEN	= LEN(NODES)
		!ITEM_LIST::ITEM(1)::ITEM_CODE	= PSCAN$_NODENAME
		!ITEM_LIST::ITEM(1)::BUFFER_ADDR = LOC(RET_STRING)
		!ITEM_LIST::ITEM(1)::LENGTH_ADDR = PSCAN$M_WILDCARD
		!ITEM_LIST::ITEM(2)::TERMINATOR  = 0%
		ITEM_LIST::ITEM(1)::TERMINATOR  = 0%
		STAT = SYS$PROCESS_SCAN(CONTEXT BY REF,	&
		                        ITEM_LIST BY REF)
		CALL LIB$STOP(STAT BY VALUE) IF (STAT AND 1%) = 0%
		ITEM_LIST::ITEM(0)::BUFFER_LEN	= 4%
		ITEM_LIST::ITEM(0)::ITEM_CODE	= JPI$_PID
		ITEM_LIST::ITEM(0)::BUFFER_ADDR = LOC(PROCESS_ID)
		ITEM_LIST::ITEM(0)::LENGTH_ADDR = LOC(RET_LENGTH)
		ITEM_LIST::ITEM(1)::TERMINATOR  = 0%
		WHILE STAT <> SS$_NOMOREPROC
		STAT = SYS$GETJPIW(,CONTEXT BY REF,,	&
		                   ITEM_LIST BY REF,	&
		                   IOSB(0%) BY REF,,)
		IF STAT = SS$_NOMOREPROC THEN ITERATE \ END IF
		CALL LIB$STOP(STAT BY VALUE) IF (STAT AND 1%) = 0%
		CALL LIB$STOP(IOSB(0%) BY VALUE) IF (IOSB(0%) AND 1%) = 0%
		STAT = SYS$REVOKID(PROCESS_ID BY REF,,	&
		                   RIGHTS_ID BY REF,,)
		CALL LIB$SIGNAL(STAT BY VALUE) IF (STAT AND 1%) = 0%
		STAT = SYS$FAO("!XL",RET_LENGTH BY REF,	&
		               RET_STRING BY DESC,	&
		               PROCESS_ID BY VALUE)
		CALL LIB$SIGNAL(STAT BY VALUE) IF (STAT AND 1%) = 0%
		PID = LEFT$(RET_STRING,RET_LENGTH)
		PRINT "%MASTER-I-REVOKE, updated process ";PID
		NEXT ! STAT <> SS$_NOMOREPROC
	    CASE ELSE
		PRINT "%MASTER-F-UNKCMD, unknown command: " + COMMAND
		CALL LIB$STOP(FATAL BY VALUE)
	    END SELECT
	THE_END:
	END
