Java tutorial
/* * Copyright (c) 2011 United ID. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package org.unitedid.shibboleth.attribute.resolver.provider.dataConnector; import com.mongodb.*; import com.mongodb.util.JSON; import edu.internet2.middleware.shibboleth.common.attribute.BaseAttribute; import edu.internet2.middleware.shibboleth.common.attribute.provider.BasicAttribute; import edu.internet2.middleware.shibboleth.common.attribute.resolver.AttributeResolutionException; import edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.ShibbolethResolutionContext; import edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.dataConnector.BaseDataConnector; import edu.internet2.middleware.shibboleth.common.attribute.resolver.provider.dataConnector.TemplateEngine; import edu.internet2.middleware.shibboleth.common.profile.provider.SAMLProfileRequestContext; import edu.internet2.middleware.shibboleth.common.session.LogoutEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import java.util.*; /** * <code>MongoDbDataConnector</code> provides a plugin to fetch attributes from a mongo database. * * @author Stefan Wold <stefan.wold@unitedid.org> */ public class MongoDbDataConnector extends BaseDataConnector implements ApplicationListener { /** Class logger. */ private final Logger log = LoggerFactory.getLogger(MongoDbDataConnector.class); /** Indicates if this connector has been initialized. */ private boolean initialized; /** A list of mongo database servers. */ private List<ServerAddress> mongoHost; /** Mongo database. */ private String mongoDbName; /** Mongo collection */ private String mongoCollection; /** Username */ private String mongoUser; /** Password */ private String mongoPassword; /** Preferred read preference valid values */ private static final Map<String, ReadPreference> MONGO_READ_PREF = Collections .unmodifiableMap(new HashMap<String, ReadPreference>() { { put("primary", ReadPreference.primary()); put("primaryPreferred", ReadPreference.primaryPreferred()); put("secondary", ReadPreference.secondary()); put("secondaryPreferred", ReadPreference.secondaryPreferred()); put("nearest", ReadPreference.nearest()); } }); /** Preferred read preference for MongoDB */ private String preferredRead; /** Whether the query results should be cached */ private boolean cacheResults; /** Data cache */ private Map<String, Map<String, Map<String, BaseAttribute>>> dataCache; /** Mapping object between mongo document keys and shibboleth attributes */ private Map<String, MongoDbKeyAttributeMapper> keyAttributeMap; /** DB ojbect. */ private DB db; /** Template name that produces the query to use. */ private String queryTemplateName; /** Template that produces the query to use. */ private String queryTemplate; /** Template engine that create a real query from the query template. */ private TemplateEngine queryCreator; /** Whether persistent ID mode should be used. */ private boolean usePersistentId; /** ID of the attribute generated by this data connector. */ private String pidGeneratedAttributeId; /** ID of the attribute whose first value is used when generating a persistent ID. */ private String pidSourceAttributeId; /** * Constructor * * @param hosts the list of hosts to connect to * @param dbName the mongo database name to use * @param collection the mongo collection name to use */ public MongoDbDataConnector(List<ServerAddress> hosts, String dbName, String collection) { super(); mongoHost = hosts; mongoDbName = dbName; mongoCollection = collection; keyAttributeMap = new HashMap<String, MongoDbKeyAttributeMapper>(); } /** * Initializes the connector */ public void initialize() { initialized = true; registerTemplate(); initializeMongoDbConnection(); initializeCache(); } /** * Creates the mongo database connection */ protected void initializeMongoDbConnection() { if (initialized) { log.debug("MongoDB connector initializing!"); Mongo mongoCon = new Mongo(mongoHost); mongoCon.setReadPreference(getPreferredRead()); db = mongoCon.getDB(mongoDbName); if (getMongoUser() != null && getMongoPassword() != null) { boolean dbAuth = db.authenticate(getMongoUser(), getMongoPassword().toCharArray()); if (!dbAuth) { log.error( "MongoDB data connector {} authentication failed for database {}, username or password!", getId(), mongoDbName); throw new MongoException("MongoDB data connector " + getId() + " authentication failed!"); } else { log.debug("MongoDB data connector {} authentication successful!", getId()); } } } } /** * Initialize search results cache */ protected void initializeCache() { if (cacheResults && initialized) { dataCache = new HashMap<String, Map<String, Map<String, BaseAttribute>>>(); } } /** * Reset search results cache */ protected void resetCache() { if (cacheResults && initialized) { dataCache.clear(); } } /** * Registers the query template with template engine. */ protected void registerTemplate() { if (initialized) { queryTemplateName = "shibboleth.resolver.dc." + getId(); queryCreator.registerTemplate(queryTemplateName, queryTemplate); } } /** {@inheritDoc} */ public Map<String, BaseAttribute> resolve(ShibbolethResolutionContext resolutionContext) throws AttributeResolutionException { String query; if (isPersistentId()) { query = "persistentId"; } else { query = queryCreator.createStatement(queryTemplateName, resolutionContext, getDependencyIds(), null); } log.debug("Data connector {} search query: {}", getId(), query); Map<String, BaseAttribute> resolvedAttributes = new HashMap<String, BaseAttribute>(); //TODO: Implement cache timeout if (cacheResults) { log.debug("Data connector {} checking cache for attributes.", getId()); resolvedAttributes = getAttributeCache(resolutionContext, query); } if (resolvedAttributes == null || resolvedAttributes.isEmpty()) { log.debug("Data connector {} persistent id mode: {}", getId(), isPersistentId()); if (isPersistentId()) { resolvedAttributes = retrievePersistentIdAttribute(resolutionContext); } else { resolvedAttributes = retrieveAttributesFromDatabase(query); } if (cacheResults && resolvedAttributes != null) { setAttributeCache(resolutionContext, query, resolvedAttributes); } } return resolvedAttributes; } /** * Retrieves cached attributes for current resolution context * * @param resolutionContext <code>ResolutionContext</code> * @param query the query used to get the search result * @return cached attributes */ protected Map<String, BaseAttribute> getAttributeCache(ShibbolethResolutionContext resolutionContext, String query) { if (cacheResults) { String principal = resolutionContext.getAttributeRequestContext().getPrincipalName(); Map<String, Map<String, BaseAttribute>> cache = dataCache.get(principal); if (cache != null) { Map<String, BaseAttribute> attributes = cache.get(query); log.debug("Data connector {} got cached attributes for principal: {}", getId(), principal); return attributes; } } return null; } /** * Stores the attributes in the cache * * @param resolutionContext <code>ResolutionContext</code> * @param query the query used to get the search result * @param resolvedAttributes <code>Map</code> of attribute ids to attribute */ protected void setAttributeCache(ShibbolethResolutionContext resolutionContext, String query, Map<String, BaseAttribute> resolvedAttributes) { String principal = resolutionContext.getAttributeRequestContext().getPrincipalName(); Map<String, Map<String, BaseAttribute>> cache = dataCache.get(principal); if (cache == null) { cache = new HashMap<String, Map<String, BaseAttribute>>(); dataCache.put(principal, cache); } cache.put(query, resolvedAttributes); } /** {@inheritDoc} */ public void validate() throws AttributeResolutionException { log.debug("Validating data connector {} configuration.", getId()); Mongo connection = null; try { connection = new Mongo(mongoHost); if (connection == null) { log.error("Unable to create connections using {} data connector ", getId()); throw new AttributeResolutionException( "Unable to create connections using " + getId() + " data connector."); } } catch (MongoException e) { log.error("Unable to validate {} data connector", getId(), e); throw new AttributeResolutionException("Unable to validate " + getId() + " data connector: ", e); } finally { connection.close(); } } /** {@inheritDoc} */ public void onApplicationEvent(ApplicationEvent applicationEvent) { if (applicationEvent instanceof LogoutEvent) { LogoutEvent logoutEvent = (LogoutEvent) applicationEvent; if (cacheResults) { dataCache.remove(logoutEvent.getUserSession().getPrincipalName()); } } } /** * Retrieve attributes from the database based on the query * * @param q the query to run * @return a list of attributes * @throws AttributeResolutionException if an error occurs during query execution */ protected Map<String, BaseAttribute> retrieveAttributesFromDatabase(String q) throws AttributeResolutionException { Map<String, BaseAttribute> resolvedAttributes = new HashMap<String, BaseAttribute>(); try { log.debug("Data connector {} retrieving attributes from: {}", getId(), db.getMongo().getAddress()); DBCollection collection = db.getCollection(mongoCollection); DBObject query = (DBObject) JSON.parse(q); resolvedAttributes = processCollectionResult(collection.findOne(query)); } catch (MongoException e) { log.error("Data connector {} exception", getId(), e); throw new AttributeResolutionException("MongoDB data connector " + getId() + " unable to execute query", e); } return resolvedAttributes; } /** * Retrieve persistent id attribute for the current principal * * @param resolutionContext the current resolutionContext * @return persistent id attribute * @throws AttributeResolutionException if an error occurs when generating or fetching a persistent ID */ protected Map<String, BaseAttribute> retrievePersistentIdAttribute( ShibbolethResolutionContext resolutionContext) throws AttributeResolutionException { Map<String, BaseAttribute> resolvedAttributes = new HashMap<String, BaseAttribute>(); String persistentId = getPersistentId(resolutionContext); if (persistentId != null) { log.debug("Data connector {} got persistentId {} from backend.", getId(), persistentId); BasicAttribute<String> attribute = new BasicAttribute<String>(); attribute.setId(getPidGeneratedAttributeId()); attribute.getValues().add(persistentId); resolvedAttributes.put(attribute.getId(), attribute); } return resolvedAttributes; } /** * Process the result from the query and return a list of resolved attributes * * @param result the result from the query * @return a list of resolved attributes * @throws AttributeResolutionException if an error occurs when reading the mongo db result */ protected Map<String, BaseAttribute> processCollectionResult(DBObject result) throws AttributeResolutionException { Map<String, BaseAttribute> attributes = new HashMap<String, BaseAttribute>(); try { MongoDbKeyAttributeMapper keyAttributeMapper; BaseAttribute attribute; if (result != null) { for (String keyName : result.keySet()) { log.debug("Processing mongodb key: {} class: {}", keyName, result.get(keyName).getClass()); List<MongoDbKeyAttributeMapper> keyChildMap = null; keyAttributeMapper = keyAttributeMap.get(keyName); attribute = getAttribute(attributes, keyAttributeMapper, keyName); if (keyAttributeMapper != null) { keyChildMap = keyAttributeMapper.getChildKeyAttributeMaps(); } if (keyChildMap != null && keyChildMap.size() > 0) { BasicDBObject dataMap = (BasicDBObject) result.get(keyName); for (MongoDbKeyAttributeMapper map : keyChildMap) { attribute = getAttribute(attributes, map, map.getAttributeName()); attribute.getValues().add(dataMap.get(map.getMongoKey())); attributes.put(attribute.getId(), attribute); } } else if (result.get(keyName) instanceof com.mongodb.BasicDBList) { log.debug("Processing BasicDBList for {}.", keyName); BasicDBList res = (BasicDBList) result.get(keyName); List<String> resultList = new ArrayList<String>(); for (Object s : res) { if (s instanceof BasicDBObject) { log.error("BasicDBObjects in embedded lists not supported"); continue; } resultList.add((String) s); } attribute.getValues().addAll(resultList); attributes.put(attribute.getId(), attribute); } else { attribute.getValues().add(result.get(keyName)); attributes.put(attribute.getId(), attribute); } } } } catch (MongoException e) { log.error("Problem processing result {}:", getId(), e); throw new AttributeResolutionException("Problem processing result " + getId() + ":", e); } log.debug("MongoDb data connector {} attribute result: {}", getId(), attributes.keySet()); return attributes; } /** * Gets a base attribute object. * * @param attributes the attribute list * @param keyAttributeMapper the mongo db key attribute mapper * @param keyName the attribute name * @return base attribute */ private BaseAttribute getAttribute(Map<String, BaseAttribute> attributes, MongoDbKeyAttributeMapper keyAttributeMapper, String keyName) { BaseAttribute attribute; if (keyAttributeMapper == null || keyAttributeMapper.getAttributeName() == null) { attribute = attributes.get(keyName); if (attribute == null) { attribute = new BasicAttribute(keyName); } } else { attribute = attributes.get(keyAttributeMapper.getAttributeName()); if (attribute == null) { attribute = new BasicAttribute(keyAttributeMapper.getAttributeName()); } } return attribute; } /** * Derived from shibboleth storedId, Copyright [2007] [University Corporation for Advanced Internet Development, Inc.]. * * Gets the currently active identifier for a (principal, peer, local) tuple. * * @param resolutionContext current resolution context * @return persistent ID * @throws AttributeResolutionException if an error occurs when saving object to the database */ private String getPersistentId(ShibbolethResolutionContext resolutionContext) throws AttributeResolutionException { SAMLProfileRequestContext requestContext = resolutionContext.getAttributeRequestContext(); ArrayList<PersistentIdEntry> entries = new ArrayList<PersistentIdEntry>(); String localId = getLocalId(resolutionContext); try { DBCollection collection = db.getCollection(mongoCollection); BasicDBObject query = new BasicDBObject(); query.put("localEntity", requestContext.getLocalEntityId()); query.put("peerEntity", requestContext.getInboundMessageIssuer()); query.put("localId", localId); query.put("deactivationTime", new BasicDBObject("$exists", false)); log.debug("Data connector {} trying to fetch persistentId for principal: {}", getId(), localId); DBCursor result = collection.find(query); PersistentIdEntry entry; while (result.hasNext()) { DBObject r = result.next(); entry = new PersistentIdEntry(); entry.setLocalEntityId((String) r.get("localEntity")); entry.setPeerEntityId((String) r.get("peerEntity")); entry.setPrincipalName((String) r.get("principalName")); entry.setPersistentId((String) r.get("persistentId")); entry.setLocalId((String) r.get("localId")); entry.setCreationTime((Date) r.get("creationTime")); entry.setDeactivationTime((Date) r.get("deactivationTime")); entry.setLastVisitTime(new Date()); // Update last visit time BasicDBObject updateQuery = new BasicDBObject("$set", new BasicDBObject("lastVisitTime", entry.getLastVisitTime())); collection.update(r, updateQuery); entries.add(entry); } if (entries == null || entries.size() == 0) { log.debug("Data connector {} did not find a persistentId for principal: {}, generating a new one.", getId(), localId); entry = createPersistentId(resolutionContext, localId); savePersistentId(entry); entries.add(entry); } } catch (MongoException e) { log.error("MongoDB query failed", e); } return entries.get(0).getPersistentId(); } /** * Creates a persistent ID that is unique for a given local/peer/localId tuple. * * A random type 4 UUID is generated as the persistent ID. * * @param resolutionContext current resolution context * @param localId principal the persistent ID represents * @return persistent ID entry */ protected PersistentIdEntry createPersistentId(ShibbolethResolutionContext resolutionContext, String localId) { PersistentIdEntry persistentIdEntry = new PersistentIdEntry(); persistentIdEntry.setPeerEntityId(resolutionContext.getAttributeRequestContext().getPeerEntityId()); persistentIdEntry.setLocalEntityId(resolutionContext.getAttributeRequestContext().getLocalEntityId()); persistentIdEntry.setPrincipalName(resolutionContext.getAttributeRequestContext().getPrincipalName()); persistentIdEntry.setLocalId(localId); persistentIdEntry.setPersistentId(UUID.randomUUID().toString()); persistentIdEntry.setCreationTime(new Date()); persistentIdEntry.setLastVisitTime(new Date()); return persistentIdEntry; } /** * Save a persistent ID entry into the mongo database. * * @param entry entry to persist */ protected void savePersistentId(PersistentIdEntry entry) { try { DBCollection collection = db.getCollection(mongoCollection); BasicDBObject query = new BasicDBObject(); query.put("localEntity", entry.getLocalEntityId()); query.put("peerEntity", entry.getPeerEntityId()); query.put("principalName", entry.getPrincipalName()); query.put("localId", entry.getLocalId()); query.put("persistentId", entry.getPersistentId()); query.put("creationTime", entry.getCreationTime()); query.put("lastVisitTime", entry.getLastVisitTime()); collection.insert(query); } catch (MongoException e) { log.error("Failed to save persistent ID to the database", e); } } /** * Derived from shibboleth storedId, Copyright [2007] [University Corporation for Advanced Internet Development, Inc.]. * * Gets the local ID component of the persistent ID. * * @param resolutionContext current resolution context * @return local ID component of the persistent ID * @throws AttributeResolutionException if there is a problem resolving the local id */ protected String getLocalId(ShibbolethResolutionContext resolutionContext) throws AttributeResolutionException { Collection<Object> sourceIdValues = getValuesFromAllDependencies(resolutionContext, getPidSourceAttributeId()); if (sourceIdValues == null || sourceIdValues.isEmpty()) { log.error("Source attribute {} for connector {} provide no values", getPidSourceAttributeId(), getId()); throw new AttributeResolutionException("Source attribute " + getPidSourceAttributeId() + " for connector " + getId() + " provided no values"); } if (sourceIdValues.size() > 1) { log.warn("Source attribute {} for connector {} has more than one value, only the first value is used", getPidSourceAttributeId(), getId()); } return sourceIdValues.iterator().next().toString(); } /** * Gets the number of persistent ID entries for a (principal, peer, local) tuple. * * @param localEntity entity ID of the ID issuer * @param peerEntity entity ID of the peer the ID is for * @param localId ID part of the persistent ID * @return number of persistent ID entries for a (principal, peer, local) tuple */ public int getNumberOfPersistentIdEntries(String localEntity, String peerEntity, String localId) { try { DBCollection collection = db.getCollection(mongoCollection); log.debug("Get number of persistent ids for connector {}", getId()); BasicDBObject query = new BasicDBObject(); query.put("localEntity", localEntity); query.put("peerEntity", peerEntity); query.put("localId", localId); return collection.find(query).count(); } catch (MongoException e) { log.error("Mongodb query failed", e); } return 0; } /** * Gets the engine used to build the query. * * @return engine used to build the query */ public TemplateEngine getTemplateEngine() { return queryCreator; } /** * Sets the engine used to build the query. * * @param engine used to build the query */ public void setTemplateEngine(TemplateEngine engine) { queryCreator = engine; registerTemplate(); resetCache(); } /** * Gets the mongo database username. * * @return mongo database username */ public String getMongoUser() { return mongoUser; } /** * Sets the mongo database username. * * @param user the mongo database username */ public void setMongoUser(String user) { mongoUser = user; } /** * Gets the mongo database password. * * @return mongo database password */ public String getMongoPassword() { return mongoPassword; } /** * Sets the mongo database password. * * @param password the mongo database password */ public void setMongoPassword(String password) { mongoPassword = password; } /** * Gets preferred read method for MongoDB. * Defaults to primaryPreferred. * * @return a ReadPreference */ public ReadPreference getPreferredRead() { if (preferredRead != null && !preferredRead.isEmpty()) { return MONGO_READ_PREF.get(preferredRead); } return ReadPreference.primaryPreferred(); } /** * Sets the preferred read method for MongoDB. * * @param prefRead the preferred read keyword */ public void setPreferredRead(String prefRead) { if (MONGO_READ_PREF.containsKey(prefRead)) { preferredRead = prefRead; } else { throw new IllegalArgumentException( "Invalid value '" + prefRead + "'. Valid values are: " + MONGO_READ_PREF.keySet().toString()); } } /** * Gets whether to cache search results. * * @return cacheResults whether to cache query result */ public boolean isCacheResults() { return cacheResults; } /** * Sets whether to cache search results. * * @param cache whether to cache search results */ public void setCacheResults(boolean cache) { cacheResults = cache; } /** * Gets the template used to create a query. * * @return template used to createa a query */ public String getQueryTemplate() { return queryTemplate; } /** * Sets the template used to create a query. * * @param template used to create a query */ public void setQueryTemplate(String template) { queryTemplate = template; resetCache(); } /** * Gets a list of key and attribute mappings. * * @return list of key and attribute mappings */ public Map<String, MongoDbKeyAttributeMapper> getKeyAttributeMap() { return keyAttributeMap; } /** * Sets a map of key and attribute mappings. * * @param map of key and attribute mappings */ public void setKeyAttributeMap(Map<String, MongoDbKeyAttributeMapper> map) { keyAttributeMap = map; } /** * Gets whether persistent id mode is enabled. * * @return whether persistent id mode is enabled */ public boolean isPersistentId() { return usePersistentId; } /** * Sets whether to enable persistent id mode. * * @param persistentId whether to enable persistent id mode */ public void setPersistentId(boolean persistentId) { usePersistentId = persistentId; } /** * Gets the ID of the attribute generated by this data connector. * * @return ID of the attribute generated by this data connector */ public String getPidGeneratedAttributeId() { return pidGeneratedAttributeId; } /** * Sets the ID of the attribute generated by this data connector. * * @param attributeId the ID of the attribute generated by this data connector */ public void setPidGeneratedAttributeId(String attributeId) { pidGeneratedAttributeId = attributeId; } /** * Gets the ID of the attribute whose first value is used when generating a persistent ID. * * @return the ID of the attribute whose first value is used when generating a persistent ID */ public String getPidSourceAttributeId() { return pidSourceAttributeId; } /** * Sets the ID of the attribute whose first value is used when generating a persistent ID. * * @param attributeId the ID of the attribute whose first value is used when generating a persistent ID */ public void setPidSourceAttributeId(String attributeId) { pidSourceAttributeId = attributeId; } }