Sep 022007
 

Eine nicht funktionale Anforderung an Software-Systeme wird gemeinhin mit dem Begriff “Security” umrissen. Ein Teilaspekt betrifft die Authorisierung von Benutzern / Fremdsystemen gegenüber bestimmten Diensten oder Daten. Bei mehrschichtigen Softwaresystemen, die eine Datenbank in der Ressourcenschicht einbinden, ist die Authorisierung oft Aufgabe der Geschäftsschicht.

Ein anderer Ansatz stellt die Realisierung der Authorisierung direkt in der Datenbank dar. Diesen Ansatz findet man oft in Alt-Systemen, bei denen zumal auch Geschäftslogik in der Datenbank in Form von Stored Procedures umgesetzt ist. Damit die Datenbank Authorisierungsfunktionen übernehmen kann, muss der tatsächliche Benutzer an die DB propagiert werden.

Oracle Logo   BEA Logo

Im Rahmen dieses Artikels möchte ich ein wenig Know-How für die Implementierung dieses Ansatzes in Verbindung mit dem Oracle-DBMS (Datenbankmanagementsystem), Connection Pooling im Weblogic Application Server und JDBC weitergeben. Dieses Know-How entstammt der Anbindung einer Produktdatenbank bei einem großen Deutschen Automobilhersteller. Zum Einsatz kommt eine proprietäres Feature des Oracle-DBMS – die Oracle-Proxy-Sessions.

Eine Oracle-Proxy-Session ermöglicht es, die Verbindung zum DBS (Datenbanksystem) mittels technischem Benutzerkonto herzustellen und basierend auf dieser Verbindung eine Sitzung im Kontext eines anderem Benutzerkontos auszuführen. Das technische Benutzerkonto und die damit hergestellte DB-Verbindung sind sozusagen der Proxy. Es wird ein fester technischer Benutzer verwendet, der selbst nur die notwendigsten Rechte in der Datenbank besitzt.

BEA WebLogic kann Connection Pools für Verbindungen über JDBC verwalten. Der Connection Pool steht transparent durch eine DataSource über JNDI zur Verfügung. Die Applikation verwendet einen JNDI-Lookup um das DataSource-Object zu beziehen. Über das DataSource-Objekt wird eine Datenbank-Verbindung erhalten. BEA Weblogic liefert eine Verbindung aus dem Connection Pool zurück und started die Transaktion. Dabei wird die Datenbanktransaktion mit der EJB Container-Transaktion verknüpft. Falls eine Datenbankverbindung aus derselben DataSource in dieser EJB Container-Transaktion verwendet wurde, wird von der DataSource diese s Connection-Objekt zurückgeliefert. Auf diese Weise ist die Verknüpfung von Datenbanktransaktion mit EJB Container-Transaktion realisiert.

Die Applikation muss dafür sorgen, dass beim Starten der Transaktion nachdem die DB-Verbindung aus dem Pool vorliegt, eine Oracle-Proxy-Session für den tatsächlichen Benutzer geöffnet. Dieser tatsächliche Benutzer arbeitet zum bsp. mit dem GUI der Applikation oder stellt ein Fremdsystem dar welches zum Beispiel über einen WebService auf unser System zugreift. Über die Proxy-Session laufen alle Anfragen während dieser Transaktion.

Die Geschäftslogik in der Datenbank (Stored Procedures) können den Nutzer der Sitzung ermitteln und entsprechende Authorisierungsmechanismen fahren. Sofern lesenden Zugriffe über Views realisiert werden, kann durch die Query-Logik der Views der lesende Zugriffauf Daten durch weitere Authorisierungslogik abgeschirmt werden.

Nachdem die EJB Container-Transaktion und damit die Container-Transatkion beendet wird, sollte die Oracle-Proxy-Session geschlossen werden. Der Benutzer sollte nach Ausführung der Transaktion nicht mehr mit der Datenbank-Verbindung verknüpft sein, die nach dem Beenden der Transaktion im Connection Pool als verfügbare Verbindung bereit steht.

