NSF-Cached Credential Store for IBM Social Business Toolkit

package com.developi.sbt;

/**
 * NSF-Cached Credential Store Snippet for the IBM Social Business Toolkit 
 * Version 1.0, (C) 2014 Serdar Basegmez 
 * 
 * This class is a derivative work of MemoryStore class from IBM Social 
 * Business Toolkit SDK project, licensed under Apache License V2.0. 
 * 
 * Social Business Toolkit SDK (http://ibmsbt.openntf.org)
 * (c) Copyright IBM Corp. 2010, 2013
 * 
 * Also uses MIMEBean utility methods from OpenNTF Domino API project,
 * created by Nathan T Freeman, Jesse Gallagher with Jesse Gallagher, 
 * Tim Tripcony, Paul Withers, Declan Lynch, Rene Winklemeyer.
 * 
 * OpenNTF Domino API (http://openntf.org/main.nsf/project.xsp?r=project/OpenNTF%20Domino%20API)
 * Licensed under Apache License V2.0
 * 
 */


import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Externalizable;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import lotus.domino.Database;
import lotus.domino.Document;
import lotus.domino.MIMEEntity;
import lotus.domino.MIMEHeader;
import lotus.domino.NotesException;
import lotus.domino.Session;
import lotus.domino.Stream;
import lotus.domino.View;

import com.ibm.commons.util.StringUtil;
import com.ibm.sbt.security.credential.store.BaseStore;
import com.ibm.sbt.security.credential.store.CredentialStoreException;
import com.ibm.xsp.extlib.util.ExtLibUtil;

public class CachedCredStore extends BaseStore {

/**
 * DESCRIPTION:
 * 
 * IBM Social Business Toolkit supports RDBMS-based persistent credential store
 * for Endpoints. However it doesn't support NSF-based stores. So when you develop
 * SBT applications on XPages, it would be pretty annoying that every time the 
 * classloader cleared (e.g.HTTP restart, Java class changes, etc.), you lose all
 * credentials and tokens.
 * 
 * This class can be used as an alternative to Memory store to persist endpoint 
 * information in a notes document. It simply uses a MIME stream in a rich text 
 * item, thanks to the brilliant idea of Tim Tripcony.
 * 
 * Warning: This implementation is not using any security. All sensitive data would
 * be accessible with a NotesClient. So use it for development and testing for now!
 * 
 * Next version I will convert it to sessionAsSigner but still, it should be supported 
 * by Reader/Author fields and even with encryption.
 * 
 * USAGE:
 * 
 * 1. Copy the Java class into your NSF project.
 * 
 * 2. Create a view named or aliased as "tokenstores".
 * 		Selection for the view : SELECT form="tokenstore"
 * 		First column, ascending sorted with "ServerName" field 
 * 
 * 3. Define the credential store in your Faces-Config file:
 * 
 *     		<managed-bean-name>CredStore</managed-bean-name>
 *    			<managed-bean-class>com.developi.sbt.CachedCredStore</managed-bean-class>
 *    			<managed-bean-scope>application</managed-bean-scope>
 *    		</managed-bean>
 *
 * 4. Use CredStore in your endpoint declarations
 */
		
	private Map<String,byte[]> map = new HashMap<String,byte[]>();
	
	public CachedCredStore() {
		loadStore();
	}
	
	public Object load(String service, String type, String user) throws CredentialStoreException {
		String application = findApplicationName();
		String key = createKey(application, service, type, user);
		return deSerialize(map.get(key));
	}

	public void store(String service, String type, String user, Object credentials) throws CredentialStoreException {
		String application = findApplicationName();
		String key = createKey(application, service, type, user);

		map.put(key, serialize(credentials) );
		saveStore();
	}

	public void remove(String service, String type, String user) throws CredentialStoreException {
		String application = findApplicationName();
		String key = createKey(application, service, type, user);
		map.remove(key);
		saveStore();
	}
	
	/**
	 * Create a key for the internal map.
	 */
	protected String createKey(String application, String service, String type, String user) throws CredentialStoreException {
		StringBuilder b = new StringBuilder(128);
		b.append(StringUtil.getNonNullString(application));
		b.append('|');
		b.append(StringUtil.getNonNullString(service));
		b.append('|');
		b.append(StringUtil.getNonNullString(type));
		b.append('|');
		b.append(StringUtil.getNonNullString(user));
		return b.toString();
	}
	
