10. Oktober 2012

DBMS_LDAP: Suche mit einem binären Filter ("ObjectGUID")

DBMS_LDAP: Searching with a binary filter ("ObjectGUID")
Vor mittlerweile einiger Zeit (um genau zu sein: vor 5 Jahren) hatte ich das Blog-Posting "LDAP-Server abfragen ... mit SQL veröffentlicht, welches zeigt, wie man mit DBMS_LDAP auf einen LDAP-Server zugreift und die Ergebnisse als Tabelle aufbereiten kann. Das Paket DBMS_LDAP war eigentlich gar nicht mein Ziel, sondern vielmehr die darin beschriebene "andere Art der Table Function". Aber egal - dieses Blog-Posting gehört zu denen, zu denen ich die meisten Fragen bekomme. LDAP-Zugriffe aus der Datenbank scheinen weit verbreitet und häufig gefordert zu sein.
Eine der Fragen war die von André Meier nach der Möglichkeit, eine LDAP-Suche in einem Microsoft Active Directory anhand der ObjectGUID durchführen zu können. Zuerst dachte ich, dass man das mit einem einfachen LDAP-Filter á la (OBJECTGUID=\55\34\5D\11\96\95\24\47\88\22\85\01\52\EB\13\F9) macht - aber das war falsch gedacht. Denn auch ein LDAP-Server kennt Datentypen; und der Datentyp einer ObjectGUID im Active Directory ist Binary - in der Oracle-Datenbank würde man RAW nehmen.
Und genau hier ist das Problem: Die Funktion SEARCH_S im Package DBMS_LDAP unterstützt für den Parameter FILTER nur Datentypen VARCHAR2 - eine Überladung mit dem Datentypen RAW gibt es nicht. Nun kann man sich Tricks überlegen, einen RAW-Wert mit UTL_RAW als VARCHAR2 aufzufassen ...
SQL> select utl_raw.cast_to_varchar2(hextoraw('55345d11969524478822850152eb13f9')) from dual;

UTL_RAW.CAST_TO_VARCHAR2(HEXTORAW('55345D11969524478822850152EB13F9'))
--------------------------------------------------------------------------------
U4]?¦¦$G¦"¦?R¦

1 Zeile wurde ausgewählt.
... aber das hilft alles nix - denn man kann sich nie darauf verlassen, dass der String auf dem Weg von der Datenbank zum LDAP-Server nicht doch konvertiert wird - damit wird der übergebene Filter verfälscht und die Suche schlägt fehl. Generell kann man also festhalten, dass die Suche nach einer ObjectGUID in einem Microsoft Active Directory mit dem Paket DBMS_LDAP nicht möglich ist - oder allgemeiner formuliert: RAW-Werte können mit DBMS_LDAP nicht als Filter übergeben werden (das Abrufen von binären Attributen ist dagegen möglich).
Aber es gibt ja noch Java in der Datenbank ...
Mein nächster Vorschlag war denn auch, "Java in der Datenbank" zu nutzen. André Meier und Stefan Bucholz haben dann eine Java-Klasse geschrieben, die das Problem mit Hilfe der LDAP-Client API in Java löst (erstmal außerhalb der Datenbank). In Java kann man auch zur Suche binäre Werte an den LDAP-Server übergeben. Und wenn das mit Java außerhalb der Datenbank läuft, läuft das auch in der Datenbank. Der Java-Code von Stefan Buchholz sieht dann so aus - und mit einem CREATE OR REPLACE JAVA SOURCE bekommt man das Java-Programm auch direkt in die Datenbank ...
CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED "XpLdapClientJava" AS 
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Properties;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;

import java.sql.*;

/**
* @author Stefan Buchholz, Leipzig
*/

public class XpLdapClientQuery {
  private static XpLdapClientQuery ldapclient = null;
  private String ldapAdServer;
  private String ldapSearchBase;
  private String ldapUsername;
  private String ldapPassword;
  private LdapContext ctx;
  