Zudem ist es unmöglich mit der noch offenen Proxy-Session das Feature von BEA Weblogic zur Überprüfung der Connections im Pool zu verwenden. Dieses Feature ist essentiell damit Weblogic automatisch ungültige Verbindungen nach einem Datenbanksystem-Neustart oder einem Recompile von Stored Procedures erkennt und diese verwirft. Der Connection Pool kann so konfiguriert werden, dass vor dem erstmaligen Herausgeben einer Connection in einer EJB Container-Transaktion die Verbindung mittels einem einfachem Select-Statement geprüft werde (zum Bsp. “select 1 from dual”).

Das Öffnen und Schließen der Oracle-Proxy-Session wird am Besten vom Datenbankzugriffswork (Resource Layer) der Applikation übernommen. In meinem Fall handelte es sich bei dem Datenbankzugriffsframework um eine Abstraktion des Spring-JDBC-Frameworks. Das Beziehen der Datenbankverbindung, sowie das Öffnen und Schließen der Proxy-Session ist dabei einer Klasse “DataSourceUtil” gekapselt. Diese Klasse stellt öffentliche, statische Methoden bereit um

  • eine Verbindung zu beziehen: “getConnection(..)”
  • die Proxy-Session auf einer Verbindung zu öffnen: “openProxySession(..)”
  • die Proxy-Session zu schließen: “closeProxySession(..)”
  • die Proxy-Session für den aktuellen Thread zu schließen, falls eine vorhanden ist: “closeProxySessionForCurrentThreadIfAny(..)”

Das folgende Ablaufdiagramm gibt einen Überblick der Proxy-Session-Mechanik in einer J2EE-Applikation:

Oracle-Proxy-Session UML Sequence

Das Beziehen der Connection arbeitet mit JDBC-Interfaces und ist relativ simpel:

/**
 * Correctly changes exceptions
 * @param ds The data source. Never <code>null</code>.
 * @return The connection. Never <code>null</code>.
 */
public static Connection getConnection(DataSource ds) {
  assert (ds != null);

  try {
    return ds.getConnection();
  }
  catch (SQLException ex) {
    throw new CannotGetJdbcConnectionException("DataSource " + ds, ex);
  }
}

Beim Öffnen der Oracle-Proxy-Session müssen Oracle-spezifische Klassen verwendet werden. Dies ist schon wesentlich komplexer:

/**
 * <p>
 * Ensures that the connection which is used for the current transaction (given connection) uses a proxy session for
 * the user who originated the request at the business facade. If there is already a proxy session at the connection
 * retrieved from the data source, then this proxy session is either closed or kept. If the proxy user at the
 * connection differs from the originator of this transaction, then the proxy session is closed and opened again for
 * the actual user (originator of this transaction). If the proxy user is equal to the originator of this transaction,
 * then the proxy session is simply kept.
 * </p><p>
 * Throws a CannotGetJdbcConnectionException if the proxy session could not be created,
 * because for the oracle user probably the grant for proxy sessions has not be given to the connection oracle user.
 * </p>
 *
 * @param conn The connection to ensure the proxy session for. Never <code>null</code>.
 * @param proxyUser The proxy user. Never <code>null</code>.
 * @return The given connection casted to an oracle connection. (TODO why is this required?)
 */