	@SuppressWarnings("unchecked")
	protected void loadStore() {
		Document doc=getStoreDoc();
		if(doc==null) return;
		
		try {
			Object obj=restoreState(doc, "CredStore");
			
			if(obj instanceof Map) {
				Iterator it = ((Map)obj).entrySet().iterator();
			    while (it.hasNext()) {
			        Map.Entry pairs = (Map.Entry)it.next();
			        if(pairs.getValue() instanceof byte[]) {
				        map.put((String)pairs.getKey(), (byte[])pairs.getValue());
			        }
			        it.remove(); // avoids a ConcurrentModificationException
			    }
			}
			
		} catch (Throwable t) {
			System.out.println("Error loading tokenstore: "+t.getMessage());
			t.printStackTrace(System.err);
		} finally {
			if(doc!=null) {
				try {
					doc.recycle();
				} catch (NotesException e) { }
			}
		}
		
		System.out.println("Token store loaded...");
	}

	protected void saveStore() {
		Document doc=getStoreDoc();
		if(doc==null) return;
		
		try {
			saveState((HashMap<String,byte[]>)map, doc, "CredStore");
			doc.save();
		} catch(Throwable t) {
			System.out.println("Error saving tokenstore: "+t.getMessage());
			t.printStackTrace(System.err);
		} finally {
			if(doc!=null) {
				try {
					doc.recycle();
				} catch (NotesException e) { }
			}
		}
		
	}	
	
	protected Document getStoreDoc() {
		Database database=ExtLibUtil.getCurrentDatabase();
				
		Document doc=null;
		View view=null;

		try {
			view=database.getView("tokenstores");
			if(view==null) {
				System.out.println("Unable to find 'tokenstores' view...");
				return null;
			}
			String serverName=database.getServer();
			
			doc=view.getDocumentByKey(serverName, true);
			
			if(doc==null) {
				doc=database.createDocument();
				doc.replaceItemValue("Form", "tokenstore");
				doc.replaceItemValue("ServerName", serverName);
				doc.computeWithForm(false, false);
				doc.save();
			}
			
		} catch (NotesException e) {
			e.printStackTrace();
		} finally {
			if(view!=null) {
				try {
					view.recycle();
				} catch (NotesException e) { }
			}
		}

		return doc;
	}
	
	// MIMEBean methods

	/**
	 * Restore state. Imported from org.openntf.domino
	 * 
	 * @param doc
	 *            the doc
	 * @param itemName
	 *            the item name
	 * @return the serializable
	 * @throws Throwable
	 *             the throwable
	 */
	@SuppressWarnings("unchecked")
	public static Object restoreState(Document doc, String itemName) throws Throwable {
		Session session=doc.getParentDatabase().getParent();
		boolean convertMime = session.isConvertMime();
		session.setConvertMime(false);

		Object result = null;
		MIMEEntity entity = doc.getMIMEEntity(itemName);

		if(null==entity) return null;
		
		Stream mimeStream = session.createStream();
		entity.getContentAsBytes(mimeStream);

		ByteArrayOutputStream streamOut = new ByteArrayOutputStream();
		mimeStream.getContents(streamOut);
		mimeStream.recycle();

		byte[] stateBytes = streamOut.toByteArray();
		ByteArrayInputStream byteStream = new ByteArrayInputStream(stateBytes);
		ObjectInputStream objectStream;
		if (entity.getHeaders().toLowerCase().contains("content-encoding: gzip")) {
			GZIPInputStream zipStream = new GZIPInputStream(byteStream);
			objectStream = new ObjectInputStream(zipStream);
		} else {
			objectStream = new ObjectInputStream(byteStream);
		}

		// There are three potential storage forms: Externalizable, Serializable, and StateHolder, distinguished by type or header
		if(entity.getContentSubType().equals("x-java-externalized-object")) {
			Class<Externalizable> externalizableClass = (Class<Externalizable>)Class.forName(entity.getNthHeader("X-Java-Class").getHeaderVal());
			Externalizable restored = externalizableClass.newInstance();
			restored.readExternal(objectStream);
			result = restored;
		} else {
			Object restored = (Serializable) objectStream.readObject();

			// But wait! It might be a StateHolder object or Collection!
			MIMEHeader storageScheme = entity.getNthHeader("X-Storage-Scheme");
			MIMEHeader originalJavaClass = entity.getNthHeader("X-Original-Java-Class");
			if(storageScheme != null && storageScheme.getHeaderVal().equals("StateHolder")) {
				Class<?> facesContextClass = Class.forName("javax.faces.context.FacesContext");
				Method getCurrentInstance = facesContextClass.getMethod("getCurrentInstance");

				Class<?> stateHoldingClass = (Class<?>)Class.forName(originalJavaClass.getHeaderVal());
				Method restoreStateMethod = stateHoldingClass.getMethod("restoreState", facesContextClass, Object.class);
				result = stateHoldingClass.newInstance();
				restoreStateMethod.invoke(result, getCurrentInstance.invoke(null), restored);
			} else {
				result = restored;
			}
		}


		if(entity!=null) {
			entity.recycle();
		}

		session.setConvertMime(convertMime);

		return result;
	}