  private XpLdapClientQuery() throws Exception {
    Connection con = DriverManager.getConnection("jdbc:default:connection:");
    Statement stmt = con.createStatement();
    ResultSet rs = stmt.executeQuery(
      "SELECT 'ldap://' || LDAP_HOST, LDAP_SEARCH_BASE, LDAP_USER, LDAP_PASSWD FROM XP_LDAP_CLIENT_PROPERTIES"
    );
    if (rs.next()) {
      this.ldapAdServer = rs.getString(1);
      this.ldapSearchBase = rs.getString(2);
      this.ldapUsername = rs.getString(3);
      this.ldapPassword = rs.getString(4);
    }
    rs.close();
    stmt.close();

    Hashtable<String, String> env = new Hashtable<String, String>();
    if (ldapUsername == null) {
      env.put(Context.SECURITY_AUTHENTICATION, "none");
    } else {
      env.put(Context.SECURITY_AUTHENTICATION, "simple");
      env.put(Context.SECURITY_PRINCIPAL, ldapUsername);
      env.put(Context.SECURITY_CREDENTIALS, ldapPassword);
    }
    env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
    env.put(Context.PROVIDER_URL, ldapAdServer);

    env.put("java.naming.ldap.attributes.binary", "objectGUID");
    ctx = new InitialLdapContext(env, null);
  }

  public static String getAccount(String p_uid) throws Exception {
    if (ldapclient == null) {
      ldapclient = new XpLdapClientQuery();
    }
    return ldapclient.findAccountNameByObjectId(p_uid);
  }

  private String findAccountNameByObjectId(String obectId) throws Exception {
    String searchFilter = "(&(objectClass=user)(objectguid=" + obectId + "))";
    SearchControls searchControls = new SearchControls();
    searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
    String ret = null;
    try {   
      NamingEnumeration<SearchResult> results = ctx.search(ldapSearchBase, searchFilter, searchControls);
      if (results.hasMoreElements()) {
        SearchResult searchResult = results.nextElement();
        if (results.hasMoreElements()) {
          throw new Exception("MORE THAN ONE MATCH FOR GIVEN OBJECTGUID");
        } else {
          ret = (String) searchResult.getAttributes().get("sAMAccountName").get();
        }
      }
    } catch (NullPointerException e) {
      throw new Exception ("NO MATCH FOR GIVEN OBJECTGUID");
    }   
    return ret;
  }
}
/
Das Programm setzt voraus, dass die Informationen zum LDAP-Server, also Hostname, TCP/IP-Port, Username und Passwort in der Tabelle XP_LDAP_CLIENT_PROPERTIES zu finden sind.
create table XP_LDAP_CLIENT_PROPERTIES(
   LDAP_HOST         varchar2(100), 
   LDAP_SEARCH_BASE  varchar2(500), 
   LDAP_USER         varchar2(200), 
   LDAP_PASSWD       varchar2(200)
)
/
Der entscheidende Unterschied zu DBMS_LDAP ist in der Zeile ...
   env.put("java.naming.ldap.attributes.binary", "objectGUID");
Womit klar ist, dass dieser Wert als binär zu behandeln ist und in der Form \XX\XX\XX ... übergeben werden kann. Bei Java in der Datenbank kann jede Java-Methode, die "static" ist, mit Hilfe einer PL/SQL Call Specification auf PL/SQL abgebildet werden.
/**
 * Diese Wrapper-Funktion fuer Java gibt fuer in_objectguid (MSAD) den samaccountname zurück.
 * @AUTHOR  AMei, 20120802
 * @PARAM:  in_objectguid
 * @RETURN: VARCHAR2
 * @EXCEPTION: "NO MATCH FOR GIVEN OBJECTGUID"
 * @SEE: XpLdapClientJava, XpLdapClientQuery
 * SELECT get_samaccountname ('\59\34\5d\11\96\95\24\47\88\22\85\01\52\eb\13\f9') AS samaccountname FROM dual;
 */
CREATE OR REPLACE FUNCTION get_samaccountname (
  in_objectguid VARCHAR2
) RETURN VARCHAR2 IS
LANGUAGE JAVA
NAME 'XpLdapClientQuery.getAccount(java.lang.String) return java.lang.String';
Bevor wir es ausprobieren können, braucht der Datenbank-User noch die nötigen Netzwerkprivilegien, denn ein "normaler" Datenbankuser hat erstmal nicht das Privileg, Netzwerkoperationen durchzuführen. Der Datenbankadministrator kann die Rechte wie folgt einräumen.
begin
  dbms_java.grant_permission(
    grantee           => '[Datenbankuser, welcher die LDAP-Suche durchführen soll]',
    permission_type   => 'SYS:java.net.SocketPermission',
    permission_name   => '[LDAP-Server oder "*" für das ganze Netzwerk]',
    permission_action => 'connect,resolve'
  );
