# DatabaseAuthenticationHook.java

    /*
    ** Module   : DatabaseAuthenticationHook.java
    ** Abstract : auth hook that asks the user for credentials for accessing a database
    **
    ** Copyright (c) 2005-2023, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- -------------------------------- Description --------------------------------
    ** 001 OM  20130927 First commit with initial implementation.
    ** 002 EVL 20140217 Fixed javadoc comments to be able to compile in Windows 1252 CP.
    ** 003 GES 20140626 Renamed color attributes to match naming standards.
    ** 004 EVL 20140829 All widgets in login screen must have predefined ID.  Including space and skip
    **                  never leaving -1 as ID when passing widget config to ScreenDefinition class.
    **                  We can not use the new widget ID allocation here because there is no
    **                  LogicalTerminal at this time.  Size of ScreenBuffer changed to be
    **                  LAST_CONFIG_COMP - EDIT_FID to store last skip into buffer as well.
    ** 006 CA  20140926 Added shared widget configuration support, to allow GUI/ChUI concrete
    **                  implementation for the same widget ID.  Refactored the widget configuration
    **                  classes: all fields were made public; these classes need to be as dumb as
    **                  possible, all logic related to setting/getting a certain field should be at
    **                  the widget or at the caller. Refs #2254
    ** 007 VIG 20140911 Changed method clientFinalize() with statusInput method instead of
    **                  removed statusInputRevert(). Added an argument of messagebox method call.
    ** 008 CA  20141119 Refactored drawing code to enable multiple OS windows, for GUI support.  Misc
    **                  changes/fixes related to drawing on multiple windows.
    ** 009 HC  20150405 Refactoring of config coordinate fields.
    ** 010 EVL 20160224 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
    ** 011 IAS 20160314 DEBUG-ALERT support.
    ** 012 EVL 20160726 Now we use screen buffer array in waitFor() result instead of single buffer.
    ** 013 SBI 20161101 Added tab item list as a parameter to the enable, view, display,
    **                  displayAndDown and promptFor methods provided by ClientExports.
    ** 014 CA  20180620 Ensure both on/off calls for an invalidation bracket are always called.
    ** 016 IAS 20220713 Added 'lock' argument to the ConnectionManager.setAuthenticatedUserid call.
    ** 017 GBB 20230825 checkCallerAbort moved to SecurityUtil & calls updated.
    */
    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.security;

    import com.goldencode.p2j.directory.DirectoryService;
    import com.goldencode.p2j.net.*;
    import com.goldencode.p2j.net.Session;
    import com.goldencode.p2j.persist.ConnectionManager;
    import com.goldencode.p2j.ui.*;
    import com.goldencode.p2j.ui.chui.ThinClient;
    import com.goldencode.p2j.ui.client.*;
    import com.goldencode.p2j.ui.client.widget.*;
    import com.goldencode.p2j.util.*;
    import java.io.ByteArrayInputStream;
    import java.io.DataInputStream;
    import java.io.IOException;
    import java.util.*;
    import static com.goldencode.p2j.ui.LogicalTerminal.ALERT_MESSAGE;
    import static com.goldencode.p2j.ui.LogicalTerminal.BTN_OK;

    /**
     * This class is a database authentication hook implementation. If the database requires
     * authentication (ie. the _User meta-table is populated) this hook is used to prompt the user
     * for a userid/password combination. If the credentials cannot be validated, the user is
     * informed and allows to enter the combination again for maximum three times; after that the
     * session is closed.
     * However, the user is allowed to [Cancel] the authorization process and continue as "blank"
     * user (if the database allows this.).
     */
    public class DatabaseAuthenticationHook
    implements  Authenticator
    {
       /** Edit frame ID. */
       private static final int EDIT_FID = 100;

       /** ID for message on 1st line. */
       private static final int MSG1_ID = EDIT_FID + 2; // 1 + skip

       /** ID for message on 2nd line. */
       private static final int MSG2_ID = EDIT_FID + 4;

       /** User ID field ID. */
       private static final int USER_ID = EDIT_FID + 6;

       /** Password field ID. */
       private static final int PASSWORD_ID = EDIT_FID + 8;

       /** OK button field ID. */
       private static final int BUTTON_OK_ID = EDIT_FID + 10;

       /** Cancel button field ID. */
       private static final int BUTTON_CANCEL_ID = EDIT_FID + 11;

       /** Keep the id of the last component. */
       private static final int LAST_CONFIG_COMP = EDIT_FID + 12; /* including skip(1) after buttons*/

       /** First line of the [Login] dialog. */
       private static final String STD_MSG_1 = "Please enter a User Id and Password for";

       /** First line of the [Login] dialog. The name of teh database will be concatenated. */
       private static final String STD_MSG_2 = "database: ";

       /** The text on OK button from the [Login] dialog */
       private static final String DLG_BTN_OK_TEXT = "OK";

       /** The text on OK button from the [Login] dialog */
       private static final String DLG_BTN_CANCEL_TEXT = "Cancel";

       /** Invalid user status message. */
       private static final String INVALID_CREDENTIALS = "Userid/Password is incorrect.";

       /** Client interface. */
       private ThinClient client = null;

       /**
        * The screen buffer for the editing frame.
        * NOTE: numWidgets is important and must be exactly the numbers of widget configs defined.
        */
       private ScreenBuffer editsb = null;

       /** The map of authenticated userids for each database. */
       private Map<String, String> authUsers = null;

       /** The value of "option" node from AuthPlugin for this Authenticator */
       private String p2jAuthUser = null;

       /**
        * The default constructor.
        */
       public DatabaseAuthenticationHook()
       {
          client = ThinClient.getInstance();
       }

       /**
        * Implements client side authentication logic.
        *
        * @param    parameters
        *           Additional configuration parameters.
        * @param    code
        *           The result of the most recent attempt to authenticate or
        *           <code>AUTH_RESULT_NONE</code> if this is the first attempt.
        *
        * @return   Array of bytes to be transmitted to the server as the authorization input.
        */
       @Override
       public byte[] clientAuthHook(Map<String, Object> parameters, int code)
       {
          OutputManager<?> tk = OutputManager.instance();
          String database = (String) parameters.get("option"); // the processed entity

          if (editsb == null)
          {
             //lazy initialization
             editsb = new ScreenBuffer(EDIT_FID, LAST_CONFIG_COMP - EDIT_FID);
          }

          if (code == AUTH_RESULT_NONE)
          {
             // push in the frame definition the first time:
             createFrameDefinition(database);
          }
          else
          {
             // Some kind of error occurred, most likely AUTH_RESULT_INVALID_PASSWORD.
             displayWrongPassword(null);
          }

          String id = null;
          String pw = null;
          EventList el = new EventList(true);
          ScreenBuffer[] sbf = null;

          // get the widget
          Widget<?> edit = client.getWidget(EDIT_FID);

          // drive the logon frame I/O
          tk.setInvalidate(true);
          int[] activeWidgets = new int[] { USER_ID, PASSWORD_ID, BUTTON_OK_ID, BUTTON_CANCEL_ID };
          int[] tabItemList   = new int[] { USER_ID, PASSWORD_ID, BUTTON_OK_ID, BUTTON_CANCEL_ID };
          try
          {
             client.enable(EDIT_FID, activeWidgets, new ScreenBuffer[] {editsb}, true,
                           WindowConfig.RESOLVE_WINDOW, tabItemList);
             client.clear(EDIT_FID, true);
             client.view(EDIT_FID, new ScreenBuffer[] {editsb}, null, true,
                         WindowConfig.RESOLVE_WINDOW, tabItemList);
             editsb.resetChanged();
          }
          finally
          {
             tk.setInvalidate(edit, false);
          }

          try
          {
             client.setAuthMode(true);
             sbf = client.waitFor(el, USER_ID, -1, new ScreenBuffer[] {editsb});
          }
          catch (ConditionException cx)
          {
             // EndConditionException: END key function executed
             return null;
          }
          finally
          {
             client.setAuthMode(false);
          }

          tk.setInvalidate(true);
          try
          {
             client.enable(EDIT_FID, null, new ScreenBuffer[] {editsb}, false,
                           WindowConfig.RESOLVE_WINDOW, tabItemList);
          }
          finally
          {
             tk.setInvalidate(edit, false);
          }

          // inspect the input; exit out if requested
          if (Keyboard.keyFunction(sbf[0].getKeyCode()).equals("END-ERROR"))
          {
             return null;
          }

          character idc = (character) sbf[0].getWidgetValue(USER_ID);
          id = (idc == null) ? "" : idc.getValue().trim();

          character pwc = (character) sbf[0].getWidgetValue(PASSWORD_ID);
          pw = (pwc == null) ? "" : pwc.getValue();

          // return a byte array
          return SecurityUtil.packageIdPassword(id, pw);
       }

       /**
        * Implements server side authentication logic.
        * <p>
        * Accepts the byte array produced by the client side authentication hook
        * as authentication input as well as any custom parameters.
        *
        * @param    auth
        *           The authorization input from the client.
        * @param    ldbName
        *           Additional configuration parameters taken from the directory.
        *
        * @return   The result of the authentication processing.
        */
       @Override
       public AuthenticationResponse serverAuthHook(byte[] auth, String ldbName)
       {
          if (auth == null)
          {
             // user cancelled the authentication for this entity
             // we shoud do: addAuthUser(ldbName, SecurityOps.BLANK_USER);
             // however, this is not necessary, as the default userid is already BLANK_USER

             // from p2j SM authentication point, this is a success
             return new AuthenticationResponse(p2jAuthUser, AUTH_RESULT_SUCCESS);
          }
          // parse the byte array input
          ByteArrayInputStream bis = new ByteArrayInputStream(auth);
          DataInputStream dis = new DataInputStream(bis);

          String userId;
          String password;

          try
          {
             userId   = dis.readUTF();
             password = dis.readUTF();
          }
          catch (IOException ioe)
          {
             return new AuthenticationResponse(null, AUTH_RESULT_UNSPECIFIED_FAILURE);
          }

          if (ConnectionManager.authenticate(userId, password, ldbName))
          {
             addAuthUser(ldbName, userId);

             // This is a small workaround in order to allow successful database authentication.
             // The authentication is successful but we need convince SecurityManager that we
             // trustworthy, so we use the p2j userid that was configured in directory for this
             // purpose.
             return new AuthenticationResponse(p2jAuthUser, AUTH_RESULT_SUCCESS);
          }
          else
          {
             return new AuthenticationResponse(null, AUTH_RESULT_INVALID_PASSWORD);
          }
       }

       /**
        * Returns a set of databases that need to be authenticated.
        *
        * @return  The set of databases that requires the user to authenticate to.
        */
       @Override
       public Set<String> getAuthenticationEntities()
       {
          DirectoryService ds = DirectoryService.getInstance();
          final HashSet<String> entities = new HashSet<>();

          // We could use temporary context to access scoped directory location for p2jAuthUser
          // but access to SecurityManager.createSecurityContext(p2jAuthUser) is needed.
          // So we will use a scopeless search. Perhaps this is rather logical as there is only a
          // dedicated p2j user whose userid will be used tu authenticate to P2J SecurityManager.
          int n = ConnectionManager.getConnectedDbCount();
          for (int k = 0; k < n; k++)
          {
             final String ldbName = ConnectionManager.ldbName(k + 1).toStringMessage(); // 1 - base
             int authLevel = ConnectionManager.getAuthLevel(ldbName);
             if (authLevel == SecurityOps.FREE_ACCESS)
             {
                addAuthUser(ldbName, SecurityOps.getDefaultUserid(ldbName));
                /* or OS username or blank will be completed by ConnManager */
             }
             else
             {
                // the client should authenticate to database:
                //    check if credentials are stored in the directory.
                final String userid = Utils.getDirectoryNodeString(ds,
                                     "databases-auth/" + ldbName + "/userid", "", false);
                final String password = Utils.getDirectoryNodeString(ds,
                                     "databases-auth/" + ldbName + "/password", "", false);

                if (userid == null || userid.isEmpty())
                {
                   // store the database for user-interactive authentication session
                   entities.add(ldbName);
                }
                else if (ConnectionManager.authenticate(userid, password, ldbName))
                {
                   addAuthUser(ldbName, userid);
                }
                else
                {
                   // the credentials read from directory are incorrect: bail out:
                   // NOTE: replace the RuntimeException with a customized Exception ?
                   throw new RuntimeException("Authentication with " + userid +
                                "to " + ldbName + " using directory-saved values failed");
                }
             }
          }

          return entities;
       }

       /**
        * Configures the Authenticator by setting the "option" parameter from directory.xml.
        *
        * @param   p2jUser
        *          The value of userid stored that will be used for authentication to p2j
        *          SecurityManager.
        */
       @Override
       public void configure(String p2jUser)
       {
          this.p2jAuthUser = p2jUser;
       }

       /**
        * Adds an authenticated user to the internal list list.
        *
        * @param   ldbName
        *          The logical name of teh database that authenticated the user.
        * @param   userid
        *          The authenticated user.
        */
       private void addAuthUser(String ldbName, String userid)
       {
          if (authUsers == null)
          {
             // lazy initialization is preferred because the same class is run on client
             authUsers = new HashMap<>();
          }
          authUsers.put(ldbName, userid);
       }

       /**
        * A session listener will be returned only if any user was successfully authenticated
        * to a database.
        * The initialization callback will set the authenticated userid into the correct context,
        * after it will have been created.
        * The termination callback does nothing.
        *
        * @return  The current session listener (if there is any).
        */
       @Override
       public SessionListener getSessionListener()
       {
          if (authUsers == null)
          {
             // if no user has been authenticated no listener is needed
             return null;
          }

          return new SessionListener() {
             @Override
             public void terminate(Session session)
             {
                // not interested in this event
             }

             @Override
             public void initialize(Session session)
             {
                for (String ldbname : authUsers.keySet())
                {
                   ConnectionManager.setAuthenticatedUserid(ldbname, authUsers.get(ldbname), true, true);
                }
             }
          };
       }

       /**
        * Finalizes any resources allocated during authentication by the client.
        */
       @Override
       public void clientFinalize()
       {
          client.statusInputRevert(WindowConfig.RESOLVE_WINDOW);
          client.hideAll(true, WindowConfig.RESOLVE_WINDOW);
          client.destroyFrame(EDIT_FID);
       }

       /**
        * Displays a dialog informing the user that the entered database credentials are invalid.
        * If <code>userid</code> is null, the displayed dialog will simply print
        *        +------------ Error ------------+
        *        | Userid/Password is incorrect. |
        *        | ----------------------------- |
        *        |             &lt;OK&gt;              |
        *        +-------------------------------+
        *
        * Otherwise will print the P2J counter-part message of the 710 dialog of P4GL.
        * +---------------------- Error ----------------------+
        * | ** Your Password and Userid x do not match. (710) |
        * |                                                   |
        * | ------------------------------------------------- |
        * |                       &lt;OK&gt;                        |
        * +---------------------------------------------------+
        *
        * @param   userid
        *          The userid that was used in an authentication attempt.
        */
       public static void displayWrongPassword(String userid)
       {
          String message = (userid == null)
                ? INVALID_CREDENTIALS
                : "** Your Password and Userid " + userid + " do not match. (710)";

          ThinClient.getInstance().messageBox(new Object[]{message}, null, ALERT_MESSAGE,
                                              BTN_OK, " Error ", null, WindowConfig.RESOLVE_WINDOW,
                                              null /* TODO: is it always correct? */);

       }

       /**
        * Helper method, adds specified number of empty lines to a ScreenDefinition.
        *
        * @param   sd
        *          The frame to which to add a skip.
        * @param   lines
        *          Lines to skip.
        * @param   id
        *          Widget ID to be used for this skip widget.
        */
       private static void addSkip(ScreenDefinition sd, int lines, int id)
       {
          SkipConfig sc = new SkipConfig();
          sc.vertical = true;   // skip not space
          sc.length = lines;
          sd.addConfig(sc, id);
       }

       /**
        * Create the user interface definition for frame that holds the standard dialog that prompts
        * the user for userid/password combination.
        * The dialog is centered on the screen and looks like
        * <pre>
        * +------------------- Login -------------------+
        * |                                             |
        * | Please enter a User Id and Password for     |
        * | database: &lt;ldbname&gt;                         |
        * |                                             |
        * |    User Id: ________________                |
        * |   Password: ________________                |
        * |                                             |
        * |              &lt;OK&gt;   &lt;Cancel&gt;                |
        * +---------------------------------------------+
        * </pre>
        *
        * @param   database
        *          The name of the database for which the userid/password is requested.
        */
       private void createFrameDefinition(String database)
       {
          // remove any widgets created in a previous database dialog
          clientFinalize();

          // create the frame definition on the fly
          ScreenDefinition editsd = new ScreenDefinition(EDIT_FID);

          // edit frame configuration
          FrameConfig frameConfig = new FrameConfig();
          frameConfig.row = 10;
          frameConfig.box = true;
          frameConfig.centered = true;
          frameConfig.overlay = true;
          frameConfig.dcolor = Color.NORMAL;
          frameConfig.sideLabels = true;
          frameConfig.title = " Login ";
          frameConfig.widthChars = 47;
          editsd.addConfig(frameConfig, EDIT_FID);

          // set next widget position
          addSkip(editsd, 1, EDIT_FID + 1); // id = EDIT_FID + 1

          ControlTextConfig mess1Fic = new FillInConfig();
          mess1Fic.dataType = "character";
          mess1Fic.column = 2;
          mess1Fic.flagStatic = true;
          editsd.addConfig(mess1Fic, MSG1_ID);

          // set next widget position
          addSkip(editsd, 0, MSG1_ID + 1); // id = EDIT_FID + 3

          ControlTextConfig mess2Fic = new FillInConfig();
          mess2Fic.dataType = "character";
          mess2Fic.flagStatic = true;
          mess2Fic.column = 2;
          editsd.addConfig(mess2Fic, MSG2_ID);

          // set next widget position
          addSkip(editsd, 1, MSG2_ID + 1);  // id = EDIT_FID + 5

          // user ID entry field
          FillInConfig userFic = new FillInConfig();
          userFic.dataType = "character";
          userFic.format = "x(16)";
          userFic.column = 4;
          userFic.label = " User ID";
          editsd.addConfig(userFic, USER_ID);

          // set next widget position
          addSkip(editsd, 0, USER_ID + 1);  // id = EDIT_FID + 7

          // password entry field
          FillInConfig passFic = new FillInConfig();
          passFic.dataType = "character";
          passFic.format = "x(16)";
          passFic.column = 4;
          passFic.label = "Password";
          passFic.blank = true;  // this is a password; should not be visible.
          editsd.addConfig(passFic, PASSWORD_ID);

          // set next widget position
          addSkip(editsd, 1, PASSWORD_ID + 1);  // id = EDIT_FID + 9

          ButtonConfig bOk = new ButtonConfig();
          bOk.autoGo = true;
          bOk.column = 15;
          bOk.label = DLG_BTN_OK_TEXT;
          editsd.addConfig(bOk, BUTTON_OK_ID);

          ButtonConfig bCancel = new ButtonConfig();
          bCancel.autoEndKey = true;
          bCancel.column = 22;
          bCancel.label = DLG_BTN_CANCEL_TEXT;
          editsd.addConfig(bCancel, BUTTON_CANCEL_ID);

          addSkip(editsd, 1, BUTTON_CANCEL_ID + 1);  // id = EDIT_FID + 12

          // instantiate screen data
          client.pushScreenDefinition(new ScreenDefinition[] {editsd});

          editsb.putWidgetValue(MSG1_ID, new character(STD_MSG_1));
          editsb.putWidgetValue(MSG2_ID, new character(STD_MSG_2 + database));
       }
    }