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. |
* | ----------------------------- |
* | <OK> |
* +-------------------------------+
*
* Otherwise will print the P2J counter-part message of the 710 dialog of P4GL.
* +---------------------- Error ----------------------+
* | ** Your Password and Userid x do not match. (710) |
* | |
* | ------------------------------------------------- |
* | <OK> |
* +---------------------------------------------------+
*
* @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: <ldbname> |
* | |
* | User Id: ________________ |
* | Password: ________________ |
* | |
* | <OK> <Cancel> |
* +---------------------------------------------+
* </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));
}
}