public static OracleConnection openProxySession(Connection conn, String proxyUser) {
    assert (conn != null);
    assert (proxyUser != null);

  WLConnection wlConn = (WLConnection) conn;

  try {
    // ---------------------------------------------------------------------------------------------------------------
    // We are casting to the oracle interface type. If we call any special oracle methods, the connection will
    // become "dirty" (weblogic term) and will not be put back in the connection pool, UNLESS a special
    // switch is set at the connection pool ("Remove Infected Connections Disabled").
    // ---------------------------------------------------------------------------------------------------------------
    OracleConnection oraConnection = (OracleConnection) wlConn.getVendorConnection();
    boolean bOpenProxySession = false;

    // ---------------------------------------------------------------------------------------------------------------
    // Check if there is a proxy session. Retain the proxy session if the correct user is set,
    // otherwise close it and open a new proxy session.
    // ---------------------------------------------------------------------------------------------------------------
    if (oraConnection.isProxySession()) {
      log.debug("Connection is already an proxy connection");
      if (!proxyUser.equalsIgnoreCase(oraConnection.getUserName())) {
        closeProxySession(oraConnection);
        bOpenProxySession = true;
        OpenProxySessionRegistry.getInstance().removeOpenProxySessionConnectionForCurrentThread();
      }
    }
    else {
      bOpenProxySession = true;
    }

    // ---------------------------------------------------------------------------------------------------------------
    // Open a new proxy session, if necessary.
    // ---------------------------------------------------------------------------------------------------------------
    if (bOpenProxySession) {
      Properties props = new Properties();

      try {
        props.put("PROXY_USER_NAME", proxyUser);

        oraConnection.openProxySession(OracleConnection.PROXYTYPE_USER_NAME, props);
      }
      catch (SQLException e) {
        // -----------------------------------------------------------------------------------------------------------
        // Another proxy user is used for opening a proxy session when opening a proxy session for the actual
        // user failed. This is required, because when opening a proxy connection failed, the connection can not be
        // used.
        // The ejb container will perform a rollback or commit when the transaction boundary is left and a system
        // exception will be risen. In order to get around this problem we have to implement this behaviour.
        // -----------------------------------------------------------------------------------------------------------
        {
          String userFallback = ConfigPrisma.getInstance().getProxyUserFallback();
          props.put("PROXY_USER_NAME", userFallback);

          try {
            oraConnection.openProxySession(OracleConnection.PROXYTYPE_USER_NAME, props);
          }
          catch (SQLException e2) {
            log.error("Could not open a proxy session with the configured fall back user '" + userFallback + "'"
              + " after opening a proxy session for the actual user failed.", e);

            throw e2;
          }
        }

        throw new CannotOpenProxySessionException("Failed to open proxy connection for user " + proxyUser, e);
      }

      // -------------------------------------------------------------------------------------------------------------
      // The connection with just opened proxy session is registered,
      // so that the proxy session (not the connection) can be closed by the DynamicFacadeBFBeanProxy
      // -------------------------------------------------------------------------------------------------------------
      OpenProxySessionRegistry.getInstance().setOpenProxySessionConnectionForCurrentThread(oraConnection);
    }

    return oraConnection;
  }
  catch (SQLException e) {
    throw new CannotGetJdbcConnectionException("Failed to open proxy connection", e);
  }
}

Das Oracle-spezifische Connection-Interface wird benötigt um Proxy-Sessions zu kontrollieren. Sobald auf dem Weblogic-Wrapper Objekt aus dem Connection-Pool “getVendorConnection()” aufgerufen wird, markiert Weblogic die Connection als “dirty”. Daher muss in den Einstellungen des Connection Pools in Weblogc “Remove Infected Connections” auf “false” gesetzt werden. Andernfalls würde der Connection Pool defacto kein Pooling betreiben, sondern jede Connection sofort verwerfen.

Das Datenbankzugriffsframework ruft vor der Ausführung jeder Datenbankanfrage / Datenbankoperation “getConnection(..)” und danach “openProxySession(..)” auf. Daher muss in “openProxySession”Logik enthalten sein, um eine bestehende Proxy-Session für den aktuellen Benutzer beizubehalten.

Der aktuelle Benutzer ist der Principal-Name des Callers, welcher im EJB-Context gegeben ist. Dieser wird für das Datenbankzugriffsframework mit dem aktuellen Thread verknüpft. Diese Verknüpfung wird in meinem Fall in der Facade vor der Business Service Layer realisiert. Die Facade-EJBs kapseln alle Zugriffe auf die komplexeren Subsysteme der Business Service Layer. Natürlich könnte der Name des aktuellen Benutzers auch anderweitig bereitgestellt und an “openProxySession” übergeben werden.

Gibt es einen Fehler beim Öffnen der Proxy-Session, so hatte wir im Projekt das Problem, dass das Oracle-Connection-Objekt in einem “unsauberen” Zustand war. Nur das erfolgreiche Öffnen einer weiteren Proxy-Session konnte diesen Zustand wieder bereinigen. Diesen meta-physische Beschreibung möchte ich jetzt einmal so stehen lassen. Eventuell besteht das Problem mit dem aktuellsten JDBC-Treiber von Oracle nicht mehr.

