public abstract class AuthProvider { private static Log sLog = LogFactory.getLog(AuthProvider.class); // registered/installed providers private static Map<String, AuthProvider> registeredProviders = new HashMap<String, AuthProvider>(); // ordered list of enabled providers private static List<AuthProvider> enabledProviders = null; static { register(new ZimbraAuthProvider()); // register(new ZimbraOAuthProvider()); refresh(); } public static synchronized void register(AuthProvider ap) { String name = ap.getName(); logger().info("Adding auth provider: " + name + " " + ap.getClass().getName()); if (registeredProviders.get(name) == null) { registeredProviders.put(name, ap); } else { logger() .error( "auth provider " + name + " already exists, not adding " + ap.getClass().getName()); } } /** * refresh the enabled cache * * <p>TODO, can be called from zmprov flushCache to flush the enabled cache */ public static void refresh() { List<AuthProvider> providerList = new ArrayList<AuthProvider>(); String[] providers = LC.zimbra_auth_provider.value().split(","); for (String provider : providers) { /* * ignore zimbra and oauth auth providers, they are built-in providers. * * If no external auth providers is configured, zimbra and oauth providers * will be used. */ if (ZimbraAuthProvider.ZIMBRA_AUTH_PROVIDER.equals(provider) || ZimbraOAuthProvider.ZIMBRA_OAUTH_PROVIDER.equals(provider)) { continue; } provider = provider.trim(); if (!Strings.isNullOrEmpty(provider)) { AuthProvider ap = registeredProviders.get(provider); if (ap != null) { providerList.add(ap); } } } // always add the zimbra providers if there is no provider configured. if (providerList.size() == 0) { providerList.add(registeredProviders.get(ZimbraAuthProvider.ZIMBRA_AUTH_PROVIDER)); // providerList.add(registeredProviders.get(ZimbraOAuthProvider.ZIMBRA_OAUTH_PROVIDER)); } setProviders(providerList); } private static synchronized void setProviders(List<AuthProvider> providers) { enabledProviders = providers; } private static synchronized List<AuthProvider> getProviders() { return enabledProviders; } private String mName; protected AuthProvider(String name) { mName = name; } private String getName() { return mName; } protected static Log logger() { return sLog; } /** * Returns an AuthToken by auth data in http request * * <p>Should never return null. Throws AuthProviderException.NO_AUTH_TOKEN if auth data for the * provider is not present Throws AuthTokenException if auth data for the provider is present but * cannot be resolved into a valid AuthToken * * @param req * @param isAdmin * @return * @throws AuthTokenException */ protected abstract AuthToken authToken(HttpServletRequest req, boolean isAdminReq) throws AuthProviderException, AuthTokenException; /** * Returns an AuthToken by auth data in http request * * <p>Should never return null. Throws AuthProviderException.NO_AUTH_TOKEN if auth data for the * provider is not present Throws AuthTokenException if auth data for the provider is present but * cannot be resolved into a valid AuthToken * * @param soapCtxt * @param engineCtxt * @param isAdmin * @return * @throws AuthTokenException */ protected abstract AuthToken authToken(Element soapCtxt, Map engineCtxt) throws AuthProviderException, AuthTokenException; /** * Returns an AuthToken by auth data in the <authToken> element. * * @param authTokenElem * @param acct TODO: This may not be needed if we can get account info from the OAuth access * token. * @return * @throws AuthProviderException * @throws AuthTokenException */ protected AuthToken authToken(Element authTokenElem, Account acct) throws AuthProviderException, AuthTokenException { // default implementation just extracts the text and calls the authToken(String) // the acct parameter is ignored. String token = authTokenElem.getText(); return authToken(token); } /** * Returns an AuthToken from an encoded String. * * <p>This API is for servlets that support auth from a non-cookie channel, where it honors a * String token from a specific element in the request, which is neither a cookie nor a SOAP * context header. e.g. a query param. * * <p>By default, an AuthProvider do not need to implement this method. The default implementation * is throwing AuthProviderException.NOT_SUPPORTED. * * <p>Should never return null. Throws AuthProviderException.NO_SUPPORTED if this API is not * supported by the auth provider Throws AuthProviderException.NO_AUTH_TOKEN if auth data for the * provider is not present Throws AuthTokenException if auth data for the provider is present but * cannot be resolved into a valid AuthToken * * @param encoded * @return * @throws AuthProviderException * @throws AuthTokenException */ protected AuthToken authToken(String encoded) throws AuthProviderException, AuthTokenException { throw AuthProviderException.NOT_SUPPORTED(); } /** * Returns an AuthToken from the account object. Should never return null. * * @param acct * @return * @throws AuthProviderException */ protected AuthToken authToken(Account acct) throws AuthProviderException { return authToken(acct, false, null); } /** * Returns an AuthToken from the account object. Should never return null. * * @param acct * @param isAdmin * @param authMech * @return * @throws AuthProviderException */ protected AuthToken authToken(Account acct, boolean isAdmin, AuthMech authMech) throws AuthProviderException { if (acct == null) { throw AuthProviderException.NOT_SUPPORTED(); } long lifetime = isAdmin ? acct.getTimeInterval( Provisioning.A_zimbraAdminAuthTokenLifetime, AuthToken.DEFAULT_AUTH_LIFETIME * 1000) : acct.getTimeInterval( Provisioning.A_zimbraAuthTokenLifetime, AuthToken.DEFAULT_AUTH_LIFETIME * 1000); return authToken(acct, lifetime); } /** * Returns an AuthToken from the account object with specified lifetime. Should never return null. * * @param acct * @param expires * @return * @throws AuthProviderException */ protected AuthToken authToken(Account acct, long expires) throws AuthProviderException { throw AuthProviderException.NOT_SUPPORTED(); } /** * Returns an AuthToken from the account object with specified expiration. Should never return * null. * * @param acct * @param expires * @param isAdmin * @param adminAcct * @param acct account authtoken will be valid for * @param expires when the token expires * @param isAdmin true if acct is using its admin privileges * @param adminAcct the admin account accessing acct's information, if this token was created by * an admin. * @return * @throws AuthProviderException */ protected AuthToken authToken(Account acct, long expires, boolean isAdmin, Account adminAcct) throws AuthProviderException { throw AuthProviderException.NOT_SUPPORTED(); } /** * @param req * @return whether http basic authentication is allowed */ protected boolean allowHttpBasicAuth(HttpServletRequest req, ZimbraServlet servlet) { return true; } /** * @param req * @return whether accesskey authentication is allowed */ protected boolean allowURLAccessKeyAuth(HttpServletRequest req, ZimbraServlet servlet) { return false; } /** * The static getAuthToken methods go through all the providers, trying them in order until one * returns an AuthToken. Return null when there is no auth data for any of the enabled providers * Throw AuthTokenException if any provider in the chain throws an AuthTokenException. * * <p>Note: 1. we proceed to try the next provider if the provider throws an AuthProviderException * that is ignorable (AuthProviderException.canIgnore()). for example: * AuthProviderException.NO_AUTH_TOKEN, AuthProviderException.NOT_SUPPORTED. * * <p>2. in all other cases, we stop processing and throws an AuthTokenException to our caller. In * particular, when a provider: - returns null -> it should not. Treat it as a provider error and * throws AuthTokenException - throws AuthTokenException, this means auth data is present for a * provider but it cannot be resolved into a valid AuthToken. - Any AuthProviderException that is * not ignorable. */ /** * @param req http request * @return an AuthToken object, or null if auth data is not present for any of the enabled * providers * @throws ServiceException */ public static AuthToken getAuthToken(HttpServletRequest req, boolean isAdminReq) throws AuthTokenException { AuthToken at = null; List<AuthProvider> providers = getProviders(); for (AuthProvider ap : providers) { try { at = ap.authToken(req, isAdminReq); if (at == null) { throw new AuthTokenException("auth provider " + ap.getName() + " returned null"); } else { return at; } } catch (AuthProviderException e) { // if there is no auth data for this provider, log and continue with next provider if (e.canIgnore()) { logger().debug(ap.getName() + ":" + e.getMessage()); } else { throw new AuthTokenException("auth provider error", e); } } catch (AuthTokenException e) { // log and rethrow logger() .debug("getAuthToken error: provider=" + ap.getName() + ", err=" + e.getMessage(), e); throw e; } } // there is no auth data for any of the enabled providers return null; } /** * For SOAP, we do not pass in isAdminReq, because with the current flow in SoapEngine, at the * point when the SOAP context(ZimbraSoapContext) is examined, we haven't looked at the SOAP body * yet. Whether admin auth is required is based on the SOAP command, which has to be extracted * from the body. ZimbraAuthProvider always retrieves the encoded auth token from the fixed tag, * so does YahooYT auth. This should be fine for now. * * @param soapCtxt <context> element in SOAP header * @param engineCtxt soap engine context * @return an AuthToken object, or null if auth data is not present for any of the enabled * providers * @throws AuthTokenException */ public static AuthToken getAuthToken(Element soapCtxt, Map engineCtxt) throws AuthTokenException { AuthToken at = null; List<AuthProvider> providers = getProviders(); for (AuthProvider ap : providers) { try { at = ap.authToken(soapCtxt, engineCtxt); if (at == null) { throw new AuthTokenException("auth provider " + ap.getName() + " returned null"); } else { return at; } } catch (AuthProviderException e) { // if there is no auth data for this provider, log and continue with next provider if (e.canIgnore()) { logger().debug(ap.getName() + ":" + e.getMessage()); } else { throw new AuthTokenException("auth provider error", e); } } catch (AuthTokenException e) { // log and rethrow logger() .debug("getAuthToken error: provider=" + ap.getName() + ", err=" + e.getMessage(), e); throw e; } } // there is no auth data for any of the enabled providers return null; } public static AuthToken getAuthToken(Element authTokenElem, Account acct) throws AuthTokenException { AuthToken at = null; List<AuthProvider> providers = getProviders(); for (AuthProvider ap : providers) { try { at = ap.authToken(authTokenElem, acct); if (at == null) { throw new AuthTokenException("auth provider " + ap.getName() + " returned null"); } else { return at; } } catch (AuthProviderException e) { // if there is no auth data for this provider, log and continue with next provider if (e.canIgnore()) { logger().debug(ap.getName() + ":" + e.getMessage()); } else { throw new AuthTokenException("auth provider error", e); } } catch (AuthTokenException e) { // log and rethrow logger() .debug("getAuthToken error: provider=" + ap.getName() + ", err=" + e.getMessage(), e); throw e; } } // there is no auth data for any of the enabled providers return null; } /** * Creates an AuthToken object from token string. * * @param encoded * @return * @throws AuthTokenException * @see #authToken(String) */ public static AuthToken getAuthToken(String encoded) throws AuthTokenException { AuthToken at = null; List<AuthProvider> providers = getProviders(); for (AuthProvider ap : providers) { try { at = ap.authToken(encoded); if (at == null) { throw new AuthTokenException("auth provider " + ap.getName() + " returned null"); } else { return at; } } catch (AuthProviderException e) { // if there is no auth data for this provider, log and continue with next provider if (e.canIgnore()) { logger().warn(ap.getName() + ":" + e.getMessage()); } else { throw new AuthTokenException("auth provider error", e); } } catch (AuthTokenException e) { // log and rethrow logger() .debug("getAuthToken error: provider=" + ap.getName() + ", err=" + e.getMessage(), e); throw e; } } // there is no auth data for any of the enabled providers logger().error("unable to get AuthToken from encoded " + encoded); return null; } public static AuthToken getAuthToken(Account acct) throws AuthProviderException { List<AuthProvider> providers = getProviders(); for (AuthProvider ap : providers) { try { AuthToken at = ap.authToken(acct); if (at == null) { throw AuthProviderException.FAILURE("auth provider " + ap.getName() + " returned null"); } else { return at; } } catch (AuthProviderException e) { if (e.canIgnore()) { logger().debug(ap.getName() + ":" + e.getMessage()); } else { throw e; } } } throw AuthProviderException.FAILURE("cannot get authtoken from account " + acct.getName()); } public static AuthToken getAuthToken(Account acct, boolean isAdmin) throws AuthProviderException { return getAuthToken(acct, isAdmin, null); } public static AuthToken getAuthToken(Account acct, boolean isAdmin, AuthMech authMech) throws AuthProviderException { List<AuthProvider> providers = getProviders(); for (AuthProvider ap : providers) { try { AuthToken at = ap.authToken(acct, isAdmin, authMech); if (at == null) { throw AuthProviderException.FAILURE("auth provider " + ap.getName() + " returned null"); } else { return at; } } catch (AuthProviderException e) { if (e.canIgnore()) { logger().debug(ap.getName() + ":" + e.getMessage()); } else { throw e; } } } String acctName = acct != null ? acct.getName() : "null"; throw AuthProviderException.FAILURE("cannot get authtoken from account " + acctName); } public static AuthToken getAdminAuthToken() throws ServiceException { Account acct = Provisioning.getInstance().get(AccountBy.adminName, LC.zimbra_ldap_user.value()); return AuthProvider.getAuthToken(acct, true); } public static AuthToken getAuthToken(Account acct, long expires) throws AuthProviderException { List<AuthProvider> providers = getProviders(); for (AuthProvider ap : providers) { try { AuthToken at = ap.authToken(acct, expires); if (at == null) { throw AuthProviderException.FAILURE("auth provider " + ap.getName() + " returned null"); } else { return at; } } catch (AuthProviderException e) { if (e.canIgnore()) { logger().debug(ap.getName() + ":" + e.getMessage()); } else { throw e; } } } throw AuthProviderException.FAILURE("cannot get authtoken from account " + acct.getName()); } public static AuthToken getAuthToken( Account acct, long expires, boolean isAdmin, Account adminAcct) throws AuthProviderException { List<AuthProvider> providers = getProviders(); for (AuthProvider ap : providers) { try { AuthToken at = ap.authToken(acct, expires, isAdmin, adminAcct); if (at == null) { throw AuthProviderException.FAILURE("auth provider " + ap.getName() + " returned null"); } else { return at; } } catch (AuthProviderException e) { if (e.canIgnore()) { logger().debug(ap.getName() + ":" + e.getMessage()); } else { throw e; } } } throw AuthProviderException.FAILURE("cannot get authtoken from account " + acct.getName()); } public static boolean allowBasicAuth(HttpServletRequest req, ZimbraServlet servlet) { List<AuthProvider> providers = getProviders(); for (AuthProvider ap : providers) { if (ap.allowHttpBasicAuth(req, servlet)) { return true; } } return false; } public static boolean allowAccessKeyAuth(HttpServletRequest req, ZimbraServlet servlet) { List<AuthProvider> providers = getProviders(); for (AuthProvider ap : providers) { if (ap.allowURLAccessKeyAuth(req, servlet)) { return true; } } return false; } public static Account validateAuthToken( Provisioning prov, AuthToken at, boolean addToLoggingContext) throws ServiceException { try { return validateAuthTokenInternal(prov, at, addToLoggingContext); } catch (ServiceException e) { if (ServiceException.AUTH_EXPIRED.equals(e.getCode())) { // we may not want to expose the details to malicious caller // debug log the message and throw a vanilla AUTH_EXPIRED ZimbraLog.account.debug("auth token validation failed", e); throw ServiceException.AUTH_EXPIRED(); } else { // rethrow the same exception throw e; } } } private static Account validateAuthTokenInternal( Provisioning prov, AuthToken at, boolean addToLoggingContext) throws ServiceException { if (prov == null) { prov = Provisioning.getInstance(); } if (at.isExpired()) { throw ServiceException.AUTH_EXPIRED(); } // make sure that the authenticated account is still active and has not been deleted since the // last request String acctId = at.getAccountId(); Account acct = prov.get(AccountBy.id, acctId, at); if (acct == null) { throw ServiceException.AUTH_EXPIRED("account " + acctId + " not found"); } if (addToLoggingContext) { ZimbraLog.addAccountNameToContext(acct.getName()); } if (!acct.checkAuthTokenValidityValue(at)) { throw ServiceException.AUTH_EXPIRED("invalid validity value"); } boolean delegatedAuth = at.isDelegatedAuth(); String acctStatus = acct.getAccountStatus(prov); if (!delegatedAuth && !Provisioning.ACCOUNT_STATUS_ACTIVE.equals(acctStatus)) { throw ServiceException.AUTH_EXPIRED("account not active"); } // if using delegated auth, make sure the "admin" is really an active admin account if (delegatedAuth) { // note that delegated auth allows access unless the account's in maintenance mode if (Provisioning.ACCOUNT_STATUS_MAINTENANCE.equals(acctStatus)) { throw ServiceException.AUTH_EXPIRED("delegated account in MAINTENANCE mode"); } Account admin = prov.get(AccountBy.id, at.getAdminAccountId()); if (admin == null) { throw ServiceException.AUTH_EXPIRED( "delegating account " + at.getAdminAccountId() + " not found"); } boolean isAdmin = AdminAccessControl.isAdequateAdminAccount(admin); if (!isAdmin) { throw ServiceException.PERM_DENIED("not an admin for delegated auth"); } if (!Provisioning.ACCOUNT_STATUS_ACTIVE.equals(admin.getAccountStatus(prov))) { throw ServiceException.AUTH_EXPIRED("delegating account is not active"); } } return acct; } }
/** * Caches uncompressed versions of compressed files on disk. Uses the digest of the file internally * to dedupe multiple copies of the same data. The cache size can be limited by the number of files * or the total size. * * @param <K> the type of key used to look up files in the cache */ public class UncompressedFileCache<K> { private static final Log sLog = LogFactory.getLog(UncompressedFileCache.class); private File mCacheDir; /** Maps the key to the cache to the uncompressed file digest. */ private Map<K, String> mKeyToDigest; /** Reverse map of the digest to all the keys that reference it. */ private Map<String, Set<K>> mDigestToKeys; /** All the files in the cache, indexed by digest. */ private LinkedHashMap<String, File> mDigestToFile; private long mNumBytes = 0; public UncompressedFileCache(String path) { if (path == null) { throw new NullPointerException("Path cannot be null."); } mCacheDir = new File(path); } /** * Initializes the cache and deletes any existing files. Call this method before using the cache. */ public synchronized UncompressedFileCache<K> startup() throws IOException { if (!mCacheDir.exists()) throw new IOException("uncompressed file cache folder does not exist: " + mCacheDir); if (!mCacheDir.isDirectory()) throw new IOException("uncompressed file cache folder is not a directory: " + mCacheDir); // Create caches with default LinkedHashMap values, but sorted by last access time. mKeyToDigest = new HashMap<K, String>(); mDigestToKeys = new HashMap<String, Set<K>>(); mDigestToFile = new LinkedHashMap<String, File>(16, 0.75f, true); for (File file : mCacheDir.listFiles()) { sLog.debug("Deleting %s.", file.getPath()); if (!file.delete()) ZimbraLog.store.warn( "unable to delete " + file.getPath() + " from uncompressed file cache"); } return this; } private class UncompressedFile { String digest; File file; UncompressedFile() {} } /** * Returns the uncompressed version of the given file. If the uncompressed file is not in the * cache, uncompresses it and adds it to the cache. * * @param key the key used to look up the uncompressed data * @param compressedFile the compressed file. This file is read, if necessary, to write the * uncompressed file. * @param sync <tt>true</tt> to use fsync */ public SharedFile get(K key, File compressedFile, boolean sync) throws IOException { File uncompressedFile = null; sLog.debug("Looking up SharedFile for key %s, path %s.", key, compressedFile.getPath()); synchronized (this) { String digest = mKeyToDigest.get(key); sLog.debug("Digest for %s is %s", key, digest); if (digest != null) { uncompressedFile = mDigestToFile.get(digest); if (uncompressedFile != null) { sLog.debug("Found existing uncompressed file. Returning new SharedFile."); return new SharedFile(uncompressedFile); } else { sLog.debug("No existing uncompressed file."); } } } // Uncompress the file outside of the synchronized block. UncompressedFile temp = uncompressToTempFile(compressedFile, sync); SharedFile shared = null; synchronized (this) { uncompressedFile = mDigestToFile.get(temp.digest); if (uncompressedFile != null) { // Another thread uncompressed the same file at the same time. sLog.debug("Found existing uncompressed file. Deleting %s.", temp.file); mapKeyToDigest(key, temp.digest); FileUtil.delete(temp.file); shared = new SharedFile(uncompressedFile); } else { uncompressedFile = new File(mCacheDir, temp.digest); sLog.debug("Renaming %s to %s.", temp.file, uncompressedFile); FileUtil.rename(temp.file, uncompressedFile); shared = new SharedFile(uncompressedFile); // Opens the file implicitly. put(key, temp.digest, uncompressedFile); } } return shared; } private UncompressedFile uncompressToTempFile(File compressedFile, boolean sync) throws IOException { // Write the uncompressed file and calculate the digest. CalculatorStream calc = new CalculatorStream(new GZIPInputStream(new FileInputStream(compressedFile))); File tempFile = File.createTempFile(UncompressedFileCache.class.getSimpleName(), null, mCacheDir); FileUtil.uncompress(calc, tempFile, sync); String digest = calc.getDigest(); sLog.debug( "Uncompressed %s to %s, digest=%s.", compressedFile.getPath(), tempFile.getPath(), digest); UncompressedFile result = new UncompressedFile(); result.file = tempFile; result.digest = digest; return result; } /** Creates a record of a new uncompressed file in the cache data structures. */ private synchronized void put(K key, String digest, File file) { long fileSize = file.length(); mapKeyToDigest(key, digest); mDigestToFile.put(digest, file); mNumBytes += fileSize; sLog.debug( "Added file: key=%s, size=%d, path=%s. Cache size=%d, numBytes=%d.", key, fileSize, file.getPath(), mDigestToFile.size(), mNumBytes); } private synchronized void mapKeyToDigest(K key, String digest) { mKeyToDigest.put(key, digest); Set<K> keys = mDigestToKeys.get(digest); if (keys == null) { keys = new HashSet<K>(); mDigestToKeys.put(digest, keys); } keys.add(key); } public synchronized void remove(K key) { String digest = mKeyToDigest.remove(key); sLog.debug("Removing %s, digest=%s", key, digest); if (digest != null) { Set<K> keys = mDigestToKeys.get(digest); if (keys != null) { keys.remove(key); } if (keys == null || keys.isEmpty()) { File file = mDigestToFile.remove(digest); mDigestToKeys.remove(digest); sLog.debug("Deleting unreferenced file %s.", file); if (file != null) { try { FileUtil.delete(file); } catch (Exception e) { // IOException and SecurityException ZimbraLog.store.warn("Unable to remove a file from the uncompressed cache.", e); } } } else { sLog.debug("Not deleting %s. It is referenced by %s.", digest, keys); } } } }