Thursday, July 22, 2010

A JAAS LoginModule for ActiveMQ

In order to let ActiveMQ to use the existing authentication repository, I created a JAAS plugin for ActiveMQ serve the purpose.

1.  change <ActiveMQ_HOME>/conf/activemq.xml to enable JAAS authentication.

<!-- comment out simpleAuthenticationPlugin-->
 <jaasAuthenticationPlugin configuration="activemq-domain" />

2. change <ActiveMQ_HOME>/conf/loging.conf, add login module definition.
activemq-domain {
    com.zeon.auth.CmdJdbcLoginModule Sufficient
                                debug=true
                                com.zeon.db.url="jdbc:mysql://localhost:3306/userdb"
                                com.zeon.db.driver="com.mysql.jdbc.Driver"
                                com.zeon.db.username="user"
                                com.zeon.db.password="pass";
                org.apache.activemq.jaas.PropertiesLoginModule Sufficient
        debug=true
        org.apache.activemq.jaas.properties.user="users.properties"
        org.apache.activemq.jaas.properties.group="groups.properties";
};

3. finally, the class that extended from javax.security.auth.spi.LoginModule

package com.zeon.sync.auth;

import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;

import org.apache.activemq.jaas.GroupPrincipal;
import org.apache.activemq.jaas.UserPrincipal;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.zeon.jdbc.JDBCException;
import com.zeon.jdbc.JDBCUtils;
import com.zeon.jdbc.UniqueResultHandler;

public class CmdJdbcLoginModule implements LoginModule {

    private static final Log log = LogFactory.getLog(CmdJdbcLoginModule.class);
    private static final String[] ROLE = {"hs", "users"};
    private Subject subject;
    private CallbackHandler callbackHandler;
    private boolean debug;
    private String user;
    private Set principals;
   
    private JDBCUtils jdbcUtils;

    public CmdJdbcLoginModule() {
        principals = new HashSet();
    }

    public void initialize(Subject subject, CallbackHandler callbackHandler,
            Map sharedState, Map options) {
        this.subject = subject;
        this.callbackHandler = callbackHandler;
       
        debug = "true".equalsIgnoreCase((String) options.get("debug"));
        String url = (new StringBuilder()).append(
                (String) options.get("com.hs.db.url"))
                .append("").toString();
        String username = (new StringBuilder()).append(
                (String) options.get("com.hs.db.username"))
                .append("").toString();
        String password = (new StringBuilder()).append((String)options
                .get("com.zeon.db.password")).append("").toString();
        String driver = (new StringBuilder()).append((String)options
                .get("com.zeon.db.driver")).append("").toString();
       
        Properties props = new Properties();
        props.setProperty("jdbc.user", username);
        props.setProperty("jdbc.password", password);
        props.setProperty("jdbc.driver", driver);
        props.setProperty("jdbc.url", url);

        try {
            jdbcUtils = new JDBCUtils(props);
        } catch (SQLException e) {
            log.error("connect to db failed.", e);
        }
        if (debug)
            log.debug((new StringBuilder()).append("Initialized debug=")
                    .append(debug));
    }

    public boolean login() throws LoginException {
        Callback callbacks[] = new Callback[2];
        callbacks[0] = new NameCallback("Username: ");
        callbacks[1] = new PasswordCallback("Password: ", false);
        try {
            callbackHandler.handle(callbacks);
        } catch (IOException ioe) {
            throw new LoginException(ioe.getMessage());
        } catch (UnsupportedCallbackException uce) {
            throw new LoginException((new StringBuilder()).append(
                    uce.getMessage()).append(
                    " not available to obtain information from user")
                    .toString());
        }
        user = ((NameCallback) callbacks[0]).getName();
        char tmpPassword[] = ((PasswordCallback) callbacks[1]).getPassword();
        if (tmpPassword == null)
            tmpPassword = new char[0];
        String password = null;
        try {
            password = findPassword(user);
            log.debug("=============the password for user " + user + " is " + password);
        } catch (JDBCException e) {
            throw new FailedLoginException("DB access failed.");
        }
        if (password == null)
            throw new FailedLoginException("User does exist");
        if (!password.equals(new String(tmpPassword)))
            throw new FailedLoginException("Password does not match");
        if (debug)
            log.debug((new StringBuilder()).append("login ").append(user)
                    .toString());
        return true;
    }

    @SuppressWarnings("unchecked")
    private String findPassword(String user) throws JDBCException {
        String sql = "select JMSpassword from DoctorSyncMap where DoctorID='" + user + "'";
        String result = (String)jdbcUtils.query(sql, new UniqueResultHandler() {

            @Override
            public Object handle(ResultSet rs) throws JDBCException {
                String result = "";
                try {
                    if (rs.next()) {
                        result = rs.getString(1);
                    }
                } catch (Exception e) {
                    log.error("query db failed.", e);
                    throw new JDBCException(e);
                }
                return result;
            }
           
        });
        return result;
    }

    @SuppressWarnings("unchecked")
    public boolean commit() throws LoginException {
        principals.add(new UserPrincipal(user));
        for (String r : ROLE) {
            principals.add(new GroupPrincipal(r));
        }
        subject.getPrincipals().addAll(principals);
        log.debug("commit");
        return true;
    }

    public boolean abort() throws LoginException {
        user = null;
        if (debug)
            log.debug("abort");
        return true;
    }

    public boolean logout() throws LoginException {
        subject.getPrincipals().removeAll(principals);
        principals.clear();
        if (debug)
            log.debug("logout");
        return true;
    }

}

Configurable Auditing Interceptor for Hibernate

