/*
* Copyright © 2015-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.gateway.handlers;
import co.cask.cdap.api.Transactional;
import co.cask.cdap.api.TxRunnable;
import co.cask.cdap.client.AuthorizationClient;
import co.cask.cdap.client.config.ClientConfig;
import co.cask.cdap.client.config.ConnectionConfig;
import co.cask.cdap.common.FeatureDisabledException;
import co.cask.cdap.common.NotFoundException;
import co.cask.cdap.common.UnauthenticatedException;
import co.cask.cdap.common.conf.CConfiguration;
import co.cask.cdap.common.conf.Constants;
import co.cask.cdap.common.entity.EntityExistenceVerifier;
import co.cask.cdap.common.http.AuthenticationChannelHandler;
import co.cask.cdap.common.http.CommonNettyHttpServiceBuilder;
import co.cask.cdap.proto.id.EntityId;
import co.cask.cdap.proto.id.Ids;
import co.cask.cdap.proto.id.NamespaceId;
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.authorization.AuthorizationContextFactory;
import co.cask.cdap.security.authorization.AuthorizerInstantiatorService;
import co.cask.cdap.security.authorization.DefaultAuthorizationContext;
import co.cask.cdap.security.authorization.NoOpAdmin;
import co.cask.cdap.security.authorization.NoOpDatasetContext;
import co.cask.cdap.security.spi.authorization.AuthorizationContext;
import co.cask.cdap.security.spi.authorization.Authorizer;
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.http.NettyHttpService;
import co.cask.tephra.TransactionFailureException;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
/**
* Tests for {@link AuthorizationHandler}.
*/
public class AuthorizationHandlerTest {
private static final String USERNAME_PROPERTY = "cdap.username";
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 final Principal admin = new Principal("admin", Principal.PrincipalType.USER);
private final Properties properties = new Properties();
private final EntityId ns1 = Ids.namespace("ns1");
private final EntityId ns2 = Ids.namespace("ns2");
private final EntityExistenceVerifier entityExistenceVerifier = new InMemoryEntityExistenceVerifier(
ImmutableSet.of(ns1, ns2)
);
private NettyHttpService service;
private AuthorizationClient client;
@Before
public void setUp() throws Exception {
CConfiguration conf = CConfiguration.create();
conf.setBoolean(Constants.Security.Authorization.ENABLED, true);
conf.setBoolean(Constants.Security.ENABLED, true);
properties.setProperty("superusers", admin.getName());
final InMemoryAuthorizer auth = new InMemoryAuthorizer();
auth.initialize(factory.create(properties));
service = new CommonNettyHttpServiceBuilder(conf)
.addHttpHandlers(ImmutableList.of(new AuthorizationHandler(
new AuthorizerInstantiatorService(conf, factory) {
@Override
public Authorizer get() {
return auth;
}
}, conf, entityExistenceVerifier)))
.modifyChannelPipeline(new Function<ChannelPipeline, ChannelPipeline>() {
@Override
public ChannelPipeline apply(ChannelPipeline input) {
input.addBefore("dispatcher", "usernamesetter", new TestUserNameSetter());
input.addAfter("usernamesetter", "authenticator", new AuthenticationChannelHandler());
return input;
}
})
.build();
service.startAndWait();
client = new AuthorizationClient(
ClientConfig.builder()
.setConnectionConfig(
ConnectionConfig.builder()
.setHostname(service.getBindAddress().getHostName())
.setPort(service.getBindAddress().getPort())
.setSSLEnabled(false)
.build())
.build());
System.setProperty(USERNAME_PROPERTY, admin.getName());
}
@After
public void tearDown() {
service.stopAndWait();
}
@Test
public void testAuthenticationDisabled() throws Exception {
CConfiguration cConf = CConfiguration.create();
cConf.setBoolean(Constants.Security.ENABLED, false);
cConf.setBoolean(Constants.Security.Authorization.ENABLED, true);
testDisabled(cConf, FeatureDisabledException.Feature.AUTHENTICATION, Constants.Security.ENABLED);
}
@Test
public void testAuthorizationDisabled() throws Exception {
CConfiguration cConf = CConfiguration.create();
cConf.setBoolean(Constants.Security.ENABLED, true);
cConf.setBoolean(Constants.Security.Authorization.ENABLED, false);
testDisabled(cConf, FeatureDisabledException.Feature.AUTHORIZATION, Constants.Security.Authorization.ENABLED);
}
private void testDisabled(CConfiguration cConf, FeatureDisabledException.Feature feature,
String configSetting) throws Exception {
NettyHttpService service = new CommonNettyHttpServiceBuilder(cConf)
.addHttpHandlers(ImmutableList.of(new AuthorizationHandler(
new AuthorizerInstantiatorService(cConf, factory) {
@Override
public Authorizer get() {
return new InMemoryAuthorizer();
}
}, cConf, entityExistenceVerifier)))
.build();
service.startAndWait();
try {
final AuthorizationClient client = new AuthorizationClient(
ClientConfig.builder()
.setConnectionConfig(
ConnectionConfig.builder()
.setHostname(service.getBindAddress().getHostName())
.setPort(service.getBindAddress().getPort())
.setSSLEnabled(false)
.build())
.build());
final NamespaceId ns1 = Ids.namespace("ns1");
final Role admins = new Role("admins");
// Test that the right exception is thrown when any Authorization REST API is called with authorization disabled
verifyFeatureDisabled(new DisabledFeatureCaller() {
@Override
public void call() throws Exception {
client.grant(ns1, admin, ImmutableSet.of(Action.READ));
}
}, feature, configSetting);
verifyFeatureDisabled(new DisabledFeatureCaller() {
@Override
public void call() throws Exception {
client.revoke(ns1, admin, ImmutableSet.of(Action.READ));
}
}, feature, configSetting);
verifyFeatureDisabled(new DisabledFeatureCaller() {
@Override
public void call() throws Exception {
client.revoke(ns1);
}
}, feature, configSetting);
verifyFeatureDisabled(new DisabledFeatureCaller() {
@Override
public void call() throws Exception {
client.listPrivileges(admin);
}
}, feature, configSetting);
verifyFeatureDisabled(new DisabledFeatureCaller() {
@Override
public void call() throws Exception {
client.addRoleToPrincipal(admins, admin);
}
}, feature, configSetting);
verifyFeatureDisabled(new DisabledFeatureCaller() {
@Override
public void call() throws Exception {
client.removeRoleFromPrincipal(admins, admin);
}
}, feature, configSetting);
verifyFeatureDisabled(new DisabledFeatureCaller() {
@Override
public void call() throws Exception {
client.createRole(admins);
}
}, feature, configSetting);
verifyFeatureDisabled(new DisabledFeatureCaller() {
@Override
public void call() throws Exception {
client.dropRole(admins);
}
}, feature, configSetting);
verifyFeatureDisabled(new DisabledFeatureCaller() {
@Override
public void call() throws Exception {
client.listAllRoles();
}
}, feature, configSetting);
} finally {
service.stopAndWait();
}
}
@Test
public void testRevokeEntityUserActions() throws Exception {
// grant() and revoke(EntityId, String, Set<Action>)
verifyAuthFailure(ns1, admin, Action.READ);
client.grant(ns1, admin, ImmutableSet.of(Action.READ));
verifyAuthSuccess(ns1, admin, Action.READ);
client.revoke(ns1, admin, ImmutableSet.of(Action.READ));
verifyAuthFailure(ns1, admin, Action.READ);
}
@Test
public void testRevokeEntityUser() throws Exception {
Principal adminGroup = new Principal("admin", Principal.PrincipalType.GROUP);
Principal bob = new Principal("bob", Principal.PrincipalType.USER);
// grant() and revoke(EntityId, String)
client.grant(ns1, adminGroup, ImmutableSet.of(Action.READ));
client.grant(ns1, bob, ImmutableSet.of(Action.READ));
verifyAuthSuccess(ns1, adminGroup, Action.READ);
verifyAuthSuccess(ns1, bob, Action.READ);
client.revoke(ns1, adminGroup, EnumSet.allOf(Action.class));
verifyAuthFailure(ns1, adminGroup, Action.READ);
verifyAuthSuccess(ns1, bob, Action.READ);
}
@Test
public void testRevokeEntity() throws Exception {
Principal adminGroup = new Principal("admin", Principal.PrincipalType.GROUP);
Principal bob = new Principal("bob", Principal.PrincipalType.USER);
// grant() and revoke(EntityId)
client.grant(ns1, adminGroup, ImmutableSet.of(Action.READ));
client.grant(ns1, bob, ImmutableSet.of(Action.READ));
client.grant(ns2, adminGroup, ImmutableSet.of(Action.READ));
verifyAuthSuccess(ns1, adminGroup, Action.READ);
verifyAuthSuccess(ns1, bob, Action.READ);
verifyAuthSuccess(ns2, adminGroup, Action.READ);
client.revoke(ns1);
verifyAuthFailure(ns1, adminGroup, Action.READ);
verifyAuthFailure(ns1, bob, Action.READ);
verifyAuthSuccess(ns2, adminGroup, Action.READ);
}
@Test
public void testRBAC() throws Exception {
Role admins = new Role("admins");
Role engineers = new Role("engineers");
// create a role
client.createRole(admins);
// add another role
client.createRole(engineers);
// listing role should show the added role
Set<Role> roles = client.listAllRoles();
Assert.assertEquals(Sets.newHashSet(admins, engineers), roles);
// creating a role which already exists should throw an exception
try {
client.createRole(admins);
Assert.fail(String.format("Created a role %s which already exists. Should have failed.", admins.getName()));
} catch (RoleAlreadyExistsException expected) {
// expected
}
// drop an existing role
client.dropRole(admins);
// the list should not have the dropped role
roles = client.listAllRoles();
Assert.assertEquals(Sets.newHashSet(engineers), roles);
// dropping a non-existing role should throw exception
try {
client.dropRole(admins);
Assert.fail(String.format("Dropped a role %s which does not exists. Should have failed.", admins.getName()));
} catch (RoleNotFoundException expected) {
// expected
}
// add an user to an existing role
Principal spiderman = new Principal("spiderman", Principal.PrincipalType.USER);
client.addRoleToPrincipal(engineers, spiderman);
// add an user to an non-existing role should throw an exception
try {
client.addRoleToPrincipal(admins, spiderman);
Assert.fail(String.format("Added role %s to principal %s. Should have failed.", admins, spiderman));
} catch (RoleNotFoundException expected) {
// expected
}
// check listing roles for spiderman have engineers role
Assert.assertEquals(Sets.newHashSet(engineers), client.listRoles(spiderman));
// check that spiderman who has engineers roles cannot read from ns1
verifyAuthFailure(ns1, spiderman, Action.READ);
// give a permission to engineers role
client.grant(ns1, engineers, ImmutableSet.of(Action.READ));
// check that a spiderman who has engineers role has access
verifyAuthSuccess(ns1, spiderman, Action.READ);
// list privileges for spiderman should have read action on ns1
Assert.assertEquals(Sets.newHashSet(new Privilege(ns1, Action.READ)), client.listPrivileges(spiderman));
// revoke action from the role
client.revoke(ns1, engineers, ImmutableSet.of(Action.READ));
// now the privileges for spiderman should be empty
Assert.assertEquals(new HashSet<>(), client.listPrivileges(spiderman));
// check that the user of this role is not authorized to do the revoked operation
verifyAuthFailure(ns1, spiderman, Action.READ);
// remove an user from a existing role
client.removeRoleFromPrincipal(engineers, spiderman);
// check listing roles for spiderman should be empty
Assert.assertEquals(new HashSet<>(), client.listRoles(spiderman));
// remove an user from a non-existing role should throw exception
try {
client.removeRoleFromPrincipal(admins, spiderman);
Assert.fail(String.format("Removed non-existing role %s from principal %s. Should have failed.", admins,
spiderman));
} catch (RoleNotFoundException expected) {
// expected
}
}
@Test
public void testAuthorizationForPrivileges() throws Exception {
Principal bob = new Principal("bob", Principal.PrincipalType.USER);
Principal alice = new Principal("alice", Principal.PrincipalType.USER);
// olduser has been set as admin in the beginning of this test. admin has been configured as a superuser.
String oldUser = getCurrentUser();
setCurrentUser(alice.getName());
try {
try {
client.grant(ns1, bob, ImmutableSet.of(Action.ALL));
Assert.fail(String.format("alice should not be able to grant privileges to bob on namespace %s because she " +
"does not have admin privileges on the namespace.", ns1));
} catch (UnauthorizedException expected) {
// expected
}
setCurrentUser(oldUser);
// admin should be able to grant since he is a super user
client.grant(ns1, alice, ImmutableSet.of(Action.ADMIN));
// now alice should be able to grant privileges on ns since she has ADMIN privileges
setCurrentUser(alice.getName());
client.grant(ns1, bob, ImmutableSet.of(Action.ALL));
// revoke alice's permissions as admin
setCurrentUser(oldUser);
client.revoke(ns1);
// revoking bob's privileges as alice should fail
setCurrentUser(alice.getName());
try {
client.revoke(ns1, bob, ImmutableSet.of(Action.ALL));
Assert.fail(String.format("alice should not be able to revoke bob's privileges on namespace %s because she " +
"does not have admin privileges on the namespace.", ns1));
} catch (UnauthorizedException expected) {
// expected
}
// grant alice privileges as admin again
setCurrentUser(oldUser);
client.grant(ns1, alice, ImmutableSet.of(Action.ALL));
// Now alice should be able to revoke bob's privileges
setCurrentUser(alice.getName());
client.revoke(ns1, bob, ImmutableSet.of(Action.ALL));
} finally {
setCurrentUser(oldUser);
}
}
@Test(expected = NotFoundException.class)
public void testGrantOnNonExistingEntity()
throws FeatureDisabledException, UnauthenticatedException, UnauthorizedException, IOException, NotFoundException {
client.grant(Ids.namespace("ns3"), admin, ImmutableSet.of(Action.ADMIN));
}
@Test(expected = NotFoundException.class)
public void testRevokeOnNonExistingEntity()
throws FeatureDisabledException, UnauthenticatedException, UnauthorizedException, IOException, NotFoundException {
client.revoke(Ids.namespace("ns3"), admin, ImmutableSet.of(Action.ADMIN));
}
@Test(expected = NotFoundException.class)
public void testRevokeAllOnNonExistingEntity()
throws FeatureDisabledException, UnauthenticatedException, UnauthorizedException, IOException, NotFoundException {
client.revoke(Ids.namespace("ns3"));
}
/**
* Interface to centralize testing the exception thrown when Authorization is disabled.
*/
private interface DisabledFeatureCaller {
void call() throws Exception;
}
/**
* Calls a {@link DisabledFeatureCaller} and verifies that the right exception was thrown.
*
* @param caller the {@link DisabledFeatureCaller} that wraps the operation to test
* @param expectedDisabledFeature the disabled feature
* @param expectedEnableConfig the expected config setting to enable the disabled feature
*/
private void verifyFeatureDisabled(DisabledFeatureCaller caller,
FeatureDisabledException.Feature expectedDisabledFeature,
String expectedEnableConfig) throws Exception {
try {
caller.call();
} catch (FeatureDisabledException expected) {
Assert.assertEquals(expectedDisabledFeature, expected.getFeature());
Assert.assertEquals(FeatureDisabledException.CDAP_SITE, expected.getConfigFile());
Assert.assertEquals(expectedEnableConfig, expected.getEnableConfigKey());
Assert.assertEquals("true", expected.getEnableConfigValue());
}
}
private void verifyAuthSuccess(EntityId entity, Principal principal, Action action) throws Exception {
Set<Privilege> privileges = client.listPrivileges(principal);
Privilege privilegeToCheck = new Privilege(entity, action);
Assert.assertTrue(
String.format(
"Expected principal %s to have the privilege %s, but found that it did not.", principal, privilegeToCheck
),
privileges.contains(privilegeToCheck)
);
}
private void verifyAuthFailure(EntityId entity, Principal principal, Action action) throws Exception {
Set<Privilege> privileges = client.listPrivileges(principal);
Privilege privilegeToCheck = new Privilege(entity, action);
Assert.assertFalse(
String.format(
"Expected principal %s to not have the privilege %s, but found that it did.", principal, privilegeToCheck
),
privileges.contains(privilegeToCheck)
);
}
private static void setCurrentUser(String username) {
System.setProperty(USERNAME_PROPERTY, username);
}
private static String getCurrentUser() {
return System.getProperty(USERNAME_PROPERTY);
}
/**
* Test {@link SimpleChannelHandler} to set the username as an HTTP Header
* {@link co.cask.cdap.common.conf.Constants.Security.Headers#USER_ID}. In production, this is done in the router by
* SecurityAuthenticationHandler
*/
private static final class TestUserNameSetter extends SimpleChannelHandler {
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
Object msg = e.getMessage();
if (!(msg instanceof HttpRequest)) {
return;
}
HttpRequest request = (HttpRequest) msg;
request.setHeader(Constants.Security.Headers.USER_ID, System.getProperty(USERNAME_PROPERTY));
super.messageReceived(ctx, e);
}
}
}