// Copyright 2006 Google Inc. // // 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 com.google.enterprise.connector.servlet; import com.google.enterprise.connector.spi.AuthenticationIdentity; import com.google.enterprise.connector.spi.SimpleAuthenticationIdentity; import com.google.enterprise.connector.util.XmlParseUtil; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; /** * This class parses the xml body of an Authorization request. * <p> * An authorization is a sequence of {@code ConnectorQuery} elements. Each * {@code ConnectorQuery} has a single {@code Identity} element and a sequence * of {@code Resource} elements. For example: * <pre> * <AuthorizationQuery> * <ConnectorQuery> * <Identity domain="domain" password="user1" source="connector">user1</Identity> * <Resource>googleconnector://connector1.localhost/doc?docid=doc1a</Resource> * <Resource>googleconnector://connector2.localhost/doc?docid=doc2a</Resource> * ... * </ConnectorQuery> * ... * </pre> * Note that both the {@code domain} and {@code password} attributes of the * {@code Identity} element are optional. */ public class AuthorizationParser { private static final Logger LOGGER = Logger.getLogger(AuthorizationParser.class.getName()); private final String xmlBody; private ConnectorMessageCode status; private int numDocs; private final Map<AuthenticationIdentity, ConnectorQueries> parseMap; public AuthorizationParser(String xmlBody) { this.xmlBody = xmlBody; parseMap = new HashMap<AuthenticationIdentity, ConnectorQueries>(); status = new ConnectorMessageCode(); numDocs = 0; parse(); } /** * Parse the Authorization Request XML into a hierarchy: AuthorizationParser, * ConnectorQueries, QueryResources. * <p> * The top-level, this object (AuthorizationParser) is a map from * AuthorizationIdentity objects to ConnectorQueries objects. This partitions * the request by Identity. * <p> * The second-level object, ConnectorQueries, is a map from connector name to * QueryResources objects. For each connector, it gives all the object for which * the request wants a decision. * <p> * The third-level object, QueryResources, is a map from docid strings to the * corresponding parsed representation as a ParsedUrl. * <p> * In practice, for now, it is unlikely that the same connector will show up * under more than one identity. In fact, the most likely case is that the top * two levels (AuthorizationParser and ConnectorQueries) each have only one * item. */ private void parse() { Element root = XmlParseUtil.parseAndGetRootElement(xmlBody, ServletUtil.XMLTAG_AUTHZ_QUERY); if (root == null) { setStatus(ConnectorMessageCode.ERROR_PARSING_XML_REQUEST); return; } NodeList queryList = root.getElementsByTagName(ServletUtil.XMLTAG_CONNECTOR_QUERY); if (queryList.getLength() == 0) { LOGGER.log(Level.WARNING, ServletUtil.LOG_RESPONSE_EMPTY_NODE); return; } numDocs = 0; for (int i = 0; i < queryList.getLength(); ++i) { Element queryItem = (Element) queryList.item(i); AuthenticationIdentity identity = parseIdentityGroup(queryItem); // Only consider Resources for which there is an associated identity. // A null Identity is considered an error on the part of the GSA. // Skip all its resources and continue with the next ConnectorQuery item. // Subsequently, this ConnectorQuery will not run and none of its // QueryResources will be returned. The GSA will then consider them // INDETERMINATE. if (identity != null) { parseResourceGroup(identity, queryItem); } } if (numDocs == 0) { LOGGER.warning("No docid available."); return; } } /** * Utility function to establish the first level mapping from the Identity * to the ConnectorQueries. */ private AuthenticationIdentity parseIdentityGroup(Element queryItem) { String username = XmlParseUtil.getFirstElementByTagName(queryItem, ServletUtil.XMLTAG_IDENTITY); String domain = XmlParseUtil.getFirstAttribute(queryItem, ServletUtil.XMLTAG_IDENTITY, ServletUtil.XMLTAG_DOMAIN_ATTRIBUTE); String password = XmlParseUtil.getFirstAttribute(queryItem, ServletUtil.XMLTAG_IDENTITY, ServletUtil.XMLTAG_PASSWORD_ATTRIBUTE); if (username == null) { LOGGER.warning("Null Identity"); // TODO: Is this the only way this can happen? LOGGER.warning("Flexible Authorization may be misconfigured to use " + "connector authorization with a credential group which has no " + "authentication rules defined."); setStatus(ConnectorMessageCode.RESPONSE_NULL_IDENTITY, "Null Identity"); return null; } AuthenticationIdentity identity = findIdentity(username, password, domain); ConnectorQueries urlsByConnector = getConnectorQueriesForIdentity(identity); if (urlsByConnector == null) { urlsByConnector = new ConnectorQueries(); putConnectorQueriesForIdentity(identity, urlsByConnector); } return identity; } /** * Utility function to establish the second level mapping from the connector * name to the QueryResources and the third level mapping from the docid to the * AuthorizationResource. * <p> * Unfortunately the response to this complex query only has one status so * it's all or nothing. If there is one improperly formatted element in the * query that is enough to bail on the request and set an appropriate status. * Therefore, the resources are validated at this point to determine if they * can be routed to a connector for authorization. */ private void parseResourceGroup(AuthenticationIdentity identity, Element queryItem) { NodeList resourceList = queryItem.getElementsByTagName(ServletUtil.XMLTAG_RESOURCE); if (resourceList.getLength() == 0) { LOGGER.warning("Null Resources"); setStatus(ConnectorMessageCode.RESPONSE_NULL_RESOURCE); return; } // Get the ConnectorQueries for the given Identity. ConnectorQueries urlsByConnector = getConnectorQueriesForIdentity(identity); for (int i = 0; i < resourceList.getLength(); ++i) { Element resourceItem = (Element) resourceList.item(i); AuthorizationResource resource = new AuthorizationResource(resourceItem); if (resource.getStatus() != ConnectorMessageCode.SUCCESS) { setStatus(resource.getStatus()); // Skip this failed resource and continue with the next one. // Since it was not added to the resources for this connector, it will // not get auto-DENY in AuthorizationHandler.accumlateQueryResults. // The GSA will then consider it INDETERMINATE. } else { // Create a mapping for this resource. QueryResources urlsByDocid = urlsByConnector.getQueryResources(resource.getConnectorName()); if (urlsByDocid == null) { urlsByDocid = new QueryResources(); urlsByConnector.putQueryResources(resource.getConnectorName(), urlsByDocid); } urlsByDocid.putResource(resource.getDocId(), resource); numDocs++; } } } // Package-level visibility for testing. static boolean matchesIdentity(AuthenticationIdentity id, String username, String password, String domain) { if (!id.getUsername().equals(username)) { return false; } if (!matchNullString(password, id.getPassword())) { return false; } if (!matchNullString(domain, id.getDomain())) { return false; } return true; } /** * Utility method to compare two strings with the special logic of treating * null the same as the empty string. * * @param string1 * @param string2 * @return true if the given strings are considered the same. */ private static boolean matchNullString(String string1, String string2) { String value1 = (string1 == null) ? "" : string1; String value2 = (string2 == null) ? "" : string2; return value1.equals(value2); } private AuthenticationIdentity findIdentity(String username, String password, String domain) { for (AuthenticationIdentity identity : parseMap.keySet()) { if (matchesIdentity(identity, username, password, domain)) { return identity; } } return new SimpleAuthenticationIdentity(username, password, domain); } public int getNumDocs() { return numDocs; } public void setStatus(int messageId) { setStatus(messageId, null); } public void setStatus(int messageId, String message) { // Only override a SUCCESS status, not any previous error. if (status.isSuccess()) { status = new ConnectorMessageCode(messageId, message, ConnectorMessageCode.EMPTY_PARAMS); } } public ConnectorMessageCode getStatus() { return status; } private void putConnectorQueriesForIdentity(AuthenticationIdentity identity, ConnectorQueries urlsByConnector) { parseMap.put(identity, urlsByConnector); } public Collection<AuthenticationIdentity> getIdentities() { return parseMap.keySet(); } public ConnectorQueries getConnectorQueriesForIdentity(AuthenticationIdentity identity) { return parseMap.get(identity); } // Note: this is for testing only -- thus package private int countParsedIdentities() { return parseMap.size(); } /** * {@code ConnectorQueries} is a map from connector name to QueryResources objects. * For each connector, it gives all the object for which the request wants a * decision. */ public static class ConnectorQueries { private final Map<String, QueryResources> queryMap; /* * Private constructor so this class can only be constructed by * AuthorizationParser. */ private ConnectorQueries() { queryMap = new HashMap<String, QueryResources>(); } public int size() { return queryMap.size(); } public Collection<String> getConnectors() { return queryMap.keySet(); } public QueryResources getQueryResources(String connector) { return queryMap.get(connector); } private QueryResources putQueryResources(String connector, QueryResources queryResources) { return queryMap.put(connector, queryResources); } } /** * {@code QueryResources} is a map from docid strings to the corresponding * {@link AuthorizationResource}. */ public static class QueryResources { Map<String, AuthorizationResource> resourceMap; /* * Private constructor so this class can only be constructed by * AuthorizationParser. */ private QueryResources() { resourceMap = new HashMap<String, AuthorizationResource>(); } private void putResource(String docid, AuthorizationResource p) { resourceMap.put(docid, p); } public Collection<String> getDocids() { return resourceMap.keySet(); } public int size() { return resourceMap.size(); } public AuthorizationResource getResource(String docid) { return resourceMap.get(docid); } } }