/*
* 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.api.Transactional;
import co.cask.cdap.api.TxRunnable;
import co.cask.cdap.common.FeatureDisabledException;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.internal.test.AppJarHelper;
import co.cask.cdap.proto.id.EntityId;
import co.cask.cdap.proto.security.Action;
import co.cask.cdap.proto.security.Principal;
import co.cask.cdap.proto.security.Privilege;
import co.cask.cdap.proto.security.Role;
import co.cask.cdap.security.spi.authorization.AbstractAuthorizer;
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 co.cask.cdap.security.spi.authorization.RoleAlreadyExistsException;
import co.cask.cdap.security.spi.authorization.RoleNotFoundException;
import co.cask.cdap.security.spi.authorization.UnauthorizedException;
import co.cask.tephra.TransactionFailureException;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.gson.Gson;
import org.apache.twill.filesystem.LocalLocationFactory;
import org.apache.twill.filesystem.Location;
import org.apache.twill.filesystem.LocationFactory;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Properties;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import javax.annotation.Nullable;
/**
* Tests for {@link AuthorizerInstantiatorService}.
*/
public class AuthorizerInstantiatorServiceTest {
@ClassRule
public static final TemporaryFolder TEMPORARY_FOLDER = new TemporaryFolder();
private static final CConfiguration cConf = CConfiguration.create();
private static final AuthorizationContextFactory factory = new AuthorizationContextFactory() {
@Override
public AuthorizationContext create(Properties extensionProperties) {
Transactional txnl = new Transactional() {
@Override
public void execute(TxRunnable runnable) throws TransactionFailureException {
//no-op
}
};
return new DefaultAuthorizationContext(extensionProperties, new NoOpDatasetContext(), new NoOpAdmin(), txnl);
}
};
private static LocationFactory locationFactory;
@BeforeClass
public static void setup() throws IOException {
cConf.set(Constants.CFG_LOCAL_DATA_DIR, TEMPORARY_FOLDER.newFolder().getAbsolutePath());
cConf.setBoolean(Constants.Security.ENABLED, true);
cConf.setBoolean(Constants.Security.Authorization.ENABLED, true);
locationFactory = new LocalLocationFactory(TEMPORARY_FOLDER.newFolder());
}
@Test
public void testAuthenticationDisabled() throws IOException {
CConfiguration cConf = CConfiguration.create();
cConf.set(Constants.CFG_LOCAL_DATA_DIR, TEMPORARY_FOLDER.newFolder().getAbsolutePath());
cConf.setBoolean(Constants.Security.Authorization.ENABLED, true);
assertDisabled(cConf, FeatureDisabledException.Feature.AUTHENTICATION);
}
@Test
public void testAuthorizationDisabled() throws IOException {
CConfiguration cConf = CConfiguration.create();
cConf.setBoolean(Constants.Security.ENABLED, true);
cConf.set(Constants.CFG_LOCAL_DATA_DIR, TEMPORARY_FOLDER.newFolder().getAbsolutePath());
assertDisabled(cConf, FeatureDisabledException.Feature.AUTHORIZATION);
}
private void assertDisabled(CConfiguration cConf, FeatureDisabledException.Feature feature) {
AuthorizerInstantiatorService instantiator = new AuthorizerInstantiatorService(cConf, factory);
instantiator.startAndWait();
try {
Authorizer authorizer = instantiator.get();
Assert.assertTrue(
String.format("When %s is disabled, a %s must be returned, but got %s.",
feature.name().toLowerCase(), NoOpAuthorizer.class.getSimpleName(),
authorizer.getClass().getName()),
authorizer instanceof NoOpAuthorizer
);
} finally {
instantiator.stopAndWait();
}
}
@Test(expected = InvalidAuthorizerException.class)
public void testNonExistingAuthorizerJarPath() throws Throwable {
cConf.set(Constants.Security.Authorization.EXTENSION_JAR_PATH, "/path/to/external-test-authorizer.jar");
AuthorizerInstantiatorService instantiator = new AuthorizerInstantiatorService(cConf, factory);
try {
instantiator.startAndWait();
} catch (UncheckedExecutionException e) {
throw Throwables.getRootCause(e);
}
Assert.assertFalse("Authorizer Instantiator should not have started because extension jar does not exist",
instantiator.isRunning());
}
@Test(expected = InvalidAuthorizerException.class)
public void testAuthorizerJarPathIsDirectory() throws Throwable {
cConf.set(Constants.Security.Authorization.EXTENSION_JAR_PATH, TEMPORARY_FOLDER.newFolder().getPath());
AuthorizerInstantiatorService instantiator = new AuthorizerInstantiatorService(cConf, factory);
try {
instantiator.startAndWait();
} catch (UncheckedExecutionException e) {
throw Throwables.getRootCause(e);
}
Assert.assertFalse("Authorizer Instantiator should not have started because extension jar is a directory",
instantiator.isRunning());
}
@Test(expected = InvalidAuthorizerException.class)
public void testAuthorizerJarPathIsNotJar() throws Throwable {
cConf.set(Constants.Security.Authorization.EXTENSION_JAR_PATH, TEMPORARY_FOLDER.newFile("abc.txt").getPath());
AuthorizerInstantiatorService instantiator = new AuthorizerInstantiatorService(cConf, factory);
try {
instantiator.startAndWait();
} catch (UncheckedExecutionException e) {
throw Throwables.getRootCause(e);
}
Assert.assertFalse("Authorizer Instantiator should not have started because extension jar is not a jar file",
instantiator.isRunning());
}
@Test(expected = InvalidAuthorizerException.class)
public void testMissingManifest() throws Throwable {
Location externalAuthJar = createInvalidExternalAuthJar(null);
cConf.set(Constants.Security.Authorization.EXTENSION_JAR_PATH, externalAuthJar.toString());
AuthorizerInstantiatorService instantiator = new AuthorizerInstantiatorService(cConf, factory);
try {
instantiator.startAndWait();
} catch (UncheckedExecutionException e) {
throw Throwables.getRootCause(e);
}
Assert.assertFalse("Authorizer Instantiator should not have started because extension jar does not have a manifest",
instantiator.isRunning());
}
@Test(expected = InvalidAuthorizerException.class)
public void testMissingAuthorizerClassName() throws Throwable {
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
Location externalAuthJar = createInvalidExternalAuthJar(manifest);
cConf.set(Constants.Security.Authorization.EXTENSION_JAR_PATH, externalAuthJar.toString());
AuthorizerInstantiatorService instantiator = new AuthorizerInstantiatorService(cConf, factory);
try {
instantiator.startAndWait();
} catch (UncheckedExecutionException e) {
throw Throwables.getRootCause(e);
}
Assert.assertFalse("Authorizer Instantiator should not have started because extension jar's manifest does not " +
"define Authorizer class.", instantiator.isRunning());
}
@Test(expected = InvalidAuthorizerException.class)
public void testDoesNotImplementAuthorizer() throws Throwable {
Manifest manifest = new Manifest();
Attributes mainAttributes = manifest.getMainAttributes();
mainAttributes.put(Attributes.Name.MAIN_CLASS, DoesNotImplementAuthorizer.class.getName());
Location externalAuthJar = AppJarHelper.createDeploymentJar(locationFactory, DoesNotImplementAuthorizer.class,
manifest);
cConf.set(Constants.Security.Authorization.EXTENSION_JAR_PATH, externalAuthJar.toString());
AuthorizerInstantiatorService instantiator = new AuthorizerInstantiatorService(cConf, factory);
try {
instantiator.startAndWait();
} catch (UncheckedExecutionException e) {
throw Throwables.getRootCause(e);
}
Assert.assertFalse("Authorizer Instantiator should not have started because the Authorizer class defined in " +
"the extension jar's manifest does not implement " + Authorizer.class.getName(),
instantiator.isRunning());
}
@Test(expected = InvalidAuthorizerException.class)
public void testInitializationThrowsException() throws Throwable {
Manifest manifest = new Manifest();
Attributes mainAttributes = manifest.getMainAttributes();
mainAttributes.put(Attributes.Name.MAIN_CLASS, ExceptionInInitialize.class.getName());
Location externalAuthJar = AppJarHelper.createDeploymentJar(locationFactory, ExceptionInInitialize.class,
manifest);
cConf.set(Constants.Security.Authorization.EXTENSION_JAR_PATH, externalAuthJar.toString());
AuthorizerInstantiatorService instantiator = new AuthorizerInstantiatorService(cConf, factory);
try {
instantiator.startAndWait();
} catch (UncheckedExecutionException e) {
throw e.getCause();
}
Assert.assertFalse("Authorizer Instantiator should not have started because the Authorizer class defined in " +
"the extension jar's manifest does not implement " + Authorizer.class.getName(),
instantiator.isRunning());
}
@Test
public void testAuthorizerExtension() throws IOException, ClassNotFoundException {
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, ValidExternalAuthorizer.class.getName());
Location externalAuthJar = AppJarHelper.createDeploymentJar(locationFactory, ValidExternalAuthorizer.class,
manifest);
CConfiguration cConfCopy = CConfiguration.copy(cConf);
cConfCopy.set(Constants.Security.Authorization.EXTENSION_JAR_PATH, externalAuthJar.toString());
cConfCopy.set(Constants.Security.Authorization.EXTENSION_CONFIG_PREFIX + "config.path",
"/path/config.ini");
cConfCopy.set(Constants.Security.Authorization.EXTENSION_CONFIG_PREFIX + "service.address",
"http://foo.bar.co:5555");
cConfCopy.set("foo." + Constants.Security.Authorization.EXTENSION_CONFIG_PREFIX + "dont.include",
"not.prefix.should.not.be.included");
AuthorizerInstantiatorService instantiator = new AuthorizerInstantiatorService(cConfCopy, factory);
try {
instantiator.startAndWait();
Assert.assertTrue(instantiator.isRunning());
// should be able to load the ExternalAuthorizer class via the AuthorizerInstantiatorService
Authorizer externalAuthorizer1 = instantiator.get();
Assert.assertNotNull(externalAuthorizer1);
Authorizer externalAuthorizer2 = instantiator.get();
Assert.assertNotNull(externalAuthorizer2);
// verify that get returns the same instance each time it is called.
Assert.assertEquals(externalAuthorizer1, externalAuthorizer2);
ClassLoader authorizerClassLoader = externalAuthorizer1.getClass().getClassLoader();
ClassLoader parent = authorizerClassLoader.getParent();
// should be able to load the Authorizer interface via the parent
parent.loadClass(Authorizer.class.getName());
// should not be able to load the ExternalAuthorizer class via the parent class loader
try {
parent.loadClass(ValidExternalAuthorizer.class.getName());
Assert.fail("Should not be able to load external authorizer classes via the parent classloader of the " +
"Authorizer class loader.");
} catch (ClassNotFoundException expected) {
// expected
}
// should be able to load the ExternalAuthorizer class via the AuthorizerClassLoader
authorizerClassLoader.loadClass(ValidExternalAuthorizer.class.getName());
// have to do this because the external authorizer instance is created in a new classloader, so casting will
// not work.
Gson gson = new Gson();
ValidExternalAuthorizer validAuthorizer = gson.fromJson(gson.toJson(externalAuthorizer1),
ValidExternalAuthorizer.class);
Properties expectedProps = new Properties();
expectedProps.put("config.path", "/path/config.ini");
expectedProps.put("service.address", "http://foo.bar.co:5555");
Properties actualProps = validAuthorizer.getProperties();
Assert.assertEquals(expectedProps, actualProps);
} finally {
instantiator.stopAndWait();
}
}
private Location createInvalidExternalAuthJar(@Nullable Manifest manifest) throws IOException {
String jarName = "external-authorizer";
Location externalAuthJar = locationFactory.create(jarName).getTempFile(".jar");
try (
OutputStream out = externalAuthJar.getOutputStream();
JarOutputStream jarOutput = manifest == null ? new JarOutputStream(out) : new JarOutputStream(out, manifest)
) {
JarEntry entry = new JarEntry("dummy.class");
jarOutput.putNextEntry(entry);
jarOutput.closeEntry();
}
return externalAuthJar;
}
public static class NoOpAbstractAuthorizer extends AbstractAuthorizer {
@Override
public void grant(EntityId entity, Principal principal, Set<Action> actions) {
// no-op
}
@Override
public void revoke(EntityId entity, Principal principal, Set<Action> actions) {
// no-op
}
@Override
public void revoke(EntityId entity) {
// no-op
}
@Override
public Set<Privilege> listPrivileges(Principal principal) {
return ImmutableSet.of();
}
@Override
public void createRole(Role role) throws RoleAlreadyExistsException {
// no-op
}
@Override
public void dropRole(Role role) throws RoleNotFoundException {
// no-op
}
@Override
public void addRoleToPrincipal(Role role, Principal principal) {
// no-op
}
@Override
public void removeRoleFromPrincipal(Role role, Principal principal) {
// no-op
}
@Override
public Set<Role> listRoles(Principal principal) {
return ImmutableSet.of();
}
@Override
public Set<Role> listAllRoles() {
return ImmutableSet.of();
}
@Override
public void enforce(EntityId entity, Principal principal, Action action) throws UnauthorizedException {
// no-op
}
}
public static final class ExceptionInInitialize extends NoOpAbstractAuthorizer {
@Override
public void initialize(AuthorizationContext context) throws Exception {
throw new IllegalStateException("Testing exception during initialize");
}
}
public static final class ValidExternalAuthorizer extends NoOpAbstractAuthorizer {
private Properties properties;
@Override
public void initialize(AuthorizationContext context) throws Exception {
this.properties = context.getExtensionProperties();
}
public Properties getProperties() {
return properties;
}
}
private static final class DoesNotImplementAuthorizer {
}
}