/** * Copyright (c) 2009--2015 Red Hat, Inc. * * This software is licensed to you under the GNU General Public License, * version 2 (GPLv2). There is NO WARRANTY for this software, express or * implied, including the implied warranties of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 * along with this software; if not, see * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. * * Red Hat trademarks are not licensed under GPLv2. No permission is * granted to use or replicate Red Hat trademarks that are incorporated * in this software or its documentation. */ package com.redhat.rhn.frontend.xmlrpc; import java.io.IOException; import java.io.StringReader; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.log4j.Logger; import org.xml.sax.SAXException; import org.hibernate.HibernateException; import redstone.xmlrpc.XmlRpcFault; import redstone.xmlrpc.XmlRpcInvocationHandler; import com.redhat.rhn.FaultException; import com.redhat.rhn.common.client.ClientCertificate; import com.redhat.rhn.common.client.ClientCertificateDigester; import com.redhat.rhn.common.client.InvalidCertificateException; import com.redhat.rhn.common.hibernate.HibernateFactory; import com.redhat.rhn.common.hibernate.LookupException; import com.redhat.rhn.common.translation.Translator; import com.redhat.rhn.common.util.MethodUtil; import com.redhat.rhn.common.util.StringUtil; import com.redhat.rhn.domain.entitlement.Entitlement; import com.redhat.rhn.domain.org.Org; import com.redhat.rhn.domain.org.OrgFactory; import com.redhat.rhn.domain.role.Role; import com.redhat.rhn.domain.role.RoleFactory; import com.redhat.rhn.domain.server.Server; import com.redhat.rhn.domain.session.WebSession; import com.redhat.rhn.domain.user.User; import com.redhat.rhn.manager.session.SessionManager; import com.redhat.rhn.manager.system.SystemManager; /** * A basic xmlrpc handler class. Uses reflection + an arbitrary algorithm * to call the appropriate method on a subclass. So, an xmlrpc call to * 'registration.privacy_message' might call * RegistrationHandler.privacyMessage */ public class BaseHandler implements XmlRpcInvocationHandler { public static final int VALID = 1; private static Logger log = Logger.getLogger(BaseHandler.class); private static final String RO_REGEX = "^(list|get|is|find).*$"; private static final String KEY_REGEX = "^[1-9][0-9]*x[a-f0-9]{64}$"; protected boolean providesAuthentication() { return false; } /** * called by BaseHandler.doPost, contains the code that determines what * method to call of a subclassed-object * * @param methodCalled the xmlrpc function called, like * 'registration.privacy_statement' * @param params a Vector of the parameters to methodCalled * @return the results of the method of the subclass * @exception XmlRpcFault if some error occurs */ public Object invoke(String methodCalled, List params) throws XmlRpcFault { Class myClass = this.getClass(); Method[] methods; try { methods = myClass.getMethods(); } catch (SecurityException e) { // This should _never_ happen, because the Handler classes must // have public classes if they're expected to work. throw new XmlRpcFault(-1, "no public methods in class " + myClass); } String[] byNamespace = methodCalled.split("\\."); String beanifiedMethod = StringUtil.beanify(byNamespace[byNamespace.length - 1]); WebSession session = null; if (params.size() > 0 && params.get(0) instanceof String && isSessionKey((String)params.get(0))) { if (!myClass.getName().endsWith("AuthHandler") && !myClass.getName().endsWith("SearchHandler")) { session = SessionManager.loadSession((String)params.get(0)); params.set(0, getLoggedInUser((String)params.get(0))); if (((User)params.get(0)).isReadOnly()) { if (!beanifiedMethod.matches(RO_REGEX)) { throw new SecurityException("The " + beanifiedMethod + " API is not available to read-only API users"); } } } } //we've found all the methods that have the same number of parameters List<Method> matchedMethods = findMethods(methods, params, beanifiedMethod); //Attempt to find a perfect match Method foundMethod = findPerfectMethod(params, matchedMethods); Object[] converted = params.toArray(); //If we were not able to find the exact method match, let's just use the first one // This isn't the best method, but if you can figure out a better way // that is easy feel free to change. //Since it is not an exact match, we have to translate the params. if (foundMethod == null) { foundMethod = matchedMethods.get(0); Class[] types = foundMethod.getParameterTypes(); Iterator iter = params.iterator(); for (int i = 0; i < types.length; i++) { Object curr = iter.next(); if (!types[i].equals(curr.getClass())) { converted[i] = Translator.convert(curr, types[i]); } } } try { return foundMethod.invoke(this, converted); } catch (IllegalAccessException e) { throw new XmlRpcFault(-1, "unhandled internal exception"); } catch (InvocationTargetException e) { Throwable t = e.getCause(); log.error("Error calling method: ", e); log.error("Caused by: ", t); /* * HACK: this should really be handled by SessionFilter.doFilter, * but unfortunately Redstone XMLRPC swallows our exceptions (see * XmlRpcDispatcher.dispatch()). Thus doFilter will never be reached * with exceptions, and will always COMMIT the transaction. To avoid * committing changes after an Exception, roll back here. */ try { log.error("Rolling back transaction"); HibernateFactory.rollbackTransaction(); } catch (HibernateException he) { log.error("Additional error during rollback", he); } // This works because FaultException extends XmlRpcFault, so // we are telling the XMLRPC library to send a fault to the client. if (t instanceof FaultException) { FaultException fe = (FaultException)t; throw new XmlRpcFault(fe.getErrorCode(), fe.getMessage()); } // If it isn't a FaultException that caused this, we still need to // send something to the client. Throwable cause = e.getCause(); // If we can get the cause of the exception, then display the message if (cause != null) { throw new XmlRpcFault(-1, "unhandled internal exception: " + cause.getLocalizedMessage()); } // Otherwise, throw the generic unhandled internal exception throw new XmlRpcFault(-1, "unhandled internal exception"); } finally { if (session != null) { SessionManager.extendSessionLifetime(session); } } } /** * Finds the perfect match for a method based upon type * @param params The parameters to find the match for. * @param matchedMethods the list of methods to check for a perfect match * @return null if no perfect match was found, otherwise the matched method. */ private Method findPerfectMethod(List params, List<Method> matchedMethods) { //now lets try to find one that matches parameters exactly for (Method currMethod : matchedMethods) { Class[] types = currMethod.getParameterTypes(); for (int i = 0; i < types.length; i++) { //if we find a param that doesn't match, go to the next method if (!types[i].isAssignableFrom(params.get(i).getClass())) { break; } //if we have gone through all of the params, and are here it is a // perfect match. if (i == types.length - 1) { return currMethod; } } } return null; } /** * Private method to find the method in the java class that is being called * via xml-rpc * @param methods The methods contained in the class * @param params The parameters sent to us via xml-rpc * @param beanifiedMethod The method name we are looking for * @return The matching method we're looking for * @throws XmlRpcFault Thrown if we can't find the method asked for */ /* * TODO: Make this method even smarterer. * Currently this finds methods that match the number of parameters and returns * those. */ private List<Method> findMethods(Method[] methods, Collection params, String beanifiedMethod) throws XmlRpcFault { List<Method> toReturn = new ArrayList<Method>(); //Loop through the methods array and find the one we are trying to call. for (int i = 0; i < methods.length; i++) { if (methods[i].getName().equals(beanifiedMethod)) { // We found a method with the right name, but does the parameter count // match? int numberOfParams = methods[i].getParameterTypes().length; if (numberOfParams == params.size()) { //Method name and number of parameters match. toReturn.add(methods[i]); } } } if (toReturn.isEmpty()) { //The caller didn't get the method name or number of parameters right String message = "Could not find method: " + beanifiedMethod + " in class: " + this.getClass().getName() + " with params: ["; for (Iterator iter = params.iterator(); iter.hasNext();) { Object param = iter.next(); message += (param.getClass().getName()); if (iter.hasNext()) { message = message + ", "; } } message = message + "]"; throw new XmlRpcFault(-1, message); } return toReturn; } /** * Gets the currently logged in user. This is all done through the sessionkey we send * the user in AuthHandler.login. * @param sessionKey The key containing the session id that we can use to load the * session. * @return Returns the user logged into the session corresponding to the given * sessionkey. */ public static User getLoggedInUser(String sessionKey) { //Load the session WebSession session = SessionManager.loadSession(sessionKey); User user = session.getUser(); //Make sure there was a valid user in the session. If not, the session is invalid. if (user == null) { throw new LookupException("Could not find a valid user for session with key: " + sessionKey); } //Return the logged in user return user; } /** * Private helper method to make sure a user has org admin role. If not, this will * throw a generic Permission exception. * @param user The user to check * @throws PermissionCheckFailureException if user is not an org admin */ public static void ensureOrgAdmin(User user) throws PermissionCheckFailureException { ensureUserRole(user, RoleFactory.ORG_ADMIN); } /** * Private helper method to make sure a user has sat admin role. If not, this will * throw a generic Permission exception. * @param user The user to check * @throws PermissionCheckFailureException if user is not an sat admin */ public static void ensureSatAdmin(User user) throws PermissionCheckFailureException { ensureUserRole(user, RoleFactory.SAT_ADMIN); } /** * Private helper method to make sure a user has system group admin role. * If not, this will throw a generic Permission exception. * @param user The user to check * @throws PermissionCheckFailureException if user is not a system group admin */ public static void ensureSystemGroupAdmin(User user) throws PermissionCheckFailureException { ensureUserRole(user, RoleFactory.SYSTEM_GROUP_ADMIN); } /** * Private helper method to make sure a user has config admin role. * If not, this will throw a generic Permission exception. * @param user The user to check * @throws PermissionCheckFailureException if user is not a config admin. */ public static void ensureConfigAdmin(User user) throws PermissionCheckFailureException { ensureUserRole(user, RoleFactory.CONFIG_ADMIN); } /** * Public helper method to make sure a user has either * an org admin or a config admin role * If not, this will throw a generic Permission exception. * @param user The user to check * @throws PermissionCheckFailureException if user is neither org nor config admin. */ public static void ensureOrgOrConfigAdmin(User user) throws PermissionCheckFailureException { if (!user.hasRole(RoleFactory.ORG_ADMIN) && !user.hasRole(RoleFactory.CONFIG_ADMIN)) { throw new PermissionCheckFailureException(RoleFactory.ORG_ADMIN, RoleFactory.CONFIG_ADMIN); } } /** * Private helper method to make sure a user the given role.. * If not, this will throw a generic Permission exception. * @param user The user to check * @param role the role to check * @throws PermissionCheckFailureException if user does not * have the given role */ public static void ensureUserRole(User user, Role role) throws PermissionCheckFailureException { if (!user.hasRole(role)) { throw new PermissionCheckFailureException(role); } } /** * Ensure the org exists * @param orgId the org id to check * @return the org */ protected Org verifyOrgExists(Number orgId) { if (orgId == null) { throw new NoSuchOrgException("null Id"); } Org org = OrgFactory.lookupById(orgId.longValue()); if (org == null) { throw new NoSuchOrgException(orgId.toString()); } return org; } /** * Validate that specified entitlement names correspond to real entitlements * that can be changed via API (in other words, they are not permanent). * * @param entitlements List of string entitlement labels to be validated. */ protected void validateEntitlements(List<Entitlement> entitlements) { for (Entitlement ent : entitlements) { if ((ent == null) || (ent.isPermanent())) { throw new InvalidEntitlementException(); } } } /** * Validate that the keys provided in the map provided * by the user are valid. * @param validKeys Set of keys that are valid for this request * @param map The map to validate */ protected void validateMap(Set<String> validKeys, Map map) { String errors = null; for (Iterator it = map.keySet().iterator(); it.hasNext();) { String key = (String) it.next(); if (!validKeys.contains(key)) { // user passed an invalid key... if (errors == null) { errors = new String(key); } else { errors += ", " + key; } } } if (errors != null) { // at least one invalid key was found... throw new InvalidArgsException(errors); } } /** * Take an attributeName and value, and apply them to an Object. * Takes advantage of introspection and bean-stds to decide what call to make * @param attrName Attribute to set - assumes entity.set<Attrname>(value) exists * @param entity The Object we are updating * @param value The new value to pass to set<AttrName> */ protected void setEntityAttribute(String attrName, Object entity, Object value) { String methodName = StringUtil.beanify("set_" + attrName); Object[] params = { value }; MethodUtil.callMethod(entity, methodName, params); } protected Server validateClientCertificate(String clientCert) { StringReader rdr = new StringReader(clientCert); Server server = null; ClientCertificate cert; try { cert = ClientCertificateDigester.buildCertificate(rdr); server = SystemManager.lookupByCert(cert); } catch (IOException ioe) { log.error("IOException - Trying to access a system with an " + "invalid certificate", ioe); throw new MethodInvalidParamException(); } catch (SAXException se) { log.error("SAXException - Trying to access a " + "system with an invalid certificate", se); throw new MethodInvalidParamException(); } catch (InvalidCertificateException e) { log.error("InvalidCertificateException - Trying to access a " + "system with an invalid certificate", e); throw new MethodInvalidParamException(); } if (server == null) { throw new NoSuchSystemException(); } return server; } private boolean isSessionKey(String string) { return string.matches(KEY_REGEX); } }