/* * This file is part of Splice Machine. * Splice Machine is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either * version 3, or (at your option) any later version. * Splice Machine is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Affero General Public License for more details. * You should have received a copy of the GNU Affero General Public License along with Splice Machine. * If not, see <http://www.gnu.org/licenses/>. * * Some parts of this source code are based on Apache Derby, and the following notices apply to * Apache Derby: * * Apache Derby is a subproject of the Apache DB project, and is licensed under * the Apache License, Version 2.0 (the "License"); you may not use these files * 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. * * Splice Machine, Inc. has modified the Apache Derby code in this file. * * All such Splice Machine modifications are Copyright 2012 - 2020 Splice Machine, Inc., * and are licensed to you under the GNU Affero General Public License. */ package com.splicemachine.db.security; import com.splicemachine.db.authentication.SystemPrincipal; import com.splicemachine.db.iapi.error.StandardException; import com.splicemachine.db.iapi.util.IdUtil; import org.junit.Ignore; import org.junit.Test; import javax.security.auth.Subject; import java.io.IOException; import java.security.AccessControlException; import java.security.AccessController; import java.security.Permission; import java.security.PrivilegedAction; import java.util.HashSet; import java.util.Set; import static org.junit.Assert.*; /** * This class tests the basic permission classes for system privileges. */ public class SystemPrivilegesPermissionTest { /** * The policy file name for the subject authorization tests. */ static private String POLICY_FILE_NAME = "com/splicemachine/db/security/SystemPrivilegesPermissionTest.policy"; /** * The policy file name for the DatabasePermission API test. */ static private String POLICY_FILE_NAME1 = "com/splicemachine/db/security/SystemPrivilegesPermissionTest1.policy"; /** * Some directory paths for testing DatabasePermissions. */ static private final String[] dirPaths = { "-", "*", "level0", "level0a", "level0/-", "level0/*", "level0/level1", "level0/level1/level2" }; /** * Some relative directory paths for testing DatabasePermissions. */ static private final String[] relDirPaths = new String[dirPaths.length]; static { for (int i = 0; i < relDirPaths.length; i++) { relDirPaths[i] = "directory:" + dirPaths[i]; } } /** * Some relative directory path aliases for testing DatabasePermissions. */ static private final String[] relDirPathAliases = new String[dirPaths.length]; static { for (int i = 0; i < relDirPaths.length; i++) { relDirPathAliases[i] = "directory:./" + dirPaths[i]; } } /** * Some absolute directory paths for testing DatabasePermissions. */ static private final String[] absDirPaths = new String[dirPaths.length]; static { for (int i = 0; i < relDirPaths.length; i++) { absDirPaths[i] = "directory:/" + dirPaths[i]; } } /** * Some absolute directory path aliases for testing DatabasePermissions. */ static private final String[] absDirPathAliases = new String[dirPaths.length]; static { for (int i = 0; i < relDirPaths.length; i++) { absDirPathAliases[i] = "directory:/dummy/../" + dirPaths[i]; } } /** * The matrix defining which of the above directory paths imply each other. * <p> * For instance, dirPathImpls[1][2] shows the expected value for: * <ul> * <li> DP("directory:*").implies(DP(directory:level0")) * <li> DP("directory:./*").implies(DP(directory:./level0")) * <li> DP("directory:/*").implies(DP(directory:/level0")) * <li> DP("directory:/dummy/..*").implies(DP(directory:/dummy/..level0")) * </ul> */ static private final boolean[][] dirPathImpls = { {true, true, true, true, true, true, true, true}, {false, true, true, true, false, false, false, false}, {false, false, true, false, false, false, false, false}, {false, false, false, true, false, false, false, false}, {false, false, false, false, true, true, true, true}, {false, false, false, false, false, true, true, false}, {false, false, false, false, false, false, true, false}, {false, false, false, false, false, false, false, true} }; /** * Tests SystemPrincipal. */ @Test public void testSystemPrincipal() { // test SystemPrincipal with null name argument try { new SystemPrincipal(null); fail("expected NullPointerException"); } catch (NullPointerException ex) { // expected exception } // test SystemPrincipal with empty name argument try { new SystemPrincipal(""); fail("expected IllegalArgumentException"); } catch (IllegalArgumentException ex) { // expected exception } } /** * Tests SystemPermission. */ @Test public void testSystemPermission() { // test SystemPermission with null name argument try { new SystemPermission(null, null); fail("expected NullPointerException"); } catch (NullPointerException ex) { // expected exception } // test SystemPermission with empty name argument try { new SystemPermission("", null); fail("expected IllegalArgumentException"); } catch (IllegalArgumentException ex) { // expected exception } // test SystemPermission with illegal name argument try { new SystemPermission("illegal_name", null); fail("expected IllegalArgumentException"); } catch (IllegalArgumentException ex) { // expected exception } String[] validNames = { SystemPermission.ENGINE, SystemPermission.JMX, SystemPermission.SERVER }; // In order of the canonical actions expected String[] validActions = { SystemPermission.CONTROL, SystemPermission.MONITOR, SystemPermission.SHUTDOWN, }; // Check all valid combinations (which is all) with // a single action Permission[] all = new Permission[ validNames.length * validActions.length]; int c = 0; for (String validName1 : validNames) { for (String validAction : validActions) { Permission p = new SystemPermission( validName1, validAction); assertEquals(validName1, p.getName()); assertEquals(validAction, p.getActions()); // test SystemPermission.equals() assertFalse(p.equals(null)); assertFalse(p.equals(new Object())); this.assertEquivalentPermissions(p, p); all[c++] = p; } } // All the permissions are different. checkDistinctPermissions(all); // Check two actions for (String validName : validNames) { for (int a = 0; a < validActions.length; a++) { Permission base = new SystemPermission( validName, validActions[a]); // Two actions for (int oa = 0; oa < validActions.length; oa++) { Permission p = new SystemPermission( validName, validActions[a] + "," + validActions[oa]); if (oa == a) { // Same action added twice assertEquivalentPermissions(base, p); // Canonical form should collapse into a single action assertEquals(validActions[a], p.getActions()); } else { // Implies logic, the one with one permission // is implied by the other but not vice-versa. assertTrue(p.implies(base)); assertFalse(base.implies(p)); // Names in canonical form int f; int s; if (oa < a) { f = oa; s = a; } else { f = a; s = oa; } if (oa < a) assertEquals(validActions[f] + "," + validActions[s], p.getActions()); } } } } } /** * Tests SystemPermissions against the Policy. */ public void policyTestSystemPermissionGrants() { final Permission shutdown = new SystemPermission( SystemPermission.SERVER, SystemPermission.SHUTDOWN); // test SystemPermission for authorized user final SystemPrincipal authorizedUser = new SystemPrincipal("authorizedSystemUser"); execute(authorizedUser, new ShutdownAction(shutdown), true); // test SystemPermission for unauthorized user final SystemPrincipal unAuthorizedUser = new SystemPrincipal("unAuthorizedSystemUser"); execute(unAuthorizedUser, new ShutdownAction(shutdown), false); } /** * Tests DatabasePermission. */ @Ignore @org.junit.Test public void testDatabasePermission() throws IOException { // test DatabasePermission with null url try { new DatabasePermission(null, DatabasePermission.CREATE); fail("expected NullPointerException"); } catch (NullPointerException ex) { // expected exception } // test DatabasePermission with empty url try { new DatabasePermission("", DatabasePermission.CREATE); fail("expected IllegalArgumentException"); } catch (IllegalArgumentException ex) { // expected exception } // test DatabasePermission with illegal url try { new DatabasePermission("no_url", DatabasePermission.CREATE); fail("expected IllegalArgumentException"); } catch (IllegalArgumentException ex) { // expected exception } // this test's commented out because it's platform-dependent // (no reliable way to make it pass on Unix) // test DatabasePermission with non-canonicalizable URL //try { // //new DatabasePermission("directory:.*/\\:///../", // // DatabasePermission.CREATE); // new DatabasePermission("directory:\n/../../../.*/\\:///../", // DatabasePermission.CREATE); // fail("expected IOException"); //} catch (IOException ex) { // // expected exception //} // test DatabasePermission with null actions try { new DatabasePermission("directory:dir", null); fail("expected NullPointerException"); } catch (NullPointerException ex) { // expected exception } // test DatabasePermission with empty actions try { new DatabasePermission("directory:dir", ""); fail("expected IllegalArgumentException"); } catch (IllegalArgumentException ex) { // expected exception } // test DatabasePermission with illegal action list try { new DatabasePermission("directory:dir", "illegal_action"); fail("expected IllegalArgumentException"); } catch (IllegalArgumentException ex) { // expected exception } // test DatabasePermission with illegal action list try { new DatabasePermission("directory:dir", "illegal,action"); fail("expected IllegalArgumentException"); } catch (IllegalArgumentException ex) { // expected exception } // test DatabasePermission on illegal action list try { new DatabasePermission("directory:dir", "illegal;action"); fail("expected IllegalArgumentException"); } catch (IllegalArgumentException ex) { // expected exception } // test DatabasePermission on relative directory paths final DatabasePermission[] relDirPathPerms = new DatabasePermission[relDirPaths.length]; for (int i = 0; i < relDirPaths.length; i++) { relDirPathPerms[i] = new DatabasePermission(relDirPaths[i], DatabasePermission.CREATE); } checkNameAndActions(relDirPathPerms, relDirPaths); checkHashCodeAndEquals(relDirPathPerms, relDirPathPerms); checkImplies(relDirPathPerms, relDirPathPerms, dirPathImpls); // test DatabasePermission on relative directory path aliases final DatabasePermission[] relDirPathAliasPerms = new DatabasePermission[relDirPathAliases.length]; for (int i = 0; i < relDirPathAliases.length; i++) { relDirPathAliasPerms[i] = new DatabasePermission(relDirPathAliases[i], DatabasePermission.CREATE); } checkNameAndActions(relDirPathAliasPerms, relDirPathAliases); checkHashCodeAndEquals(relDirPathPerms, relDirPathAliasPerms); checkImplies(relDirPathPerms, relDirPathAliasPerms, dirPathImpls); checkImplies(relDirPathAliasPerms, relDirPathPerms, dirPathImpls); // test DatabasePermission on absolute directory paths final DatabasePermission[] absDirPathPerms = new DatabasePermission[absDirPaths.length]; for (int i = 0; i < absDirPaths.length; i++) { absDirPathPerms[i] = new DatabasePermission(absDirPaths[i], DatabasePermission.CREATE); } checkNameAndActions(absDirPathPerms, absDirPaths); checkHashCodeAndEquals(absDirPathPerms, absDirPathPerms); checkImplies(absDirPathPerms, absDirPathPerms, dirPathImpls); // test DatabasePermission on absolute directory path aliases final DatabasePermission[] absDirPathAliasPerms = new DatabasePermission[absDirPathAliases.length]; for (int i = 0; i < absDirPathAliases.length; i++) { absDirPathAliasPerms[i] = new DatabasePermission(absDirPathAliases[i], DatabasePermission.CREATE); } checkNameAndActions(absDirPathAliasPerms, absDirPathAliases); checkHashCodeAndEquals(absDirPathPerms, absDirPathAliasPerms); checkImplies(absDirPathPerms, absDirPathAliasPerms, dirPathImpls); checkImplies(absDirPathAliasPerms, absDirPathPerms, dirPathImpls); // test DatabasePermission for the inclusive path specification final String inclPermissionUrl = "directory:<<ALL FILES>>"; final DatabasePermission[] inclPerms = {new DatabasePermission(inclPermissionUrl, DatabasePermission.CREATE)}; checkNameAndActions(inclPerms, new String[]{inclPermissionUrl}); final DatabasePermission[] inclPerms1 = {new DatabasePermission(inclPermissionUrl, DatabasePermission.CREATE)}; checkHashCodeAndEquals(inclPerms, inclPerms1); checkImplies(inclPerms, inclPerms1, new boolean[][]{{true}}); final boolean[][] allTrue = new boolean[1][dirPaths.length]; for (int j = 0; j < dirPaths.length; j++) { allTrue[0][j] = true; } final boolean[][] allFalse = new boolean[dirPaths.length][1]; for (int i = 0; i < dirPaths.length; i++) { allFalse[i][0] = false; } checkImplies(inclPerms, relDirPathPerms, allTrue); checkImplies(relDirPathPerms, inclPerms, allFalse); checkImplies(inclPerms, relDirPathAliasPerms, allTrue); checkImplies(relDirPathAliasPerms, inclPerms, allFalse); checkImplies(inclPerms, absDirPathPerms, allTrue); checkImplies(absDirPathPerms, inclPerms, allFalse); checkImplies(inclPerms, absDirPathAliasPerms, allTrue); checkImplies(absDirPathAliasPerms, inclPerms, allFalse); } /** * Tests DatabasePermissions against the Policy. */ public void policyTestDatabasePermissionGrants() throws IOException { final DatabasePermission[] relDirPathPerms = new DatabasePermission[relDirPaths.length]; for (int i = 0; i < relDirPaths.length; i++) { relDirPathPerms[i] = new DatabasePermission(relDirPaths[i], DatabasePermission.CREATE); } // test DatabasePermission for unauthorized, authorized, and // all-authorized users final int[] singleLocPaths = {2, 3, 6, 7}; final SystemPrincipal authorizedUser = new SystemPrincipal("authorizedSystemUser"); final SystemPrincipal unAuthorizedUser = new SystemPrincipal("unAuthorizedSystemUser"); final SystemPrincipal superUser = new SystemPrincipal("superUser"); for (final int j : singleLocPaths) { execute(unAuthorizedUser, new CreateDatabaseAction(relDirPathPerms[j]), false); execute(authorizedUser, new CreateDatabaseAction(relDirPathPerms[j]), (j != 6)); execute(superUser, new CreateDatabaseAction(relDirPathPerms[j]), true); } // test DatabasePermission for any user final SystemPrincipal anyUser = new SystemPrincipal("anyUser"); final DatabasePermission dbPerm = new DatabasePermission("directory:dir", DatabasePermission.CREATE); execute(anyUser, new CreateDatabaseAction(dbPerm), true); } /** * Runs a privileged user action for a given principal. */ private void execute(SystemPrincipal principal, PrivilegedAction action, boolean isGrantExpected) { //println(); //println(" testing action " + action); final RunAsPrivilegedUserAction runAsPrivilegedUserAction = new RunAsPrivilegedUserAction(principal, action); try { AccessController.doPrivileged(runAsPrivilegedUserAction); //println(" Congrats! access granted " + action); if (!isGrantExpected) { fail("expected AccessControlException"); } } catch (AccessControlException ace) { //println(" Yikes! " + ace.getMessage()); if (isGrantExpected) { //fail("caught AccessControlException"); throw ace; } } } /** * Tests DatabasePermission.getName() and .getActions(). */ private void checkNameAndActions(DatabasePermission[] dbperm, String[] dbpath) throws IOException { //assert(dpperm.length == dbpath.length) for (int i = 0; i < dbperm.length; i++) { final DatabasePermission dbp = dbperm[i]; assertEquals("test: " + dbp + ".getName()", dbpath[i], dbp.getName()); assertEquals("test: " + dbp + ".getActions()", DatabasePermission.CREATE, dbp.getActions()); } } /** * Tests DatabasePermission.hashCode() and .equals(). */ private void checkHashCodeAndEquals(Permission[] dbp0, Permission[] dbp1) throws IOException { //assert(dbp0.length == dbp1.length) for (int i = 0; i < dbp0.length; i++) { final Permission p0 = dbp0[i]; for (int j = 0; j < dbp0.length; j++) { final Permission p1 = dbp1[j]; if (i == j) { assertTrue(p0.hashCode() == p1.hashCode()); assertTrue(p0.equals(p1)); } else { assertTrue(p0.hashCode() != p1.hashCode()); assertTrue(!p0.equals(p1)); } } } } /** * Tests DatabasePermission.implies(). */ private void checkImplies(Permission[] dbp0, Permission[] dbp1, boolean[][] impls) throws IOException { for (int i = 0; i < dbp0.length; i++) { final Permission p0 = dbp0[i]; for (int j = 0; j < dbp1.length; j++) { final Permission p1 = dbp1[j]; assertEquals("test: " + p0 + ".implies" + p1, impls[i][j], p0.implies(p1)); //assertEquals("test: " + p1 + ".implies" + p0, // impls[j][i], p1.implies(p0)); } } } /** * Check thet a set of Permission objects are distinct, * do not equal or imply each other. */ private void checkDistinctPermissions(Permission[] set) { for (int i = 0; i < set.length; i++) { Permission pi = set[i]; for (int j = 0; j < set.length; j++) { Permission pj = set[j]; if (i == j) { // Permission is itself assertEquivalentPermissions(pi, pj); continue; } assertFalse(pi.equals(pj)); assertFalse(pj.equals(pi)); assertFalse(pi.implies(pj)); assertFalse(pj.implies(pi)); } } } private void assertEquivalentPermissions(Permission p1, Permission p2) { assertTrue(p1.equals(p2)); assertTrue(p2.equals(p1)); assertEquals(p1.hashCode(), p2.hashCode()); assertTrue(p1.implies(p2)); assertTrue(p1.implies(p2)); } /** * Represents a Shutdown server and engine action. */ public class ShutdownAction implements PrivilegedAction { protected final Permission permission; public ShutdownAction(Permission permission) { this.permission = permission; } public Object run() { //println(" checking access " + permission + "..."); AccessController.checkPermission(permission); //println(" granted access " + this); return null; } public String toString() { return permission.toString(); } } /** * Represents a Create Database action. */ public class CreateDatabaseAction implements PrivilegedAction { protected final Permission permission; public CreateDatabaseAction(Permission permission) { this.permission = permission; } public Object run() { //println(" checking access " + permission + "..."); AccessController.checkPermission(permission); //println(" granted access " + this); return null; } public String toString() { return permission.toString(); } } /** * Returns the Authorization Identifier for a principal name. * * @param name the name of the principal * @return the authorization identifier for this principal */ static private String getAuthorizationId(String name) { // RuntimeException messages not localized if (name == null) { throw new NullPointerException("name can't be null"); } if (name.length() == 0) { throw new IllegalArgumentException("name can't be empty"); } try { return IdUtil.getUserAuthorizationId(name); } catch (StandardException se) { throw new IllegalArgumentException(se.getMessage()); } } /** * Represents a Privileged User action. */ static public class RunAsPrivilegedUserAction implements PrivilegedAction { final private SystemPrincipal principal; final private PrivilegedAction action; public RunAsPrivilegedUserAction(SystemPrincipal principal, PrivilegedAction action) { this.principal = principal; this.action = action; } public Object run() { final boolean readOnly = true; final Set principals = new HashSet(); final Set publicCredentials = new HashSet(); final Set privateCredentials = new HashSet(); // add the given principal principals.add(principal); // also add a principal with the "normalized" name for testing // authorization ids final String normalized = getAuthorizationId(principal.getName()); principals.add(new SystemPrincipal(normalized)); final Subject subject = new Subject(readOnly, principals, publicCredentials, privateCredentials); // check subject's permission with a fresh AccessControlContext, // not the thread's current one (Subject.doAs(subject, action)) // println(" run doAsPrivileged() as " + principal + "..."); // The alternative approach to use Subject.doAs(subject, action) // instead of Subject.doAsPrivileged(subject, action, null) has // issues: there are subtile differences between these methods // regarding the checking of the caller's protection domain. To // make doAs() work, the shutdown/createDatabase permissions must // be granted to the codebase (class RunAsPrivilegedUserAction). // This, however, defeats the purpose since everyone now's granted // permission. In contrast, doAsPrivileged() with a null ACC // seems to effectively ignore the caller's protection domain, so // the check now only depends on the principal's permissions. Subject.doAsPrivileged(subject, action, null); //Subject.doAs(subject, action); return null; } } }