Das Oracle-Connection-Objekt wird nach dem Öffnen der Proxy-Session in einer Registry vermerkt (“OpenProxySessionRegistry”). Dieses Singleton hält alle Connection-Objekte für die aktuell eine Proxy-Session geöffnet ist, wobei die Verknüpfung mit dem Thread vermerkt ist, welcher die Proxy-Session geöffnet hat. Dies ist einfach mittels der JDK-Klasse “ThreadLocal” zu realisieren. Das Merken des Connection-Objekts ist notwendig, um später die Proxy-Session zu schließen, bevor die Transaktion vom EJB-Container beendet wird. Dies wird beim Rücksprung aus dem Methodenaufruf an die Facade mittels Aufruf an “closeProxySessionForCurrentThreadIfAny(..)” erledigt:

/**
 * Closes the oracle proxy session on the connection used by the current thread, if any.
 *
 * @param pRollback <code>true</code> if a rollback must be performed instead of a commit
 * upon closing the proxy session.
 */
public static void closeProxySessionForCurrentThreadIfAny(boolean pRollback) {
  Connection conn = OpenProxySessionRegistry.getInstance().removeOpenProxySessionConnectionForCurrentThread();
  if (conn != null) {

    // ---------------------------------------------------------------------------------------------------------------
    // This is required because closing the proxy session will always commit the current transaction.
    // ---------------------------------------------------------------------------------------------------------------
    if (pRollback) {
      log.info("rolling back PRISMA db transaction");

      try {
        conn.rollback();
      }
      catch (SQLException e) {
        throw new PrismaSysException(ErrorCodePrismaSys.ROLLBACK_FAILED, e);
      }
    }
    else {
      log.info("commiting PRISMA db transaction", Level.DEBUG);

      try {
        conn.commit();
      }
      catch (SQLException e) {
        throw new PrismaSysException(ErrorCodePrismaSys.COMMIT_FAILED, e);
      }
    }

    closeProxySession(conn);
  }
}

An die Methode “closeProxySessionForCurrentThreadIfAny” muss übergeben werden, ob ein Rollback oder Commit für die EJB Container Transaktion auzuführen ist. Da “closeProxySessionForCurrentThreadIfAny” beim Austritt aus der Facade des Business Service Layer aufgerufen wird, liegt dort die Kenntnis vor, ob eine Exception aus der Facade geworfen wird oder der EJB-Context auf Rollback gesetzt ist. Vor dem Schließen der Proxy-Session muss ein Commit bzw. ein Rollback der Datenbank-Transaktionausgeführt werden. Dies kann leider nicht vom EJB-Container übernommen werden, da dieser erst nach dem Schließen der Proxy-Session aktiv werden würde.

Das eigentliche Schließen der Proxy-Session ist dann wieder einfach:

/**
 * Closes the oracle proxy session on the given connection.
 *
 * @param pConn Never <code>null</code>.
 */
private static void closeProxySession(Connection pConn) {
  try {
    ((OracleConnection)pConn).close(OracleConnection.PROXY_SESSION);
  }
  catch (SQLException e) {
    throw new CannotCloseProxySessionException("Failed to close proxy connection", e);
  }
}

Oracle Proxy-Sessions sind verfügbar ab Oracle-DBMS 9. Wir haben den Thin JDBC-Treiber 10.2.0.2.0 im Einsatz. Bei älteren Versionen des JDBC-Treibers kam es zu unumgehbaren Problemen mit Oracle-Proxy-Sessions.

Man sieht an der Länge des Artikels und dem notwendigen Code mit seinen Spezialbehandlungen, dass das Thema Oracle-Proxy Sessions ein zweischneidiges Schwert ist. In dem Projekt wo es bei mir Anwendung fand, sind Oracle-Proxy-Sessions essentiell: Zum einen ist Connection-Pooling zwingend notwendig (Last / Performance) und zum anderen muss die Datenbanktransaktion im Kontext des Benutzers laufen, damit in der Datenbank Authorisierungsmechanismen implementiert werden können. Auf diese Weise werden die Authorisierungsfunktionen zwischen mehreren Applikation, welche auf die Datenbank zugreifen, geteilt. In diesem Umfeld war es die einzig sinnvolle Lösung, die sich mittlerweile bewährt hat.

 Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>