/* * The Alluxio Open Foundation licenses this work under the Apache License, version 2.0 * (the "License"). You may not use this work except in compliance with the License, which is * available at www.apache.org/licenses/LICENSE-2.0 * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, * either express or implied, as more fully set forth in the License. * * See the NOTICE file distributed with this work for information regarding copyright ownership. */ package alluxio.master.file; import alluxio.AlluxioURI; import alluxio.Configuration; import alluxio.ConfigurationTestUtils; import alluxio.Constants; import alluxio.PropertyKey; import alluxio.exception.AccessControlException; import alluxio.exception.ExceptionMessage; import alluxio.exception.InvalidPathException; import alluxio.master.MasterRegistry; import alluxio.master.block.BlockMaster; import alluxio.master.block.BlockMasterFactory; import alluxio.master.file.meta.Inode; import alluxio.master.file.meta.InodeDirectoryIdGenerator; import alluxio.master.file.meta.InodeFile; import alluxio.master.file.meta.InodeTree; import alluxio.master.file.meta.LockedInodePath; import alluxio.master.file.meta.MountTable; import alluxio.master.file.options.CreateFileOptions; import alluxio.master.journal.Journal; import alluxio.master.journal.JournalFactory; import alluxio.master.journal.NoopJournalContext; import alluxio.security.GroupMappingServiceTestUtils; import alluxio.security.authentication.AuthType; import alluxio.security.authentication.AuthenticatedClientUser; import alluxio.security.authorization.Mode; import alluxio.security.group.GroupMappingService; import alluxio.underfs.UfsManager; import com.google.common.collect.Lists; import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import org.mockito.Mockito; import java.io.IOException; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.List; /** * Unit tests for {@link PermissionChecker}. */ public final class PermissionCheckerTest { private static final String TEST_SUPER_GROUP = "test-supergroup"; /* * The user and group mappings for testing are: * admin -> admin * user1 -> group1 * user2 -> group2 * user3 -> group1 * user4 -> group2,test-supergroup */ private static final TestUser TEST_USER_ADMIN = new TestUser("admin", "admin"); private static final TestUser TEST_USER_1 = new TestUser("user1", "group1"); private static final TestUser TEST_USER_2 = new TestUser("user2", "group2"); private static final TestUser TEST_USER_3 = new TestUser("user3", "group1"); private static final TestUser TEST_USER_SUPERGROUP = new TestUser("user4", "group2,test-supergroup"); /* * The file structure for testing is: * / admin admin 755 * /testDir user1 group1 755 * /testDir/file user1 group1 644 * /testFile user2 group2 644 * /testWeirdFile user1 group1 046 */ private static final String TEST_DIR_URI = "/testDir"; private static final String TEST_DIR_FILE_URI = "/testDir/file"; private static final String TEST_FILE_URI = "/testFile"; private static final String TEST_NOT_EXIST_URI = "/testDir/notExistDir/notExistFile"; private static final String TEST_WEIRD_FILE_URI = "/testWeirdFile"; private static final Mode TEST_NORMAL_MODE = new Mode((short) 0755); private static final Mode TEST_WEIRD_MODE = new Mode((short) 0157); private static CreateFileOptions sFileOptions; private static CreateFileOptions sWeirdFileOptions; private static CreateFileOptions sNestedFileOptions; private static InodeTree sTree; private static MasterRegistry sRegistry; private PermissionChecker mPermissionChecker; @ClassRule public static TemporaryFolder sTestFolder = new TemporaryFolder(); @Rule public ExpectedException mThrown = ExpectedException.none(); /** * A simple structure to represent a user and its groups. */ private static final class TestUser { private String mUser; private String mGroup; TestUser(String user, String group) { mUser = user; mGroup = group; } String getUser() { return mUser; } String getGroup() { return mGroup; } } /** * Test class implements {@link GroupMappingService} providing user-to-groups mapping. */ public static class FakeUserGroupsMapping implements GroupMappingService { private HashMap<String, String> mUserGroups = new HashMap<>(); /** * Constructor of {@link FakeUserGroupsMapping} to put the user and groups in user-to-groups * HashMap. */ public FakeUserGroupsMapping() { mUserGroups.put(TEST_USER_ADMIN.getUser(), TEST_USER_ADMIN.getGroup()); mUserGroups.put(TEST_USER_1.getUser(), TEST_USER_1.getGroup()); mUserGroups.put(TEST_USER_2.getUser(), TEST_USER_2.getGroup()); mUserGroups.put(TEST_USER_3.getUser(), TEST_USER_3.getGroup()); mUserGroups.put(TEST_USER_SUPERGROUP.getUser(), TEST_USER_SUPERGROUP.getGroup()); } @Override public List<String> getGroups(String user) throws IOException { if (mUserGroups.containsKey(user)) { return Lists.newArrayList(mUserGroups.get(user).split(",")); } return new ArrayList<>(); } } @BeforeClass public static void beforeClass() throws Exception { sFileOptions = CreateFileOptions.defaults().setBlockSizeBytes(Constants.KB).setOwner(TEST_USER_2.getUser()) .setGroup(TEST_USER_2.getGroup()).setMode(TEST_NORMAL_MODE); sWeirdFileOptions = CreateFileOptions.defaults().setBlockSizeBytes(Constants.KB).setOwner(TEST_USER_1.getUser()) .setGroup(TEST_USER_1.getGroup()).setMode(TEST_WEIRD_MODE); sNestedFileOptions = CreateFileOptions.defaults().setBlockSizeBytes(Constants.KB).setOwner(TEST_USER_1.getUser()) .setGroup(TEST_USER_1.getGroup()).setMode(TEST_NORMAL_MODE).setRecursive(true); // setup an InodeTree sRegistry = new MasterRegistry(); JournalFactory factory = new Journal.Factory(new URI(sTestFolder.newFolder().getAbsolutePath())); BlockMaster blockMaster = new BlockMasterFactory().create(sRegistry, factory); InodeDirectoryIdGenerator directoryIdGenerator = new InodeDirectoryIdGenerator(blockMaster); UfsManager ufsManager = Mockito.mock(UfsManager.class); MountTable mountTable = new MountTable(ufsManager); sTree = new InodeTree(blockMaster, directoryIdGenerator, mountTable); sRegistry.start(true); GroupMappingServiceTestUtils.resetCache(); Configuration.set(PropertyKey.SECURITY_GROUP_MAPPING_CLASS, FakeUserGroupsMapping.class.getName()); Configuration.set(PropertyKey.SECURITY_AUTHENTICATION_TYPE, AuthType.SIMPLE.getAuthName()); Configuration.set(PropertyKey.SECURITY_AUTHORIZATION_PERMISSION_ENABLED, "true"); Configuration.set(PropertyKey.SECURITY_AUTHORIZATION_PERMISSION_SUPERGROUP, TEST_SUPER_GROUP); sTree.initializeRoot(TEST_USER_ADMIN.getUser(), TEST_USER_ADMIN.getGroup(), TEST_NORMAL_MODE); // build file structure createAndSetPermission(TEST_DIR_FILE_URI, sNestedFileOptions); createAndSetPermission(TEST_FILE_URI, sFileOptions); createAndSetPermission(TEST_WEIRD_FILE_URI, sWeirdFileOptions); } @AfterClass public static void afterClass() throws Exception { sRegistry.stop(); AuthenticatedClientUser.remove(); ConfigurationTestUtils.resetConfiguration(); } @Before public void before() throws Exception { AuthenticatedClientUser.remove(); mPermissionChecker = new PermissionChecker(sTree); } /** * Helper function to create a path and set the permission to what specified in option. * * @param path path to construct the {@link AlluxioURI} from * @param option method options for creating a file */ private static void createAndSetPermission(String path, CreateFileOptions option) throws Exception { try ( LockedInodePath inodePath = sTree .lockInodePath(new AlluxioURI(path), InodeTree.LockMode.WRITE)) { InodeTree.CreatePathResult result = sTree.createPath(inodePath, option, new NoopJournalContext()); ((InodeFile) result.getCreated().get(result.getCreated().size() - 1)) .setOwner(option.getOwner()).setGroup(option.getGroup()) .setMode(option.getMode().toShort()); } } /** * Verifies that the list of inodes are same as the expected ones. * @param expectedInodes the expected inodes names * @param inodes the inodes for test */ private static void verifyInodesList(String[] expectedInodes, List<Inode<?>> inodes) { String[] inodesName = new String[inodes.size()]; for (int i = 0; i < inodes.size(); i++) { inodesName[i] = inodes.get(i).getName(); } Assert.assertArrayEquals(expectedInodes, inodesName); } @Test public void createFileAndDirs() throws Exception { try (LockedInodePath inodePath = sTree.lockInodePath(new AlluxioURI(TEST_DIR_FILE_URI), InodeTree.LockMode.READ)) { verifyInodesList(TEST_DIR_FILE_URI.split("/"), inodePath.getInodeList()); } try (LockedInodePath inodePath = sTree.lockInodePath(new AlluxioURI(TEST_FILE_URI), InodeTree.LockMode.READ)) { verifyInodesList(TEST_FILE_URI.split("/"), inodePath.getInodeList()); } try (LockedInodePath inodePath = sTree.lockInodePath(new AlluxioURI(TEST_WEIRD_FILE_URI), InodeTree.LockMode.READ)) { verifyInodesList(TEST_WEIRD_FILE_URI.split("/"), inodePath.getInodeList()); } try (LockedInodePath inodePath = sTree.lockInodePath(new AlluxioURI(TEST_NOT_EXIST_URI), InodeTree.LockMode.READ)) { verifyInodesList(new String[]{"", "testDir"}, inodePath.getInodeList()); } } @Test public void fileSystemOwner() throws Exception { checkPermission(TEST_USER_ADMIN, Mode.Bits.ALL, TEST_DIR_FILE_URI); checkPermission(TEST_USER_ADMIN, Mode.Bits.ALL, TEST_DIR_URI); checkPermission(TEST_USER_ADMIN, Mode.Bits.ALL, TEST_FILE_URI); } @Test public void fileSystemSuperGroup() throws Exception { checkPermission(TEST_USER_SUPERGROUP, Mode.Bits.ALL, TEST_DIR_FILE_URI); checkPermission(TEST_USER_SUPERGROUP, Mode.Bits.ALL, TEST_DIR_URI); checkPermission(TEST_USER_SUPERGROUP, Mode.Bits.ALL, TEST_FILE_URI); } @Test public void selfCheckSuccess() throws Exception { // the same owner checkPermission(TEST_USER_1, Mode.Bits.READ, TEST_DIR_FILE_URI); checkPermission(TEST_USER_1, Mode.Bits.WRITE, TEST_DIR_FILE_URI); // not the owner and in other group checkPermission(TEST_USER_2, Mode.Bits.READ, TEST_DIR_FILE_URI); // not the owner but in same group checkPermission(TEST_USER_3, Mode.Bits.READ, TEST_DIR_FILE_URI); } @Test public void selfCheckFailByOtherGroup() throws Exception { mThrown.expect(AccessControlException.class); mThrown.expectMessage(ExceptionMessage.PERMISSION_DENIED.getMessage( toExceptionMessage(TEST_USER_2.getUser(), Mode.Bits.WRITE, TEST_DIR_FILE_URI, "file"))); // not the owner and in other group checkPermission(TEST_USER_2, Mode.Bits.WRITE, TEST_DIR_FILE_URI); } @Test public void selfCheckFailBySameGroup() throws Exception { mThrown.expect(AccessControlException.class); mThrown.expectMessage(ExceptionMessage.PERMISSION_DENIED.getMessage( toExceptionMessage(TEST_USER_3.getUser(), Mode.Bits.WRITE, TEST_DIR_FILE_URI, "file"))); // not the owner but in same group checkPermission(TEST_USER_3, Mode.Bits.WRITE, TEST_DIR_FILE_URI); } @Test public void checkFallThrough() throws Exception { // user can not read, but group can checkPermission(TEST_USER_1, Mode.Bits.READ, TEST_WEIRD_FILE_URI); // user and group can not write, but other can checkPermission(TEST_USER_1, Mode.Bits.WRITE, TEST_WEIRD_FILE_URI); } @Test public void parentCheckSuccess() throws Exception { checkParentOrAncestorPermission(TEST_USER_1, Mode.Bits.WRITE, TEST_DIR_FILE_URI); } @Test public void parentCheckFail() throws Exception { mThrown.expect(AccessControlException.class); mThrown.expectMessage(ExceptionMessage.PERMISSION_DENIED.getMessage( toExceptionMessage(TEST_USER_2.getUser(), Mode.Bits.WRITE, TEST_DIR_FILE_URI, "testDir"))); checkParentOrAncestorPermission(TEST_USER_2, Mode.Bits.WRITE, TEST_DIR_FILE_URI); } @Test public void ancestorCheckSuccess() throws Exception { checkParentOrAncestorPermission(TEST_USER_1, Mode.Bits.WRITE, TEST_NOT_EXIST_URI); } @Test public void ancestorCheckFail() throws Exception { mThrown.expect(AccessControlException.class); mThrown.expectMessage(ExceptionMessage.PERMISSION_DENIED.getMessage( toExceptionMessage(TEST_USER_2.getUser(), Mode.Bits.WRITE, TEST_NOT_EXIST_URI, "testDir"))); checkParentOrAncestorPermission(TEST_USER_2, Mode.Bits.WRITE, TEST_NOT_EXIST_URI); } @Test public void invalidPath() throws Exception { mThrown.expect(InvalidPathException.class); try (LockedInodePath inodePath = sTree .lockInodePath(new AlluxioURI(""), InodeTree.LockMode.READ)) { mPermissionChecker.checkPermission(Mode.Bits.WRITE, inodePath); } } /** * Helper function to check user can perform action on path. */ private void checkPermission(TestUser user, Mode.Bits action, String path) throws Exception { AuthenticatedClientUser.set(user.getUser()); try (LockedInodePath inodePath = sTree .lockInodePath(new AlluxioURI(path), InodeTree.LockMode.READ)) { mPermissionChecker.checkPermission(action, inodePath); } } /** * Helper function to check a user has permission to perform a action on the parent or ancestor of * the given path. * * @param user a user with groups * @param action action that capture the action {@link Mode.Bits} by user * @param path path to construct the {@link AlluxioURI} from */ private void checkParentOrAncestorPermission(TestUser user, Mode.Bits action, String path) throws Exception { AuthenticatedClientUser.set(user.getUser()); try (LockedInodePath inodePath = sTree .lockInodePath(new AlluxioURI(path), InodeTree.LockMode.READ)) { mPermissionChecker.checkParentPermission(action, inodePath); } } private String toExceptionMessage(String user, Mode.Bits action, String path, String inodeName) { StringBuilder stringBuilder = new StringBuilder() .append("user=").append(user).append(", ") .append("access=").append(action).append(", ") .append("path=").append(path).append(": ") .append("failed at ") .append(inodeName); return stringBuilder.toString(); } }