	/**
	 * Save state. Imported from org.openntf.domino
	 * 
	 * @param object
	 *            the object
	 * @param doc
	 *            the doc
	 * @param itemName
	 *            the item name
	 * @throws Throwable
	 *             the throwable
	 */
	private static void saveState(Serializable object, Document doc, String itemName) throws Throwable {
		saveState(object, doc, itemName, true, null);
	}

	/**
	 * Save state. Imported from org.openntf.domino
	 * 
	 * @param object
	 *            the object
	 * @param doc
	 *            the doc
	 * @param itemName
	 *            the item name
	 * @param compress
	 *            the compress
	 * @throws Throwable
	 *             the throwable
	 */
	private static void saveState(Serializable object, Document doc, String itemName, boolean compress, Map<String, String> headers) throws Throwable {
		Session session=doc.getParentDatabase().getParent();
		boolean convertMime = session.isConvertMime();
		session.setConvertMime(false);

		ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
		ObjectOutputStream objectStream = compress ? new ObjectOutputStream(new GZIPOutputStream(byteStream)) : new ObjectOutputStream(
				byteStream);
		String contentType = null;

		// Prefer externalization if available
		if(object instanceof Externalizable) {
			((Externalizable)object).writeExternal(objectStream);
			contentType = "application/x-java-externalized-object";
		} else {
			objectStream.writeObject(object);
			contentType = "application/x-java-serialized-object";
		}

		objectStream.flush();
		objectStream.close();

		Stream mimeStream = session.createStream();
		MIMEEntity previousState = doc.getMIMEEntity(itemName);
		MIMEEntity entity = previousState == null ? doc.createMIMEEntity(itemName) : previousState;
		ByteArrayInputStream byteIn = new ByteArrayInputStream(byteStream.toByteArray());
		mimeStream.setContents(byteIn);
		entity.setContentFromBytes(mimeStream, contentType, MIMEEntity.ENC_NONE);
		MIMEHeader contentEncoding = entity.getNthHeader("Content-Encoding");
		if (compress) {
			if (contentEncoding == null) {
				contentEncoding = entity.createHeader("Content-Encoding");
			}
			contentEncoding.setHeaderVal("gzip");
			contentEncoding.recycle();
		} else {
			if (contentEncoding != null) {
				contentEncoding.remove();
				contentEncoding.recycle();
			}
		}
		MIMEHeader javaClass = entity.getNthHeader("X-Java-Class");
		if (javaClass == null) {
			javaClass = entity.createHeader("X-Java-Class");
		}
		javaClass.setHeaderVal(object.getClass().getName());
		javaClass.recycle();

		if(headers != null) {
			for(Map.Entry<String, String> entry : headers.entrySet()) {
				MIMEHeader paramHeader = entity.getNthHeader(entry.getKey());
				if(paramHeader == null) {
					paramHeader = entity.createHeader(entry.getKey());
				}
				paramHeader.setHeaderVal(entry.getValue());
				paramHeader.recycle();
			}
		}

		entity.recycle();
		mimeStream.recycle();

		session.setConvertMime(convertMime);
	}

}





IBM Social Business Toolkit supports RDBMS-based persistent credential store for Endpoints. However it doesn't support NSF-based stores. So when you develop SBT applications on XPages, it would be pretty annoying that every time the classloader cleared (e.g.HTTP restart, Java class changes, etc.), you lose all credentials and tokens.

This class can be used as an alternative to Memory store to persist endpoint information in a notes document. It simply uses a MIME stream in a rich text item, thanks to the brilliant idea of Tim Tripcony.

Warning: This implementation is not using any security. All sensitive data would be accessible with a NotesClient. So use it for development and testing for now!

Next version I will convert it to sessionAsSigner but still, it should be supported by Reader/Author fields and even with encryption.


USAGE:

1. Copy the Java class into your NSF project.

2. Create a view named or aliased as "tokenstore".
Selection for the view : SELECT form="tokenstores"
First column, ascending sorted with "ServerName" field

3. Define the credential store in your Faces-Config file:

     <managed-bean-name>CredStore</managed-bean-name>
    <managed-bean-class>com.developi.sbt.CachedCredStore</managed-bean-class>
    <managed-bean-scope>application</managed-bean-scope>
    </managed-bean>

4. Use CredStore in your endpoint declarations

Java
Serdar Basegmez
January 21, 2014 7:31 AM
Rating
14

All code submitted to OpenNTF XSnippets, whether submitted as a "Snippet" or in the body of a Comment, is provided under the Apache License Version 2.0. See Terms of Use for full details.



1 comment(s)Login first to comment...
jeniffer homes
(at 04:28 on 19.12.2015)
This class can be used as an alternative to Memory store to persist endpoint information in a notes document. It simply uses a MIME stream in a rich text item, thanks to the brilliant idea of Tim Tripcony.