If Hibernate provides a configurable audit log function, that would be very nice. Think if we can do this:

In Hibernate.properties:
hibernate.audit_log=true

In Entity definition:
@column(audit="true")
public String name;

Then Hibernate will log the changes for the specified columns.

Tuesday, April 13, 2010

Sugar custom logic hook


Hooks for modules
Define hooks in custom/modules/<module_name>/logic_hooks.php
Event type
before_save
after_save
before_restore
after_restore
process_record                        ------   when loads a row of data into instance of a bean
before_retrieve                         ------   fetches a single row of data given the primary key
after_retrieve

Generic Hooks
Generic hooks should be defined in custom/modules/logic_hooks.php
Event Type
after_ui_frame
after_ui_footer

Wednesday, March 24, 2010

Well-known Java System Properties

For security manager:

-Djava.security.manager              //To enable java security manager
-Djava.security.policy=mypolicy      //specify the location of policy file
//This can be replaced by adding a line like policy.url.3=file:/C:/Test/mypolicy into file <Java_home>\jre\lib\security\java.security.

For SSL Connection:
-Djavax.net.debug=ssl                               //Enable ssl debug information
-Djavax.net.ssl.keyStore=/path/to/client.ks     //specify the location of key store
-Djavax.net.ssl.keyStorePassword=password    //password for keystore
-Djavax.net.ssl.trustStore=/path/to/client.ts    //specify the location of trust store

For java.util.logging (See more detail)
-Djava.util.logging.config.class
-Djava.util.logging.config.file        //Location of the configuration file, by default JVM will read <Jre_home>lib/logging.properties 
-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager
 
 
-Djava.endorsed.dirs=/opt/apache-tomcat-6.0.26/endorsed  //The Java SE runtime environment will use classes in such JAR files 
                                                                                 //to override the corresponding classes provided in the 
                                                                                 //Java platform as shipped by Sun.
 

Use SSL in Java

The default password for the Java default keystore file $JAVA_HOME/lib/security/cacerts is 'changeit'

Setting up the Key and Trust Stores

  1. Using Java keytool, create a certificate for the Server:
    keytool -genkey -alias broker -keyalg RSA -keystore broker.ks
  2. Export the broker's certificate so it can be shared with clients:
    keytool -export -alias broker -keystore broker.ks -file broker_cert
  3. Create a certificate/keystore for the client:
    keytool -genkey -alias client -keyalg RSA -keystore client.ks
  4. Create a truststore for the client, and import the broker's certificate. This establishes that the client "trusts" the broker:
    keytool -import -alias broker -keystore client.ts -file broker_cert

Starting the Server

Using the javax.net.ssl.* System Properties

Before starting the broker's VM set the SSL_OPTS enviorment variable so that it knows to use the broker keystore.

export SSL_OPTS = -Djavax.net.ssl.keyStore=/path/to/broker.ks -Djavax.net.ssl.keyStorePassword=password

Starting the Client

When starting the client's VM, specify the following system properties:

javax.net.ssl.keyStore=/path/to/client.ks
javax.net.ssl.keyStorePassword=password
javax.net.ssl.trustStore=/path/to/client.ts

Monday, March 22, 2010

How to change default sugar report entries per page?

Sugar read variable list_report_max_per_page from config.php, but this variable doesn't exist in that file by default. So simply add 'list_report_max_per_page' => 500, into config.php to change sugar report entries per page.

Friday, February 26, 2010

Hibernate Annotation and MultipleHiloPerTableGenerator

This is the example of how to define hilo generator by using annotation that I found in Hibernate Annotation document.


@Id @GeneratedValue(generator="hibseq")
@GenericGenerator(name="hibseq", strategy = "seqhilo",
    parameters = {
        @Parameter(name="max_lo", value = "5"),
        @Parameter(name="sequence", value="heybabyhey")
    }
)
public Integer getId() {
 

But when I apply to MultipleHiloPerTableGenerator, I always get exception. The way I do is like below.


@Id @GeneratedValue(generator="hibseq")
@GenericGenerator(name="hibseq", strategy = "org.hibernate.id.MultipleHiloPerTableGenerator",
    parameters = {
        @Parameter(name="max_lo", value = "100"),
    }
)
public Integer getId() {

Since no short name for MultipleHiloPerTableGenerator, I have to put the full class name into strategy according to documentation. The reason we use this generator is based on following reasons.We often have huge amount of insertion jobs, like inserting 1M records in one operation. If we use auto-increment primary key, what Hibernate does is issue a database access to get the id when you call save(object) method, then 1M insert statement will generate 2M database access, which extremely slows down the process.  MultipleHiloPerTableGenerator use a table(default name is hibernate_sequences) to save a hi value for each entity that use this generator. It uses a separate session to retrieve the hi value, at the same time update the hi value to current hi + max_lo, when the save(object) is called, it assigned the id as hi plus sequence number, the max value of sequence number is defined by property max_lo. If the sequence number reaches to the max, the generator will retrieve another hi value. In a short explanation, each session gets pre-assign a chunk of id pool. So, this generator can reduce the time consumption on retrieving id. When the insertion is huge, it saves a great deal of time.

The correct way is to add strategy attribute for @GeneratedValue. To modify the annotation like this.


@Id @GeneratedValue(generator="hibseq", strategy=GenerationType.TABLE)
@GenericGenerator(name="hibseq", strategy = "org.hibernate.id.MultipleHiloPerTableGenerator",
    parameters = {
        @Parameter(name="max_lo", value = "100"),
    }
)
public Integer getId() {


Easy, but I spent a whole afternoon tracing the Hibernate source code to find that out.
Google+