end;
Damit ist es fertig. Nun kann auch eine "ObjectGUID"-Suche wie folgt durchgeführt werden.
SELECT get_samaccountname ('\59\34\5d\11\96\95\24\47\88\22\85\01\52\eb\13\f9') AS samaccountname FROM dual;

SAMACCOUNTNAME
--------------------
      Max_Mustermann
Und mit diesem Accountnamen kann man dann per DBMS_LDAP weitersuchen ... den der liegt ja nun als String vor. Viel Spaß beim Ausprobieren ...
Almost five years ago, I published a blog posting about how to access an LDAP Server ... with SQL. The primary goal was indeed the "different" kind of table function which I used, and not that much DBMS_LDAP. DBMS_LDAP was just the "showcase". But this is one of the most popular blog postings and I still get frequent questions about it.
And this blog posting is about one of these questins: André Meier asked, how he could perform an LDAP search for an objectGUID within Microsoft Active Directory. My first thought was that this is a no-brainer and a simple LDAP filter like (OBJECTGUID=\55\34\5D\11\96\95\24\47\88\22\85\01\52\EB\13\F9) would to the trick - but this is not the case. LDAP servers also have a concept of data types; and the data type of an objectGUID is not string, its binary. The mapping within the Oracle database would be RAW.
And this exactly is the root cause of the proplem - the FILTER argument of DBMS_LDAP.SEARCH_S is a VARCHAR2 data type - a variant with RAW does not exist. We could know try some dirty tricks like an explicit cast of a VARCHAR2 to a RAW ...
SQL> select utl_raw.cast_to_varchar2(hextoraw('55345d11969524478822850152eb13f9')) from dual;

UTL_RAW.CAST_TO_VARCHAR2(HEXTORAW('55345D11969524478822850152EB13F9'))
--------------------------------------------------------------------------------
U4]?¦¦$G¦"¦?R¦

1 row selected.
... but this will not work - we cannot guarantee that this VARCHAR2 value will reach the LDAP server unchanged. DBMS_LDAP treats it as a VARCHAR2 - so the bytes might be converted on their way to the LDAP server - and the query will not return the desired results. In principle, DBMS_LDAP does not allow search operations with binary filters - or, expressed otherwise: RAW values cannot be supplied as arguments for LDAP searches with DBMS_LDAP (but fetching binary values from a search result set is supported).
But we still have java in the database ...
So my next proposal was to use Java in the database, since the Java class library contains some LDAP client classes. So André Meier and Stefan Bucholz wrote a Java class which solves the problem - since in Java there is an option to use a binary search filter. In the first step, this Java class ran outside the database, in the next step, we loaded the Java source into the database (CREATE OR REPLACE ... JAVA SOURCE ...) and executed it there.
CREATE OR REPLACE AND RESOLVE JAVA SOURCE NAMED "XpLdapClientJava" AS 
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Properties;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;

import java.sql.*;

/**
* @author Stefan Buchholz, Leipzig
*/

public class XpLdapClientQuery {
  private static XpLdapClientQuery ldapclient = null;
  private String ldapAdServer;
  private String ldapSearchBase;
  private String ldapUsername;
  private String ldapPassword;
  private LdapContext ctx;
  
  private XpLdapClientQuery() throws Exception {
    Connection con = DriverManager.getConnection("jdbc:default:connection:");
    Statement stmt = con.createStatement();
    ResultSet rs = stmt.executeQuery(
      "SELECT 'ldap://' || LDAP_HOST, LDAP_SEARCH_BASE, LDAP_USER, LDAP_PASSWD FROM XP_LDAP_CLIENT_PROPERTIES"
    );
    if (rs.next()) {
      this.ldapAdServer = rs.getString(1);
      this.ldapSearchBase = rs.getString(2);
      this.ldapUsername = rs.getString(3);
      this.ldapPassword = rs.getString(4);
    }
    rs.close();
    stmt.close();

    Hashtable<String, String> env = new Hashtable<String, String>();
    if (ldapUsername == null) {
      env.put(Context.SECURITY_AUTHENTICATION, "none");
    } else {
      env.put(Context.SECURITY_AUTHENTICATION, "simple");
      env.put(Context.SECURITY_PRINCIPAL, ldapUsername);
      env.put(Context.SECURITY_CREDENTIALS, ldapPassword);
    }
    env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
    env.put(Context.PROVIDER_URL, ldapAdServer);

    env.put("java.naming.ldap.attributes.binary", "objectGUID");
    ctx = new InitialLdapContext(env, null);
  }

