/*
* Copyright © 2016 Cask Data, 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 co.cask.cdap.security.authorization;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.io.Locations;
import co.cask.cdap.common.lang.ClassLoaders;
import co.cask.cdap.common.lang.InstantiatorFactory;
import co.cask.cdap.common.lang.jar.BundleJarUtil;
import co.cask.cdap.common.utils.DirUtils;
import co.cask.cdap.security.spi.authorization.AuthorizationContext;
import co.cask.cdap.security.spi.authorization.Authorizer;
import co.cask.cdap.security.spi.authorization.NoOpAuthorizer;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.base.Supplier;
import com.google.common.reflect.TypeToken;
import com.google.common.util.concurrent.AbstractIdleService;
import com.google.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.Properties;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipException;
/**
* Class to instantiate {@link Authorizer} extensions. Authorization extensions are instantiated using a
* separate {@link ClassLoader} that is built using a bundled jar for the {@link Authorizer} extension that
* contains all its required dependencies. The {@link ClassLoader} is created with the parent as the classloader with
* which the {@link Authorizer} interface is instantiated. This parent only has classes required by the
* {@code cdap-security-spi} module.
*
* It is implemented as a {@link AbstractIdleService} so it can manage initialization and destruction of the
* {@link AuthorizerClassLoader} in a reliable manner.
*
* The {@link AuthorizerInstantiatorService} has the following expectations from the extension:
* <ul>
* <li>Authorization is enabled setting the parameter {@link Constants.Security.Authorization#ENABLED} to true in
* {@code cdap-site.xml}. When authorization is disabled, an instance of {@link NoOpAuthorizer} is returned.</li>
* <li>The path to the extension jar bundled with all its dependencies is read from the setting
* {@link Constants.Security.Authorization#EXTENSION_JAR_PATH} in cdap-site.xml</li>
* <li>The instantiator reads a fully qualified class name specified as the {@link Attributes.Name#MAIN_CLASS}
* attribute in the extension jar's manifest file. This class must implement {@link Authorizer} and have a default
* constructor.</li>
* <li>During {@link AbstractIdleService#startUp()}, the service creates an instance of of the {@link Authorizer}
* class and also calls its {@link Authorizer#initialize(AuthorizationContext)} method with an
* {@link AuthorizationContext} created using a {@link AuthorizationContextFactory} by providing it a
* {@link Properties} object that is populated with all configuration settings from {@code cdap-site.xml} that have
* keys with the prefix {@link Constants.Security.Authorization#EXTENSION_CONFIG_PREFIX}.</li>
* <li>During {@link AbstractIdleService#shutDown()}, the {@link Authorizer#destroy()} method is invoked, and the
* {@link AuthorizerClassLoader} is closed.</li>
* </ul>
*/
public class AuthorizerInstantiatorService extends AbstractIdleService implements Supplier<Authorizer> {
private static final Logger LOG = LoggerFactory.getLogger(AuthorizerInstantiatorService.class);
private final CConfiguration cConf;
private final boolean authenticationEnabled;
private final boolean authorizationEnabled;
private final InstantiatorFactory instantiatorFactory;
private final AuthorizationContextFactory authorizationContextFactory;
private File tmpDir;
private AuthorizerClassLoader authorizerClassLoader;
private Authorizer authorizer;
@Inject
@VisibleForTesting
public AuthorizerInstantiatorService(CConfiguration cConf, AuthorizationContextFactory authorizationContextFactory) {
this.cConf = cConf;
this.authenticationEnabled = cConf.getBoolean(Constants.Security.ENABLED);
this.authorizationEnabled = cConf.getBoolean(Constants.Security.Authorization.ENABLED);
this.instantiatorFactory = new InstantiatorFactory(false);
this.authorizationContextFactory = authorizationContextFactory;
}
@Override
protected void startUp() throws Exception {
if (!authorizationEnabled) {
LOG.debug("Authorization is disabled. Using a no-op authorizer.");
this.authorizer = new NoOpAuthorizer();
return;
}
if (!authenticationEnabled) {
LOG.debug("Authorization is enabled. However, authentication is disabled. Using a no-op authorizer.");
this.authorizer = new NoOpAuthorizer();
return;
}
// Authorization is enabled, so continue with startup now
String authorizerExtensionJarPath = cConf.get(Constants.Security.Authorization.EXTENSION_JAR_PATH);
if (Strings.isNullOrEmpty(authorizerExtensionJarPath)) {
throw new IllegalArgumentException(
String.format("Authorizer extension jar path not found in configuration. Please set %s in cdap-site.xml to " +
"the fully qualified path of the jar file to use as the authorization backend.",
Constants.Security.Authorization.EXTENSION_JAR_PATH));
}
File authorizerExtensionJar = new File(authorizerExtensionJarPath);
ensureValidAuthExtensionJar(authorizerExtensionJar);
File tmpDir = new File(cConf.get(Constants.CFG_LOCAL_DATA_DIR),
cConf.get(Constants.AppFabric.TEMP_DIR)).getAbsoluteFile();
this.tmpDir = DirUtils.createTempDir(tmpDir);
this.authorizerClassLoader = createAuthorizerClassLoader(authorizerExtensionJar);
this.authorizer = createAndInitializeAuthorizerInstance(authorizerExtensionJar);
}
@Override
protected void shutDown() throws Exception {
authorizer.destroy();
if (!authorizationEnabled || !authenticationEnabled) {
// nothing to close, since we would not have created a class loader
return;
}
try {
authorizerClassLoader.close();
} catch (IOException e) {
LOG.warn("Failed to close authorizer class loader", e);
}
try {
DirUtils.deleteDirectoryContents(tmpDir);
} catch (IOException e) {
// It's a cleanup step. Nothing much can be done if cleanup fails.
LOG.warn("Failed to delete directory {}", tmpDir, e);
}
}
/**
* Returns an instance of the configured {@link Authorizer} extension, or of {@link NoOpAuthorizer}, if
* authorization is disabled.
*/
@Override
public Authorizer get() {
Preconditions.checkState(isRunning(), "Authorization Service has not yet started. Authorizer not available.");
return authorizer;
}
/**
* Creates a new instance of the configured {@link Authorizer} extension, based on the provided extension jar
* file.
*
* @return a new instance of the configured {@link Authorizer} extension
*/
private Authorizer createAndInitializeAuthorizerInstance(File authorizerExtensionJar)
throws IOException, InvalidAuthorizerException {
Class<? extends Authorizer> authorizerClass = loadAuthorizerClass(authorizerExtensionJar);
// Set the context class loader to the AuthorizerClassLoader before creating a new instance of the extension,
// so all classes required in this process are created from the AuthorizerClassLoader.
ClassLoader oldClassLoader = ClassLoaders.setContextClassLoader(authorizerClassLoader);
LOG.debug("Setting context classloader to {}. Old classloader was {}.", authorizerClassLoader, oldClassLoader);
try {
Authorizer authorizer;
try {
authorizer = instantiatorFactory.get(TypeToken.of(authorizerClass)).create();
} catch (Exception e) {
throw new InvalidAuthorizerException(
String.format("Error while instantiating for authorizer extension %s. Please make sure that the extension " +
"is a public class with a default constructor.", authorizerClass.getName()), e);
}
AuthorizationContext context = authorizationContextFactory.create(createExtensionProperties());
try {
authorizer.initialize(context);
} catch (Exception e) {
throw new InvalidAuthorizerException(
String.format("Error while initializing authorizer extension %s.", authorizerClass.getName()), e);
}
return authorizer;
} finally {
// After the process of creation of a new instance has completed (success or failure), reset the context
// classloader back to the original class loader.
ClassLoaders.setContextClassLoader(oldClassLoader);
LOG.debug("Resetting context classloader to {} from {}.", oldClassLoader, authorizerClassLoader);
}
}
private Properties createExtensionProperties() {
Properties extensionProperties = new Properties();
for (Map.Entry<String, String> cConfEntry : cConf) {
if (cConfEntry.getKey().startsWith(Constants.Security.Authorization.EXTENSION_CONFIG_PREFIX)) {
extensionProperties.put(
cConfEntry.getKey().substring(Constants.Security.Authorization.EXTENSION_CONFIG_PREFIX.length()),
cConfEntry.getValue()
);
}
}
return extensionProperties;
}
private AuthorizerClassLoader createAuthorizerClassLoader(File authorizerExtensionJar)
throws IOException, InvalidAuthorizerException {
LOG.info("Creating authorization extension using jar {}.", authorizerExtensionJar);
try {
BundleJarUtil.unJar(Locations.toLocation(authorizerExtensionJar), tmpDir);
return new AuthorizerClassLoader(tmpDir);
} catch (ZipException e) {
throw new InvalidAuthorizerException(
String.format("Authorization extension jar %s specified as %s must be a jar file.", authorizerExtensionJar,
Constants.Security.Authorization.EXTENSION_JAR_PATH), e
);
}
}
@SuppressWarnings("unchecked")
private Class<? extends Authorizer> loadAuthorizerClass(File authorizerExtensionJar)
throws IOException, InvalidAuthorizerException {
String authorizerClassName = getAuthorizerClassName(authorizerExtensionJar);
Class<?> authorizerClass;
try {
authorizerClass = authorizerClassLoader.loadClass(authorizerClassName);
} catch (ClassNotFoundException e) {
throw new InvalidAuthorizerException(
String.format("Authorizer extension class %s not found. Please make sure that the right class is specified " +
"in the extension jar's manifest located at %s.",
authorizerClassName, authorizerExtensionJar), e);
}
if (!Authorizer.class.isAssignableFrom(authorizerClass)) {
throw new InvalidAuthorizerException(
String.format("Class %s defined as %s in the authorization extension's manifest at %s must implement %s",
authorizerClass.getName(), Attributes.Name.MAIN_CLASS, authorizerExtensionJar,
Authorizer.class.getName()));
}
return (Class<? extends Authorizer>) authorizerClass;
}
/**
* Inspect the given auth extension jar to find the {@link Authorizer} class contained in it.
*
* @param authorizerExtensionJar the bundled jar file for the authorizer extension
* @return name of the class defined as the {@link Attributes.Name#MAIN_CLASS} in the authorizer extension jar
* @throws IOException if there was an exception opening the jar file
*/
private String getAuthorizerClassName(File authorizerExtensionJar) throws IOException, InvalidAuthorizerException {
File manifestFile = new File(tmpDir, JarFile.MANIFEST_NAME);
if (!manifestFile.isFile() && !manifestFile.exists()) {
throw new InvalidAuthorizerException(
String.format("No Manifest found in authorizer extension jar '%s'.", authorizerExtensionJar));
}
try (InputStream is = new FileInputStream(manifestFile)) {
Manifest manifest = new Manifest(is);
Attributes manifestAttributes = manifest.getMainAttributes();
if (manifestAttributes == null) {
throw new InvalidAuthorizerException(
String.format("No attributes found in authorizer extension jar '%s'.", authorizerExtensionJar));
}
if (!manifestAttributes.containsKey(Attributes.Name.MAIN_CLASS)) {
throw new InvalidAuthorizerException(
String.format("Authorizer class not set in the manifest of the authorizer extension jar located at %s. " +
"Please set the attribute %s to the fully qualified class name of the class that " +
"implements %s in the extension jar's manifest.",
authorizerExtensionJar, Attributes.Name.MAIN_CLASS, Authorizer.class.getName()));
}
return manifestAttributes.getValue(Attributes.Name.MAIN_CLASS);
}
}
private void ensureValidAuthExtensionJar(File authorizerExtensionJar) throws InvalidAuthorizerException {
if (!authorizerExtensionJar.exists()) {
throw new InvalidAuthorizerException(
String.format("Authorization extension jar %s specified as %s does not exist.", authorizerExtensionJar,
Constants.Security.Authorization.EXTENSION_JAR_PATH)
);
}
if (!authorizerExtensionJar.isFile()) {
throw new InvalidAuthorizerException(
String.format("Authorization extension jar %s specified as %s must be a file.", authorizerExtensionJar,
Constants.Security.Authorization.EXTENSION_JAR_PATH)
);
}
}
}