package org.rhq;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.naming.CompositeName;
import javax.naming.Context;
import javax.naming.InvalidNameException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.InvalidSearchFilterException;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.border.LineBorder;
import javax.swing.border.TitledBorder;
//import org.rhq.enterprise.server.util.security.UntrustedSSLSocketFactory;
/* Is a development test tool that allows the user to simulate the RHQ server side
* LDAP calls during auth/authz operations.
*
* The specific LDAP logic below needs to mirror the latest RHQ code and allow the user
* to test our their configuration without requiring a specific RHQ/JON build as a dependency.
*
* NOTE: To avoid a runtime dependency on specific versions of RHQ or JON, the small implementation
* methods were copied into this class with minimal changes for logging and ui messaging. The
* definitive implementation for each 'copied' method can be found in LDAPGroupManagerBean.
*
* @author Simeon Pinder
*/
public class TestLdapSettings extends JFrame {
//shared fields
private JTextArea testResults;
private JCheckBox showPasswords;
private JCheckBox ssl;
private JLabel groupPageSizeName;
private JTextField groupMemberQueryValue;
private JTextField testUserNameValue;
private JTextField testUserPasswordValue;
private HashMap<String, JTextField> fieldMappings;
private String[] keys;
private JCheckBox enableLdapReferral;
private JCheckBox enableVerboseDebugging;
private JCheckBox enableVerboseGroupParsing;
private JCheckBox iterativeVerboseLogging;
private JCheckBox enablePosixGroups;
private JCheckBox enable32xFeatures;
private JMenuBar menuBar;
private String advdb = "**Verbose:debug ----";
private static final String BASEDN_DELIMITER = ";";
private String userDN;
private static final long serialVersionUID = 1L;
int textBoxWidth = 20;
private static JPanel top = null;
private static JPanel testUserRegion = null;
private static Properties env=null;
public static void main(String args[]) {
new TestLdapSettings();
}
//After enabling support for Query parsing, we need to warn users of the effects.
final String warnMessage = "<html>***WARNING: Depending upon<br>" +
"i)how the ldap server is configured <br>" +
"ii)client query paging settings <br>" +
" enabling <b>'more verbose logging'</b>,<br>" +
" <b>'more detailed group parsing'</b> AND/OR <b>'also log to console'</b> may cause the console to hang/freeze <br>" +
" as the LDAP tool continues to parse large query results. If that occurs it is <br>" +
" suggested that you stop this tool and re-run your queries with <b>'also log to console'</b> so that the console logs<br>" +
" will show which dataset is causing the delay and then you should modify your search|group|member<br>" +
" filters accordingly to <b>return smaller results</b> and/or <b>consume larger payloads</b>.<br>" +
"***WARNING</html>";
// Configure window properties
private TestLdapSettings() {
setTitle("Check LDAP Settings: Simulates LDAP checks/queries of RHQ LDAP integration");
getContentPane().setLayout(new BorderLayout());
menuBar = new JMenuBar();
JMenu menu = new JMenu("View ***Warning");
JMenuItem menuItem = new JMenuItem(warnMessage);
menu.add(menuItem);
menuBar.add(menu);
setJMenuBar(menuBar);
// top panel definition
top = new JPanel();
top.setLayout(new BoxLayout(top, BoxLayout.Y_AXIS));
top.setBorder(LineBorder.createGrayLineBorder());
//define checkbox here as it's checked when generating UI.
showPasswords = new JCheckBox("show passwords:");
showPasswords.setSelected(false);
keys = new String[] { "URL:", "Search Filter:",
"Search Base:","Login Property",
"Username:", "Group Search Filter:",
"Password:", "Group Member Filter:",
};
fieldMappings = loadUiFields(top, keys);
//add the two checkboxes for additiona debugging options
enableLdapReferral= new JCheckBox("[follow] ldap referrals");
enableLdapReferral.setSelected(false);
enableVerboseDebugging= new JCheckBox("more verbose logging");
enableVerboseDebugging.setSelected(false);
enableVerboseDebugging.setToolTipText(warnMessage);
enableVerboseGroupParsing= new JCheckBox("more detailed group parsing");
enableVerboseGroupParsing.setSelected(false);
enableVerboseGroupParsing.setToolTipText("*Take care when using this mode with a large number of groups* Every group discovered is parsed/listed.");
iterativeVerboseLogging= new JCheckBox("also log to console");
iterativeVerboseLogging.setSelected(false);
iterativeVerboseLogging.setToolTipText("This mode is useful when the test tool is having difficulty returning results from large queries.");
iterativeVerboseLogging.setToolTipText(warnMessage);
enablePosixGroups= new JCheckBox("is Posix Group");
enablePosixGroups.setSelected(false);
enablePosixGroups.setEnabled(false);
//put into 3.2.x functionality row
JPanel jon32xRegion = new JPanel();
jon32xRegion.setLayout(new FlowLayout(FlowLayout.LEFT));
LineBorder jon32xLineBorder = new LineBorder(Color.BLACK, 2);
TitledBorder jon32xBorder = new TitledBorder(jon32xLineBorder, "JON 3.2.x/RHQ 4.8.x specific features:");
jon32xRegion.setBorder(jon32xBorder);
enable32xFeatures= new JCheckBox("enable JON 3.2.x/RHQ 4.8.x features");
enable32xFeatures.setToolTipText("This enables features not available before RHQ 4.8.x/JON 3.2.x.");
enable32xFeatures.setSelected(false);
enable32xFeatures.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
if(enable32xFeatures.isSelected()){
groupPageSizeName.setEnabled(true);
groupMemberQueryValue.setEnabled(true);
groupMemberQueryValue.setEditable(true);
groupMemberQueryValue.setText("1000");
enablePosixGroups.setEnabled(true);
}else{
groupMemberQueryValue.setText("");
groupPageSizeName.setEnabled(false);
groupMemberQueryValue.setEnabled(false);
groupMemberQueryValue.setEditable(false);
enablePosixGroups.setEnabled(false);
enablePosixGroups.setSelected(false);
}
}
});
jon32xRegion.add(enable32xFeatures);
groupPageSizeName = new JLabel("Group Query Page Size:");
groupPageSizeName.setEnabled(false);
groupMemberQueryValue = new JTextField(10);
groupMemberQueryValue.setText("1000");
groupMemberQueryValue.setEditable(false);
jon32xRegion.add(groupPageSizeName);
jon32xRegion.add(groupMemberQueryValue);
jon32xRegion.add(enablePosixGroups);
top.add(jon32xRegion);
//put into row display
JPanel advancedDebugRegion = new JPanel();
advancedDebugRegion.setLayout(new FlowLayout(FlowLayout.LEFT));
LineBorder advancedBorder = new LineBorder(Color.BLACK, 2);
TitledBorder debugBorder = new TitledBorder(advancedBorder, "Debug: **Warning --<hover HERE>**");
advancedDebugRegion.setBorder(debugBorder);
advancedDebugRegion.add(enableLdapReferral);
advancedDebugRegion.add(enableVerboseDebugging);
advancedDebugRegion.add(enableVerboseGroupParsing);
advancedDebugRegion.add(iterativeVerboseLogging);
advancedDebugRegion.setToolTipText(warnMessage);
top.add(advancedDebugRegion);
JPanel securityPanel = new JPanel();
securityPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
showPasswords.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
//store off existing value
String existingValue = "";
String existingTestUserPass = "";
JTextField current = fieldMappings.get("Password:");
if(current instanceof JPasswordField){
JPasswordField pass = ((JPasswordField)current);
if(pass!=null){
char[] charArray = pass.getPassword();
if(charArray.length>0){
existingValue = new String(charArray);
}
}
}else{
existingValue = current.getText();
}
//save off test user password as well
if(testUserPasswordValue instanceof JPasswordField){
JPasswordField pass = ((JPasswordField)testUserPasswordValue);
if(pass!=null){
char[] charArray = pass.getPassword();
if(charArray.length>0){
existingTestUserPass = new String(charArray);
}
}
}else{
existingTestUserPass=testUserPasswordValue.getText();
}
JTextField updatedContainer = null;
if(showPasswords.isSelected()){
updatedContainer = new JTextField(textBoxWidth);
updatedContainer.setText(existingValue);
testUserPasswordValue = new JTextField(textBoxWidth);
testUserPasswordValue.setText(existingTestUserPass);
}else{
updatedContainer = new JPasswordField(textBoxWidth);
updatedContainer.setText(existingValue);
testUserPasswordValue = new JPasswordField(textBoxWidth);
testUserPasswordValue.setText(existingTestUserPass);
}
//locate the JPanel and rebuild it Should be at index 3
JPanel passwordRow = (JPanel) top.getComponent(3);
// JTextField jf = (JTextField) passwordRow.getComponent(1);
//store off existing components
Component[] existing = new Component[passwordRow.getComponentCount()];
for(int i=0; i<passwordRow.getComponentCount();i++){
existing[i] = passwordRow.getComponent(i);
}
passwordRow.removeAll();
for(int j=0;j<existing.length;j++){
if(j==1){//insert new JTextField instead
passwordRow.add(updatedContainer);
}else{
passwordRow.add(existing[j]);
}
}
//reload testUserRegion
//store off existing components
Component[] existingTest = new Component[testUserRegion.getComponentCount()];
for(int i=0; i<testUserRegion.getComponentCount();i++){
existingTest[i] = testUserRegion.getComponent(i);
}
testUserRegion.removeAll();
for(int j=0;j<existingTest.length;j++){
if(j==3){//insert new JTextField instead
testUserRegion.add(testUserPasswordValue);
}else{
testUserRegion.add(existingTest[j]);
}
}
top.revalidate();
top.repaint();
}
});
}
});
securityPanel.add(showPasswords);
ssl = new JCheckBox("SSL:");
ssl.setEnabled(false);
securityPanel.add(ssl);
top.add(securityPanel);
// test user auth region
testUserRegion = new JPanel();
testUserRegion.setLayout(new FlowLayout(FlowLayout.LEFT));
LineBorder border = new LineBorder(Color.BLUE, 2);
TitledBorder tBorder = new TitledBorder(border, "Authentication/Authorization Check Credentials: (insert valid ldap user assigned to group)");
testUserRegion.setBorder(tBorder);
JLabel testUserName = new JLabel("Test UserName:");
testUserNameValue = new JTextField(textBoxWidth);
JLabel testUserPassword = new JLabel("Test Password:");
// testUserPasswordValue = new JTextField(textBoxWidth);
testUserPasswordValue = new JPasswordField(textBoxWidth);
testUserRegion.add(testUserName);
testUserRegion.add(testUserNameValue);
testUserRegion.add(testUserPassword);
testUserRegion.add(testUserPasswordValue);
top.add(testUserRegion);
// center
JPanel center = new JPanel();
center.setLayout(new BoxLayout(center, BoxLayout.X_AXIS));
// build center panel
buildCenterPanel(center);
// final component layout
getContentPane().add(top, BorderLayout.NORTH);
getContentPane().add(center, BorderLayout.CENTER);
this.setSize(720, 700);
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
setVisible(true);
}
// define the center display panel.
private void buildCenterPanel(JPanel center) {
// First element is Test Button
JButton test = new JButton("Test Settings");
center.add(test);
// second is large text box that display ldap queries
testResults = new JTextArea("(click button to test settings values: simulates 4 separate checks showing ldap filters used)",
40, 40);
JScrollPane jsp = new JScrollPane(testResults);
center.add(jsp);
test.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
testResults.setText("");//clear out empty msg
//trim spaces from all fields
String ldapServer = fieldMappings.get(keys[0]).getText().trim();
String searchFilter = fieldMappings.get(keys[1]).getText().trim();
String searchBase = fieldMappings.get(keys[2]).getText().trim();
String loginProperty = fieldMappings.get(keys[3]).getText().trim();
String bindUserName = fieldMappings.get(keys[4]).getText().trim();
String groupSearchFilter = fieldMappings.get(keys[5]).getText().trim();
String bindPassword = fieldMappings.get(keys[6]).getText().trim();
String groupMemberFilter = fieldMappings.get(keys[7]).getText().trim();
String groupMemberQuerySize = groupMemberQueryValue.getText().trim();
String testUserName = testUserNameValue.getText().trim();
String testUserPassword = testUserPasswordValue.getText().trim();
// validate initial required elements
String msg = null;
boolean proceed = true;
//valid required details set.
if (ldapServer.isEmpty() || bindUserName.isEmpty()
|| bindPassword.isEmpty() || searchBase.isEmpty()) {
msg ="STEP-1:FAIL: "+ keys[0] + ", " + keys[2] + ", " + keys[4] + ", "
+ keys[6] + " cannot be empty to proceed.";
log(msg);
proceed = false;
}
env = null;
InitialLdapContext ctx = null;
if (proceed) {// attempt initial ldap bind from RHQ server
msg = "STEP-1:TESTING: Attempting to bind to server:" + ldapServer
+ "\n with user '" + bindUserName
+ "' and password entered.";
log(msg);
env = getProperties(ldapServer);
env.setProperty(Context.SECURITY_PRINCIPAL, bindUserName);
env.setProperty(Context.SECURITY_CREDENTIALS, bindPassword);
env.setProperty(Context.SECURITY_AUTHENTICATION, "simple");
//put the rest of the LDAP properties into the Properties instance for use later.
//there still needs to be separate variables since some are for UI validation.
env.setProperty(SystemSetting.LDAP_GROUP_FILTER.getInternalName(), groupSearchFilter);
env.setProperty(SystemSetting.LDAP_GROUP_MEMBER.getInternalName(), groupMemberFilter);
env.setProperty(SystemSetting.LDAP_BASE_DN.getInternalName(), searchBase);
env.setProperty(SystemSetting.LDAP_LOGIN_PROPERTY.getInternalName(), loginProperty);
env.setProperty(SystemSetting.LDAP_BIND_DN.getInternalName(), bindUserName);
env.setProperty(SystemSetting.LDAP_BIND_PW.getInternalName(), bindPassword);
env.setProperty(SystemSetting.LDAP_GROUP_QUERY_PAGE_SIZE.getInternalName(), groupMemberQuerySize);
try {
ctx = new InitialLdapContext(env, null);
msg = "STEP-1:PASS: LDAP bind credentials are correct. Successfully connected to '"
+ ldapServer
+ "'.\n This means the LDAP Bind credentials for the RHQ Server authentication/authorization requests to ldap server "
+ "are correct.";
if(enableVerboseDebugging.isSelected()){
msg+="\n"+advdb+" LDAP simple authentication bind successful.";
}
log(msg);
proceed = true;
} catch (Exception ex) {
msg = "STEP-1:FAIL: Unable to connect to the LDAP server with credentials specified.\n";
msg+="Exception:"+ex.getMessage();
if(enableVerboseDebugging.isSelected()){
msg = appendStacktraceToMsg(msg, ex);
}
log(msg);
proceed = false;
}
}
if (proceed) {// retrieve test credentials to test run auth
// load search controls
SearchControls searchControls = getSearchControls();
// validating searchFilter and test user/pass creds
proceed = true;
if (testUserName.isEmpty() || (testUserPassword.isEmpty())) {
msg = "STEP-2:FAIL: Test Username/Password fields cannot be empty for this step.";
log(msg);
proceed = false;
}
// testing a valid user involves a filtered ldap search
// using the loginProperty, and optionally searchFilter
userDN = "";
if (proceed) {
// default loginProperty to cn if it's not set
if (loginProperty.isEmpty()) {
loginProperty = "cn";
if(enableVerboseDebugging.isSelected()){
String mesg = "As you have not specified a login property, defaulting to 'cn'";
log(advdb+" "+msg);
}
}
String filter;
if (!searchFilter.isEmpty()) {
filter = "(&(" + loginProperty + "=" + testUserName
+ ")" + "(" + searchFilter + "))";
} else {
filter = "(" + loginProperty + "=" + testUserName
+ ")";
}
if(enableVerboseDebugging.isSelected()){
log(advdb+" The searchfilter is optionally appended to login property for additional shared attribute across users.");
}
msg = "STEP-2:TESTING: To validate the test user the following LDAP filtered component will be used to find matching users:\n";
msg += filter;
log(msg);
// test out the search on the target ldap server
try {
String[] baseDNs = searchBase.split(";");
for (int x = 0; x < baseDNs.length; x++) {
NamingEnumeration answer = ctx.search(
baseDNs[x], filter, searchControls);
if(enableVerboseDebugging.isSelected()){
log(advdb+" this search was excuted against DN component '"+baseDNs[x]+"'.");
}
// boolean ldapApiNpeFound = false;
if (!answer.hasMoreElements()) {
msg="STEP-2:WARN Unable to locate a matching users for the filter'"+filter+
"'. Please check your loginProperty. Usually 'cn' or 'uid'";
log(msg);
continue;
}
// Going with the first match
SearchResult si = (SearchResult) answer.next();
constructUserDn(baseDNs, x, si);
msg = "STEP-2:PASS: The test user '"
+ testUserName
+ "' was succesfully located, and the following userDN will be used in authorization check:\n";
msg += userDN;
log(msg);
ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, userDN);
ctx.addToEnvironment(Context.SECURITY_CREDENTIALS,testUserPassword);
ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION,"simple");
// if successful then verified that user and pw
// are valid ldap credentials
ctx.reconnect(null);
msg = "STEP-2:PASS: The user '"
+ testUserName
+ "' was succesfully authenticated using userDN '"
+ userDN + "' and password provided.\n"
+"*Note: the loginProperty must match the loginProperty listed in dn: for the user. It is the DN that RHQ will lookup and use.";
log(msg);
}
} catch (Exception ex) {
msg = "STEP-2:FAIL: There was an error while searching for or authenticating the user '"
+ testUserName + "'\n";
msg += ex.getMessage();
if(enableVerboseDebugging.isSelected()){
msg = appendStacktraceToMsg(msg, ex);
}
log(msg);
proceed=false;
}
}
// with authentication completed, now check authorization.
// validate filter components to list all available groups
proceed = false;
if (!groupSearchFilter.isEmpty()) {
Set<Map<String, String>> ret = new HashSet<Map<String, String>>();
String filter = null;
if (groupSearchFilter.startsWith("(") && groupSearchFilter.endsWith(")")){
filter = groupSearchFilter; // RFC 2254 does not allow for ((expression))
}else{
filter = String
.format("(%s)", groupSearchFilter);
}
msg = "STEP-3:TESTING: This ldap filter "
+ filter
+ " will be used to locate ALL available LDAP groups";
log(msg);
Properties systemConfig = populateProperties(env);
ret = buildGroup(systemConfig, filter);
msg = "STEP-3:TESTING: Using Group Search Filter '"
+ filter + "', " + ret.size()
+ " ldap group(s) were located.\n";
if (ret.size() > 0) {
HashMap<String, String>[] ldapLists = new HashMap[ret
.size()];
ret.toArray(ldapLists);
// in this mode go beyond the first ten results.
if (enableVerboseGroupParsing.isSelected()) {
msg += "STEP-3:PASS: Listing 'all' of the ldap groups located: \n";
for (int i = 0; i < ret.size(); i++) {
msg += ldapLists[i] + "\n";
}
} else {// otherwise only show first 10[subset of
// available groups]
msg += "STEP-3:PASS: Listing a few(<=10) of the ldap groups located: \n";
for (int i = 0; (i < ret.size() && i < 10); i++) {
msg += ldapLists[i] + "\n";
}
}
proceed = true;// then can proceed to next step.
}
log(msg);
} else {
msg = "STEP-3:FAIL: Group Search Filter: cannot be empty to proceed.";
log(msg);
proceed=false;
}
// retrieve lists of authorized groups available for the
if (proceed) {
// check groupMember
if (!groupMemberFilter.isEmpty()) {
// Map<String, String> userDetails = new HashMap<String, String>();
// userDetails = findLdapUserDetails(userDN);
Set<String> userDetails = findAvailableGroupsFor(testUserName);
if(!userDetails.isEmpty()){
proceed=true;
}
} else {
msg = "STEP-4:FAIL: Group Member Filter must be non-empty to proceed with simulating authorization check for test user.";
log(msg);
}
}
if(proceed){
msg="COMPLETED:PASS: The current settings, for successful steps, should be correct to enter into your RHQ server.";
msg+="\n\n\n\n When you encounter failures, warnings or other unexpected results you should use an external ";
msg+="LDAP search utility to check that the generated filters return the expected LDAP results.";
log(msg);
}
}
}
});
}
private String appendStacktraceToMsg(String msg, Exception ex) {
String moreVerbose = "";
moreVerbose+=advdb+" Exception type:"+ex.getClass()+"\n";
moreVerbose+=advdb+" Exception stack trace reference:"+ex.getStackTrace()+"\n";
if(ex.getStackTrace()!=null){
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
ex.printStackTrace(pw);
moreVerbose+=advdb+" stack trace reference:"+sw.toString();
}
msg+="\n"+moreVerbose;
return msg;
}
private boolean containsIllegalLdap(String currentValue) {
boolean invalidData = false;
if((currentValue!=null)&&(!currentValue.trim().isEmpty())){
//TODO: spinder 3/17/11: need to figure out regex to filter/detect bad data in returned ldap. Giving up for now.
// String regex = "(?<=(?:[^\\]|^)(\\\\)+|[^\\]|^)[/,+\"><;=#]|(?<=(?:[^\\]|^)(\\\\)+|[^\\]|^)\\(?!\\|[/,+\"><;=#]| $|(?<=^\\) )|^";
// regex = "(?<=(?:[^\\\\]|^)(\\\\\\\\)+|[^\\\\]|^)[/,+\\\"><;=#]|(?<=(?:[^\\\\]|^)(\\\\\\\\)+|[^\\\\]|^)\\\\(?!\\\\|[/,+\\\"><;=#]| $|(?<=^\\\\) )|^";
// System.out.println("++++++++ CURR VAL:"+currentValue+":INV-CHeck:"+currentValue.matches(",+\"\\<;\n=/")+":NEWCHECK:"+(currentValue.matches(regex)));
// if(currentValue.matches(",+\"\\<;\n=/")){
// invalidData=true;
// }
// String badList = ",+\"\\<;\n=";
String badList = "+\"\\<;\n";
for(char car :currentValue.toCharArray()){
for(char c :badList.toCharArray()){
if(car == c){
invalidData=true;
}
}
}
}
return invalidData;
}
/**
* @throws NamingException
* @see org.jboss.security.auth.spi.UsernamePasswordLoginModule#validatePassword(java.lang.String,java.lang.String)
*/
protected Set<Map<String, String>> buildGroup(Properties systemConfig, String filter) {
Set<Map<String, String>> groupDetailsMap = new HashSet<Map<String, String>>();
// Load our LDAP specific properties
// Load the BaseDN
String baseDN = (String) systemConfig.get(SystemSetting.LDAP_BASE_DN.getInternalName());
// Load the LoginProperty
String loginProperty = (String) systemConfig.get(SystemSetting.LDAP_LOGIN_PROPERTY.getInternalName());
if (loginProperty == null) {
// Use the default
loginProperty = "cn";
}
// Load any information we may need to bind
String bindDN = (String) systemConfig.get(SystemSetting.LDAP_BIND_DN.getInternalName());
String bindPW = (String) systemConfig.get(SystemSetting.LDAP_BIND_PW.getInternalName());
if (bindDN != null) {
systemConfig.setProperty(Context.SECURITY_PRINCIPAL, bindDN);
systemConfig.setProperty(Context.SECURITY_CREDENTIALS, bindPW);
systemConfig.setProperty(Context.SECURITY_AUTHENTICATION, "simple");
}
try {
InitialLdapContext ctx = new InitialLdapContext(systemConfig, null);
SearchControls searchControls = getSearchControls();
/*String filter = "(&(objectclass=groupOfUniqueNames)(uniqueMember=uid=" + userName
+ ",ou=People, dc=rhndev, dc=redhat, dc=com))";*/
//modify the search control to only include the attributes we will use
String[] attributes = { "cn", "description" };
searchControls.setReturningAttributes(attributes);
//BZ:964250: add rfc 2696
//default to 1000 results. System setting page size from UI should be non-negative integer > 0.
//additionally as system settings are modifiable via CLI which may not have param checking enabled do some
//more checking.
int defaultPageSize = 1000;
// only if they're enabled in the UI.
if (enable32xFeatures.isSelected()) {
String groupPageSize = systemConfig.getProperty(
SystemSetting.LDAP_GROUP_QUERY_PAGE_SIZE
.getInternalName(), "" + defaultPageSize);
if ((groupPageSize != null)
&& (!groupPageSize.trim().isEmpty())) {
int passedInPageSize = -1;
try {
passedInPageSize = Integer
.valueOf(groupPageSize.trim());
if (passedInPageSize > 0) {
defaultPageSize = passedInPageSize;
if(enableVerboseDebugging.isSelected()){
log(advdb
+ " LDAP Group Query Page Sizing of '"+defaultPageSize+"' is being requested from server.");
}
}
} catch (NumberFormatException nfe) {
// log issue and do nothing. Go with the default.
String msg = "LDAP Group Page Size passed in '"
+ groupPageSize
+ "' in is invalid. Defaulting to 1000 results."
+ nfe.getMessage();
log(msg);
}
}
ctx.setRequestControls(new Control[] { new PagedResultsControl(
defaultPageSize, Control.CRITICAL) });
}
// Loop through each configured base DN. It may be useful
// in the future to allow for a filter to be configured for
// each BaseDN, but for now the filter will apply to all.
String[] baseDNs = baseDN.split(BASEDN_DELIMITER);
for (int x = 0; x < baseDNs.length; x++) {
if (enableVerboseDebugging.isSelected()) {
log(advdb
+ " this search was excuted against DN component '"
+ baseDNs[x] + "'.");
}
executeGroupSearch(filter, groupDetailsMap, ctx, searchControls, baseDNs, x);
// continually parsing pages of results until we're done.
// only if they're enabled in the UI.
if (enable32xFeatures.isSelected()) {
// handle paged results if they're being used here
byte[] cookie = null;
Control[] controls = ctx.getResponseControls();
if (controls != null) {
for (Control control : controls) {
if (control instanceof PagedResultsResponseControl) {
PagedResultsResponseControl pagedResult = (PagedResultsResponseControl) control;
cookie = pagedResult.getCookie();
}
}
}
while (cookie != null) {
String msg = "RFC 2696 is supported by the server and we are paging through the results. "+
groupDetailsMap.size()+" results returned so far.";
if(enableVerboseGroupParsing.isSelected()){
log(advdb
+ msg);
}
// ensure the next requests contains the session/cookie
// details
ctx.setRequestControls(new Control[] { new PagedResultsControl(
defaultPageSize, cookie, Control.CRITICAL) });
executeGroupSearch(filter, groupDetailsMap, ctx,
searchControls, baseDNs, x);
// empty out cookie
cookie = null;
// test for further iterations
controls = ctx.getResponseControls();
if (controls != null) {
for (Control control : controls) {
if (control instanceof PagedResultsResponseControl) {
PagedResultsResponseControl pagedResult = (PagedResultsResponseControl) control;
cookie = pagedResult.getCookie();
}
}
}
}
}
}//end of for loop
} catch (NamingException e) {
if (e instanceof InvalidSearchFilterException) {
InvalidSearchFilterException fException = (InvalidSearchFilterException) e;
String message = "The ldap group filter defined is invalid ";
log(message);
}
//TODO: check for ldap connection/unavailable/etc. exceptions.
else {
String mesg = "LDAP communication error: " + e.getMessage();
log(mesg);
}
} catch (IOException iex) {
String msg = "Unexpected LDAP communciation error:" + iex.getMessage();
log(msg);
}
return groupDetailsMap;
}
/** Executes the LDAP group query using the filters, context and search controls, etc. parameters passed in.
* The matching groups located during processing this pages of results are added as new entries to the
* groupDetailsMap passed in.
*
* @param filter
* @param groupDetailsMap
* @param ctx
* @param searchControls
* @param baseDNs
* @param x
* @throws NamingException
*/
private void executeGroupSearch(String filter, Set<Map<String, String>> groupDetailsMap, InitialLdapContext ctx,
SearchControls searchControls, String[] baseDNs, int x) throws NamingException {
//execute search based on controls and context passed in.
NamingEnumeration<SearchResult> answer = ctx.search(baseDNs[x], filter, searchControls);
boolean ldapApiEnumerationBugEncountered = false;
while ((!ldapApiEnumerationBugEncountered) && answer.hasMoreElements()) {//BZ:582471- ldap api bug change
// We use the first match
SearchResult si = null;
try {
si = answer.next();
} catch (NullPointerException npe) {
if (enableVerboseDebugging.isSelected()) {
log(advdb
+ " NullPtr exception detected. If known LDAP api enum npe ignore: "
+ npe.getMessage() + ".");
}
ldapApiEnumerationBugEncountered = true;
break;
}
if (enableVerboseDebugging.isSelected()
|| enableVerboseGroupParsing.isSelected()) {
Attributes attributeContainer = si.getAttributes();
NamingEnumeration<? extends Attribute> attributes = attributeContainer
.getAll();
String attributesReturned = " ";
while (attributes.hasMore()) {
attributesReturned += attributes.next().getID() + ",";
}
String dbugMesg = "\n"
+ advdb
+ " Group search LDAP ("
+ attributeContainer.size()
+ ") attributes located for group '"
+ si.getName()
+ "' are ["
+ attributesReturned.substring(0,
attributesReturned.length() - 1) + "].";
// directly update here to shorten messages for lots of groups
testResults.setText(testResults.getText() + dbugMesg);
//This flag can be used in the unlikely case that the UI hangs during a test operation.:
if(iterativeVerboseLogging.isSelected()){
System.out.println(dbugMesg);
}
// additionally parse attribute ids and values for illegal ldap
// characters
if (enableVerboseGroupParsing.isSelected()) {
attributes = attributeContainer.getAll();
String currentAttributeId = "";
String currentValue = "";
// spinder: 3/17/11: should we bail on first bad data or
// display them all?
while (attributes.hasMore()) {
boolean badData = false;
Attribute att = attributes.next();
currentAttributeId = att.getID();
if (containsIllegalLdap(currentAttributeId)) {
log(advdb
+ " LDAP Group: bad atrribute data detected for group '"
+ si.getName() + "' for attribute '"
+ currentAttributeId + "'.");
badData = true;
}
if (att.getAll() != null) {
NamingEnumeration<?> enumer = att.getAll();
while (enumer.hasMore()) {
currentValue = enumer.next() + "";
if (containsIllegalLdap(currentValue)) {
log(advdb
+ " LDAP Group: bad data detected for group '"
+ si.getName()
+ "' with attribute '"
+ currentAttributeId
+ "' and value:" + currentValue);
badData = true;
}
}
}
if (badData) {
log(advdb
+ "** LDAP Group: Some bad LDAP data detected for group '"
+ si.getName() + "'.");
}
}
}
}
Map<String, String> entry = new HashMap<String, String>();
// String name = (String) si.getAttributes().get("cn").get();
Attribute commonNameAttr = si.getAttributes().get("cn");
if (commonNameAttr != null) {
String name = (String) commonNameAttr.get();
name = name.trim();
Attribute desc = si.getAttributes().get("description");
String description = desc != null ? (String) desc.get() : "";
description = description.trim();
entry.put("id", name);
entry.put("name", name);
entry.put("description", description);
groupDetailsMap.add(entry);
} else {// unable to retrieve details for specific group.
log(advdb
+ " There was an error retrieving 'cn' attribute for group '"
+ si.getName()
+ "'. Not adding to returned list of groups. ");
}
}
}
public Map<String, String> findLdapUserDetails(String userName) {
// Load our LDAP specific properties
Properties systemConfig = env;
HashMap<String, String> userDetails = new HashMap<String, String>();
// Load the BaseDN
String baseDN = (String) systemConfig.get(SystemSetting.LDAP_BASE_DN.getInternalName());
// Load the LoginProperty
String loginProperty = (String) systemConfig.get(SystemSetting.LDAP_LOGIN_PROPERTY.getInternalName());
if (loginProperty == null) {
// Use the default
loginProperty = "cn";
}
// Load any information we may need to bind
String bindDN = (String) systemConfig.get(SystemSetting.LDAP_BIND_DN.getInternalName());
String bindPW = (String) systemConfig.get(SystemSetting.LDAP_BIND_PW.getInternalName());
// Load any search filter
String groupSearchFilter = (String) systemConfig.get(SystemSetting.LDAP_GROUP_FILTER.getInternalName());
String groupMemberFilter = (String) systemConfig.get(SystemSetting.LDAP_GROUP_MEMBER.getInternalName());
String testUserDN = userDN;
String ldapServer = (String) systemConfig.get(Context.PROVIDER_URL);
Properties env = getProperties(ldapServer);
if (bindDN != null) {
env.setProperty(Context.SECURITY_PRINCIPAL, bindDN);
env.setProperty(Context.SECURITY_CREDENTIALS, bindPW);
env.setProperty(Context.SECURITY_AUTHENTICATION, "simple");
}
try {
InitialLdapContext ctx = new InitialLdapContext(env, null);
SearchControls searchControls = getSearchControls();
String filter = String.format("(&(%s)(%s=%s))",
groupSearchFilter, groupMemberFilter,
// testUserDN); BZ 707047
encodeForFilter(testUserDN));
generateUiLoggingForStep4LdapFilter(userName, filter);
// Loop through each configured base DN. It may be useful
// in the future to allow for a filter to be configured for
// each BaseDN, but for now the filter will apply to all.
String[] baseDNs = baseDN.split(BASEDN_DELIMITER);
for (int x = 0; x < baseDNs.length; x++) {
NamingEnumeration<SearchResult> answer = ctx.search(baseDNs[x], filter, searchControls);
if (!answer.hasMoreElements()) { //BZ:582471- ldap api bug change
// Nothing found for this DN, move to the next one if we have one.
continue;
}
// We use the first match
SearchResult si = answer.next();
// Construct the UserDN
constructUserDn(baseDNs, x, si);
userDetails.put("dn", userDN);
// Construct the UserDN
NamingEnumeration<String> keys = si.getAttributes().getIDs();
while (keys.hasMore()) {
String key = keys.next();
Attribute value = si.getAttributes().get(key);
if ((value != null) && (value.get() != null)) {
userDetails.put(key, value.get().toString());
}
}
// return userDetails;
}//end of for loop
generateUiLoggingStep4Authz(filter);
return userDetails;
} catch (Exception ex) {
generateUiLoggingStep4Exception(ex);
}
return userDetails;
}
/* Construct UserDn.
*
*/
private void constructUserDn(String[] baseDNs, int x, SearchResult si) throws InvalidNameException {
userDN = null;
try {
userDN = si.getNameInNamespace();
} catch (UnsupportedOperationException use) {
userDN = new CompositeName(si.getName()).get(0);
if (si.isRelative()) {
userDN += "," + baseDNs[x];
}
}
}
public Set<String> findAvailableGroupsFor(String userName) {
// Load our LDAP specific properties
Properties options = env;
String groupFilter = options.getProperty(SystemSetting.LDAP_GROUP_FILTER.getInternalName(), "");
String groupMember = options.getProperty(SystemSetting.LDAP_GROUP_MEMBER.getInternalName(), "");
String groupUsePosix = options.getProperty(SystemSetting.LDAP_GROUP_USE_POSIX.getInternalName(), "false");
if (groupUsePosix == null) {
groupUsePosix = Boolean.toString(false);//default to false
}
boolean usePosixGroups = Boolean.valueOf(groupUsePosix);
String userAttribute = getUserAttribute(options, userName, usePosixGroups);
Set<String> ldapSet = new HashSet<String>();
if (userAttribute != null && userAttribute.trim().length() > 0) {
//TODO: spinder 4/21/10 put in error/debug logging messages for badly formatted filter combinations
String filter = "";
//form assumes examples where groupFilter is like 'objectclass=groupOfNames' and groupMember is 'member'
// to produce ldap filter like (&(objectclass=groupOfNames)(member=cn=Administrator,ou=People,dc=test,dc=com))
// or like (&(objectclass=groupOfNames)(memberUid=Administrator)) for posixGroups.
filter = String.format("(&(%s)(%s=%s))", groupFilter, groupMember, encodeForFilter(userAttribute));
Set<Map<String, String>> matched = buildGroup(options, filter);
// log.trace("Located '" + matched.size() + "' LDAP groups for user '" + userName
// + "' using following ldap filter '" + filter + "'.");
//iterate to extract just the group names.
for (Map<String, String> match : matched) {
ldapSet.add(match.get("id"));
}
} else {
// log.debug("Group lookup will not be performed due to no UserDN found for user " + userName);
}
return ldapSet;
}
private void generateUiLoggingStep4Exception(Exception ex) {
String groupSearchFilter = env
.getProperty(SystemSetting.LDAP_GROUP_FILTER
.getInternalName());
String msg = "STEP-4:FAIL: There was an error searching with the groupFilter supplied: "
+ groupSearchFilter + "'\n";
msg += ex.getMessage();
if (enableVerboseDebugging.isSelected()) {
msg = appendStacktraceToMsg(msg, ex);
}
log(msg);
}
private void generateUiLoggingStep4Authz(String filter) {
Set<Map<String, String>> groups = buildGroup(env, filter);
String msg = "STEP-4:TESTING: Using Group Search Filter '"
+ filter + "', " + groups.size()
+ " ldap group(s) were located.\n";
if (groups.size() > 0) {
HashMap<String, String>[] ldapLists = new HashMap[groups
.size()];
groups.toArray(ldapLists);
msg += "STEP-4:PASS: Listing a few of the ldap groups located: \n";
// iterate over first ten or less to demonstrate retrieve
for (int i = 0; (i < groups.size() && i < 10); i++) {
msg += ldapLists[i] + "\n";
}
}else{
msg+="STEP-4:WARN: With current settings, test user is not authorized for any groups. Is this correct?";
}
log(msg);
}
private void generateUiLoggingForStep4LdapFilter(String userName,
String filter) {
String msg = "STEP-4:TESTING: about to do ldap search with filter \n'"
+ filter
+ "'\n to locate groups that test user '"+userName+"' IS authorized to access.";
log(msg);
}
// throw the label and fields together, two to a row.
private HashMap<String, JTextField> loadUiFields(JPanel top,
String[] componentKeys) {
HashMap<String, JTextField> mappings = new HashMap<String, JTextField>();
for (int i = 0; i < componentKeys.length; i++) {
String firstLabelKey = componentKeys[i];
String secondLabelKey = componentKeys[++i];
// locate second key
JPanel row = new JPanel();
row.setLayout(new FlowLayout(FlowLayout.LEFT));
JLabel label1 = new JLabel(firstLabelKey);
label1.setSize(textBoxWidth, 5);
// JTextField value1 = new JTextField(textBoxWidth);
JTextField value1 = null;
if (firstLabelKey.equalsIgnoreCase("Password:")&&(!showPasswords.isSelected())) {
value1 = new JPasswordField(textBoxWidth);
} else {
value1 = new JTextField(textBoxWidth);
}
JLabel label2 = new JLabel(secondLabelKey);
JTextField value2 = new JTextField(textBoxWidth);
row.add(label1);
row.add(value1);
row.add(Box.createRigidArea(new Dimension(0, 5)));
row.add(label2);
row.add(value2);
mappings.put(firstLabelKey, value1);
mappings.put(secondLabelKey, value2);
top.add(row);
}
return mappings;
}
private Properties getProperties(String contentProvider) {
Properties env = new Properties();
env.setProperty(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
env.setProperty(Context.PROVIDER_URL, contentProvider);
if(!enableLdapReferral.isSelected()){
env.setProperty(Context.REFERRAL, "ignore");
}else{
String msg="**---- You have chosen to tell LDAP servers to [FOLLOW] context referrals. Default is [IGNORE] referrals. --**";
log(msg);
env.setProperty(Context.REFERRAL, "follow");
}
// // Setup SSL if requested
// String protocol = ssl.isSelected()? "ssl":"";
// if ((protocol != null) && protocol.equals("ssl")) {
// String ldapSocketFactory = env
// .getProperty("java.naming.ldap.factory.socket");
// if (ldapSocketFactory == null) {
// env.put("java.naming.ldap.factory.socket",
// UntrustedSSLSocketFactory.class.getName());
// }
// env.put(Context.SECURITY_PROTOCOL, "ssl");
// }
return env;
}
private String delineate() {
String line = "-";
for (int i = 0; i < 30; i++) {
line += "-";
}
return line;
}
/** Takes care of delineating messages and conditional logging contents passed in.
* @param msg
*/
private void log(String msg) {
String message = "\n" + delineate() + "\n";
message += msg;
message += "\n" + delineate() + "\n\n";
//This flag can be used in the unlikely case that the UI hangs during a test operation.:
if(iterativeVerboseLogging.isSelected()){
System.out.println(message);
}
testResults.setText(testResults.getText() + message);
}
private SearchControls getSearchControls() {
int scope = SearchControls.SUBTREE_SCOPE;
int timeLimit = 0;
long countLimit = 0;
String[] returnedAttributes = null;
boolean returnObject = false;
boolean deference = false;
SearchControls constraints = new SearchControls(scope, countLimit,
timeLimit, returnedAttributes, returnObject, deference);
return constraints;
}
/** Translate SystemSettings to familiar Properties instance since we're
* passing not one but multiple values.
*
* @param systemSettings
* @return
*/
private Properties populateProperties(Properties existing) {
Properties properties = new Properties();
if(existing!=null){
properties = existing;
}
for (SystemSetting entry : SystemSetting.values()) {
if(entry!=null){
switch(entry){
case LDAP_BASED_JAAS_PROVIDER:
properties.put(entry.getInternalName(), "");
break;
}
}
}
return properties;
}
/**Build/retrieve the user DN. Not usually a property.
*
* @param options
* @param userName
* @param usePosixGroups boolean indicating whether we search for groups with posixGroup format
* @return
*/
private String getUserAttribute(Properties options, String userName, boolean usePosixGroups) {
Map<String, String> details = findLdapUserDetails(userName);
String userAttribute = null;
if (usePosixGroups) {//return just the username as posixGroup member search uses (&(%s)(memberUid=username))
userAttribute = userName;
} else {//this is the default where group search uses (&(%s)(uniqueMember={userDn}))
userAttribute = details.get("dn");
}
return userAttribute;
}
/** See LDAPStringUtil.encodeForFilter() for original code/source/author/etc.
* <p>Encode a string so that it can be used in an LDAP search filter.</p>
*
* <p>The following table shows the characters that are encoded and their
* encoded version.</p>
*
* <table>
* <tr><th align="center">Character</th><th>Encoded As</th></tr>
* <tr><td align="center">*</td><td>\2a</td></tr>
* <tr><td align="center">(</td><td>\28</td></tr>
* <tr><td align="center">)</td><td>\29</td></tr>
* <tr><td align="center">\</td><td>\5c</td></tr>
* <tr><td align="center"><code>null</code></td><td>\00</td></tr>
* </table>
*
* <p>In addition to encoding the above characters, any non-ASCII character
* (any character with a hex value greater then <code>0x7f</code>) is also
* encoded and rewritten as a UTF-8 character or sequence of characters in
* hex notation.</p>
*
* @param filterString a string that is to be encoded
* @return the encoded version of <code>filterString</code> suitable for use
* in a LDAP search filter
* @see <a href="http://tools.ietf.org/html/rfc4515">RFC 4515</a>
*/
public static String encodeForFilter(final String filterString) {
if (filterString != null && filterString.length() > 0) {
StringBuilder encString = new StringBuilder(filterString.length());
for (int i = 0; i < filterString.length(); i++) {
char ch = filterString.charAt(i);
switch (ch) {
case '*': // encode a wildcard * character
encString.append("\\2a");
break;
case '(': // encode a open parenthesis ( character
encString.append("\\28");
break;
case ')': // encode a close parenthesis ) character
encString.append("\\29");
break;
case '\\': // encode a backslash \ character
encString.append("\\5c");
break;
case '\u0000': // encode a null character
encString.append("\\00");
break;
default:
if (ch <= 0x7f) { // an ASCII character
encString.append(ch);
} else if (ch >= 0x80) { // encode to UTF-8
try {
byte[] utf8bytes = String.valueOf(ch).getBytes("UTF8");
for (byte b : utf8bytes) {
encString.append(String.format("\\%02x", b));
}
} catch (UnsupportedEncodingException e) {
// ignore
}
}
}
}
return encString.toString();
}
return filterString;
}
}
//Mock up the upgraded system properties approach to use SystemSetting
enum SystemSetting {
LDAP_BASED_JAAS_PROVIDER("CAM_JAAS_PROVIDER"),
LDAP_NAMING_PROVIDER_URL("CAM_LDAP_NAMING_PROVIDER_URL"),
USE_SSL_FOR_LDAP("CAM_LDAP_PROTOCOL"),
LDAP_LOGIN_PROPERTY("CAM_LDAP_LOGIN_PROPERTY"),
LDAP_FILTER("CAM_LDAP_FILTER"),
LDAP_GROUP_FILTER("CAM_LDAP_GROUP_FILTER"),
LDAP_GROUP_MEMBER("CAM_LDAP_GROUP_MEMBER"),
LDAP_GROUP_QUERY_PAGE_SIZE("CAM_LDAP_GROUP_QUERY_PAGE_SIZE"),
LDAP_BASE_DN("CAM_LDAP_BASE_DN"),
LDAP_BIND_DN("CAM_LDAP_BIND_DN"),
LDAP_BIND_PW("CAM_LDAP_BIND_PW"),
LDAP_NAMING_FACTORY("CAM_LDAP_NAMING_FACTORY_INITIAL"),
LDAP_GROUP_USE_POSIX("CAM_LDAP_GROUP_USE_POSIX"),
;
private String internalName;
private SystemSetting(String name) {
this.internalName = name;
}
public String getInternalName() {
return internalName;
}
public static SystemSetting getByInternalName(String internalName) {
for (SystemSetting p : SystemSetting.values()) {
if (p.internalName.equals(internalName)) {
return p;
}
}
return null;
}
}