  public static String getAccount(String p_uid) throws Exception {
    if (ldapclient == null) {
      ldapclient = new XpLdapClientQuery();
    }
    return ldapclient.findAccountNameByObjectId(p_uid);
  }

  private String findAccountNameByObjectId(String obectId) throws Exception {
    String searchFilter = "(&(objectClass=user)(objectguid=" + obectId + "))";
    SearchControls searchControls = new SearchControls();
    searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
    String ret = null;
    try {   
      NamingEnumeration<SearchResult> results = ctx.search(ldapSearchBase, searchFilter, searchControls);
      if (results.hasMoreElements()) {
        SearchResult searchResult = results.nextElement();
        if (results.hasMoreElements()) {
          throw new Exception("MORE THAN ONE MATCH FOR GIVEN OBJECTGUID");
        } else {
          ret = (String) searchResult.getAttributes().get("sAMAccountName").get();
        }
      }
    } catch (NullPointerException e) {
      throw new Exception ("NO MATCH FOR GIVEN OBJECTGUID");
    }   
    return ret;
  }
}
/
The program requires that the LDAP connection details (hostname, port number, LDAP username and password) are stored in the table XP_LDAP_CLIENT_PROPERTIES (we are in the database, so we don't use property files, we use tables).
create table XP_LDAP_CLIENT_PROPERTIES(
   LDAP_HOST         varchar2(100), 
   LDAP_SEARCH_BASE  varchar2(500), 
   LDAP_USER         varchar2(200), 
   LDAP_PASSWD       varchar2(200)
)
/
The interesting bit is contained in just one line ...
   env.put("java.naming.ldap.attributes.binary", "objectGUID");
And this instruction makes clear that the value is to be treated as a binary value - it's specified as \XX\XX\XX and will reach the LDAP server without any conversion. The Oracle database JVM allows any static method to be mapped as a PL/SQL procedure or function. The PL/SQL call specification looks as follows ...
/**
 * Diese Wrapper-Funktion fuer Java gibt fuer in_objectguid (MSAD) den samaccountname zurück.
 * @AUTHOR  AMei, 20120802
 * @PARAM:  in_objectguid
 * @RETURN: VARCHAR2
 * @EXCEPTION: "NO MATCH FOR GIVEN OBJECTGUID"
 * @SEE: XpLdapClientJava, XpLdapClientQuery
 * SELECT get_samaccountname ('\59\34\5d\11\97\95\24\47\28\22\85\12\52\eb\13\f9') AS samaccountname FROM dual;
 */
CREATE OR REPLACE FUNCTION get_samaccountname (
  in_objectguid VARCHAR2
) RETURN VARCHAR2 IS
LANGUAGE JAVA
NAME 'XpLdapClientQuery.getAccount(java.lang.String) return java.lang.String';
Before testing the new function, we need the necessary network privileges. An ordinary database user is not allowed to perform network operations with Java. The DBA needs to grant the privileges as follows:
begin
  dbms_java.grant_permission(
    grantee           => '[database schema performing the LDAP search operation]',
    permission_type   => 'SYS:java.net.SocketPermission',
    permission_name   => '[ldap server name or "*" for the whole network]',
    permission_action => 'connect,resolve'
  );
end;
And now we can perform an ObjectGUID search - even from within the database ...
SELECT get_samaccountname ('\59\34\5d\11\96\95\24\47\88\22\85\01\52\eb\13\f9') AS samaccountname FROM dual;

SAMACCOUNTNAME
--------------------
      Max_Mustermann
And having this account name, we can use "classic" DBMS_LDAP calls to retrieve further details, since this is a VARCHAR2 value. Have fun trying it out.

Keine Kommentare:

Beliebte Postings