| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 org.apache.hadoop.ozone.shell; |
| |
| import com.fasterxml.jackson.databind.ObjectMapper; |
| import com.google.common.base.Strings; |
| import org.apache.commons.io.FileUtils; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.hadoop.fs.FileUtil; |
| import org.apache.hadoop.hdds.cli.GenericCli; |
| import org.apache.hadoop.hdds.conf.OzoneConfiguration; |
| import org.apache.hadoop.io.retry.RetryInvocationHandler; |
| import org.apache.hadoop.ozone.MiniOzoneCluster; |
| import org.apache.hadoop.ozone.OzoneConsts; |
| import org.apache.hadoop.ozone.ha.ConfUtils; |
| import org.apache.hadoop.ozone.om.multitenant.AuthorizerLockImpl; |
| import org.apache.hadoop.ozone.om.OMConfigKeys; |
| import org.apache.hadoop.ozone.om.OMMultiTenantManagerImpl; |
| import org.apache.hadoop.ozone.om.helpers.OmVolumeArgs; |
| import org.apache.hadoop.ozone.om.multitenant.MultiTenantAccessAuthorizerRangerPlugin; |
| import org.apache.hadoop.ozone.om.request.s3.tenant.OMTenantAssignUserAccessIdRequest; |
| import org.apache.hadoop.ozone.om.request.s3.tenant.OMTenantCreateRequest; |
| import org.apache.hadoop.ozone.shell.tenant.TenantShell; |
| import org.apache.hadoop.security.UserGroupInformation; |
| import org.apache.ozone.test.GenericTestUtils; |
| import org.junit.After; |
| import org.junit.AfterClass; |
| import org.junit.Assert; |
| import org.junit.Before; |
| import org.junit.BeforeClass; |
| import org.junit.Rule; |
| import org.junit.Test; |
| import org.junit.rules.Timeout; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.slf4j.event.Level; |
| import picocli.CommandLine; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.PrintStream; |
| import java.io.UnsupportedEncodingException; |
| import java.security.PrivilegedExceptionAction; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.UUID; |
| |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_MULTITENANCY_ENABLED; |
| import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_RANGER_HTTPS_ADMIN_API_PASSWD; |
| import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_RANGER_HTTPS_ADMIN_API_USER; |
| import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_RANGER_HTTPS_ADDRESS_KEY; |
| import static org.junit.Assert.fail; |
| |
| /** |
| * Integration test for Ozone tenant shell command. HA enabled. |
| * |
| * TODO: HDDS-6338. Add a Kerberized version of this |
| * TODO: HDDS-6336. Add a mock Ranger server to test Ranger HTTP endpoint calls |
| */ |
| public class TestOzoneTenantShell { |
| |
| private static final Logger LOG = |
| LoggerFactory.getLogger(TestOzoneTenantShell.class); |
| |
| static { |
| System.setProperty("log4j.configurationFile", "auditlog.properties"); |
| } |
| |
| private static final String DEFAULT_ENCODING = UTF_8.name(); |
| |
| /** |
| * Set the timeout for every test. |
| */ |
| @Rule |
| public Timeout testTimeout = Timeout.seconds(300); |
| |
| private static File baseDir; |
| private static File testFile; |
| private static final File AUDIT_LOG_FILE = new File("audit.log"); |
| |
| private static OzoneConfiguration conf = null; |
| private static MiniOzoneCluster cluster = null; |
| private static OzoneShell ozoneSh = null; |
| private static TenantShell tenantShell = null; |
| |
| private final ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| private final ByteArrayOutputStream err = new ByteArrayOutputStream(); |
| private static final PrintStream OLD_OUT = System.out; |
| private static final PrintStream OLD_ERR = System.err; |
| |
| private static String omServiceId; |
| private static String clusterId; |
| private static String scmId; |
| private static int numOfOMs; |
| |
| private static final boolean USE_ACTUAL_RANGER = false; |
| |
| /** |
| * Create a MiniOzoneCluster for testing with using distributed Ozone |
| * handler type. |
| * |
| * @throws Exception |
| */ |
| @BeforeClass |
| public static void init() throws Exception { |
| // Remove audit log output if it exists |
| if (AUDIT_LOG_FILE.exists()) { |
| AUDIT_LOG_FILE.delete(); |
| } |
| |
| conf = new OzoneConfiguration(); |
| conf.setBoolean( |
| OMMultiTenantManagerImpl.OZONE_OM_TENANT_DEV_SKIP_RANGER, true); |
| conf.setBoolean(OZONE_OM_MULTITENANCY_ENABLED, true); |
| |
| if (USE_ACTUAL_RANGER) { |
| conf.set(OZONE_RANGER_HTTPS_ADDRESS_KEY, System.getenv("RANGER_ADDRESS")); |
| conf.set(OZONE_OM_RANGER_HTTPS_ADMIN_API_USER, |
| System.getenv("RANGER_USER")); |
| conf.set(OZONE_OM_RANGER_HTTPS_ADMIN_API_PASSWD, |
| System.getenv("RANGER_PASSWD")); |
| } else { |
| conf.setBoolean(OMMultiTenantManagerImpl.OZONE_OM_TENANT_DEV_SKIP_RANGER, |
| true); |
| } |
| |
| String path = GenericTestUtils.getTempPath( |
| TestOzoneTenantShell.class.getSimpleName()); |
| baseDir = new File(path); |
| baseDir.mkdirs(); |
| |
| testFile = new File(path + OzoneConsts.OZONE_URI_DELIMITER + "testFile"); |
| testFile.getParentFile().mkdirs(); |
| testFile.createNewFile(); |
| |
| ozoneSh = new OzoneShell(); |
| tenantShell = new TenantShell(); |
| |
| // Init cluster |
| omServiceId = "om-service-test1"; |
| numOfOMs = 3; |
| clusterId = UUID.randomUUID().toString(); |
| scmId = UUID.randomUUID().toString(); |
| cluster = MiniOzoneCluster.newOMHABuilder(conf) |
| .setClusterId(clusterId) |
| .setScmId(scmId) |
| .setOMServiceId(omServiceId) |
| .setNumOfOzoneManagers(numOfOMs) |
| .withoutDatanodes() // Remove this once we are actually writing data |
| .build(); |
| cluster.waitForClusterToBeReady(); |
| } |
| |
| /** |
| * shutdown MiniOzoneCluster. |
| */ |
| @AfterClass |
| public static void shutdown() { |
| if (cluster != null) { |
| cluster.shutdown(); |
| } |
| |
| if (baseDir != null) { |
| FileUtil.fullyDelete(baseDir, true); |
| } |
| |
| if (AUDIT_LOG_FILE.exists()) { |
| AUDIT_LOG_FILE.delete(); |
| } |
| } |
| |
| @Before |
| public void setup() throws UnsupportedEncodingException { |
| System.setOut(new PrintStream(out, false, UTF_8.name())); |
| System.setErr(new PrintStream(err, false, UTF_8.name())); |
| |
| // Suppress OMNotLeaderException in the log |
| GenericTestUtils.setLogLevel(RetryInvocationHandler.LOG, Level.WARN); |
| // Enable debug logging for interested classes |
| GenericTestUtils.setLogLevel(OMTenantCreateRequest.LOG, Level.DEBUG); |
| GenericTestUtils.setLogLevel( |
| OMTenantAssignUserAccessIdRequest.LOG, Level.DEBUG); |
| GenericTestUtils.setLogLevel( |
| MultiTenantAccessAuthorizerRangerPlugin.LOG, Level.DEBUG); |
| GenericTestUtils.setLogLevel(AuthorizerLockImpl.LOG, Level.DEBUG); |
| } |
| |
| @After |
| public void reset() { |
| // reset stream after each unit test |
| out.reset(); |
| err.reset(); |
| |
| // restore system streams |
| System.setOut(OLD_OUT); |
| System.setErr(OLD_ERR); |
| } |
| |
| /** |
| * Returns exit code. |
| */ |
| private int execute(GenericCli shell, String[] args) { |
| LOG.info("Executing shell command with args {}", Arrays.asList(args)); |
| CommandLine cmd = shell.getCmd(); |
| |
| CommandLine.IExecutionExceptionHandler exceptionHandler = |
| (ex, commandLine, parseResult) -> { |
| new PrintStream(err, true, DEFAULT_ENCODING).println(ex.getMessage()); |
| return commandLine.getCommandSpec().exitCodeOnExecutionException(); |
| }; |
| |
| // Since there is no elegant way to pass Ozone config to the shell, |
| // the idea is to use 'set' to place those OM HA configs. |
| String[] argsWithHAConf = getHASetConfStrings(args); |
| |
| cmd.setExecutionExceptionHandler(exceptionHandler); |
| return cmd.execute(argsWithHAConf); |
| } |
| |
| /** |
| * Helper that appends HA service id to args. |
| */ |
| private int executeHA(GenericCli shell, String[] args) { |
| final String[] newArgs = new String[args.length + 1]; |
| System.arraycopy(args, 0, newArgs, 0, args.length); |
| newArgs[args.length] = "--om-service-id=" + omServiceId; |
| return execute(shell, newArgs); |
| } |
| |
| /** |
| * Execute command, assert exception message and returns true if error |
| * was thrown. |
| */ |
| private void executeWithError(OzoneShell shell, String[] args, |
| String expectedError) { |
| if (Strings.isNullOrEmpty(expectedError)) { |
| execute(shell, args); |
| } else { |
| try { |
| execute(shell, args); |
| fail("Exception is expected from command execution " + Arrays |
| .asList(args)); |
| } catch (Exception ex) { |
| if (!Strings.isNullOrEmpty(expectedError)) { |
| Throwable exceptionToCheck = ex; |
| if (exceptionToCheck.getCause() != null) { |
| exceptionToCheck = exceptionToCheck.getCause(); |
| } |
| Assert.assertTrue( |
| String.format( |
| "Error of OzoneShell code doesn't contain the " + |
| "exception [%s] in [%s]", |
| expectedError, exceptionToCheck.getMessage()), |
| exceptionToCheck.getMessage().contains(expectedError)); |
| } |
| } |
| } |
| } |
| |
| private String getSetConfStringFromConf(String key) { |
| return String.format("--set=%s=%s", key, conf.get(key)); |
| } |
| |
| private String generateSetConfString(String key, String value) { |
| return String.format("--set=%s=%s", key, value); |
| } |
| |
| /** |
| * Helper function to get a String array to be fed into OzoneShell. |
| * @param numOfArgs Additional number of arguments after the HA conf string, |
| * this translates into the number of empty array elements |
| * after the HA conf string. |
| * @return String array. |
| */ |
| private String[] getHASetConfStrings(int numOfArgs) { |
| assert (numOfArgs >= 0); |
| String[] res = new String[1 + 1 + numOfOMs + numOfArgs]; |
| final int indexOmServiceIds = 0; |
| final int indexOmNodes = 1; |
| final int indexOmAddressStart = 2; |
| |
| res[indexOmServiceIds] = getSetConfStringFromConf( |
| OMConfigKeys.OZONE_OM_SERVICE_IDS_KEY); |
| |
| String omNodesKey = ConfUtils.addKeySuffixes( |
| OMConfigKeys.OZONE_OM_NODES_KEY, omServiceId); |
| String omNodesVal = conf.get(omNodesKey); |
| res[indexOmNodes] = generateSetConfString(omNodesKey, omNodesVal); |
| |
| String[] omNodesArr = omNodesVal.split(","); |
| // Sanity check |
| assert (omNodesArr.length == numOfOMs); |
| for (int i = 0; i < numOfOMs; i++) { |
| res[indexOmAddressStart + i] = |
| getSetConfStringFromConf(ConfUtils.addKeySuffixes( |
| OMConfigKeys.OZONE_OM_ADDRESS_KEY, omServiceId, omNodesArr[i])); |
| } |
| |
| return res; |
| } |
| |
| /** |
| * Helper function to create a new set of arguments that contains HA configs. |
| * @param existingArgs Existing arguments to be fed into OzoneShell command. |
| * @return String array. |
| */ |
| private String[] getHASetConfStrings(String[] existingArgs) { |
| // Get a String array populated with HA configs first |
| String[] res = getHASetConfStrings(existingArgs.length); |
| |
| int indexCopyStart = res.length - existingArgs.length; |
| // Then copy the existing args to the returned String array |
| System.arraycopy(existingArgs, 0, res, indexCopyStart, |
| existingArgs.length); |
| return res; |
| } |
| |
| /** |
| * Helper function that checks command output AND clears it. |
| */ |
| private void checkOutput(ByteArrayOutputStream stream, String stringToMatch, |
| boolean exactMatch) throws IOException { |
| stream.flush(); |
| final String str = stream.toString(DEFAULT_ENCODING); |
| checkOutput(str, stringToMatch, exactMatch); |
| stream.reset(); |
| } |
| |
| private void checkOutput(ByteArrayOutputStream stream, String stringToMatch, |
| boolean exactMatch, boolean expectValidJSON) throws IOException { |
| stream.flush(); |
| final String str = stream.toString(DEFAULT_ENCODING); |
| if (expectValidJSON) { |
| // Verify if the String can be parsed as a valid JSON |
| final ObjectMapper objectMapper = new ObjectMapper(); |
| objectMapper.readTree(str); |
| } |
| checkOutput(str, stringToMatch, exactMatch); |
| stream.reset(); |
| } |
| |
| private void checkOutput(String str, String stringToMatch, |
| boolean exactMatch) { |
| if (exactMatch) { |
| Assert.assertEquals(stringToMatch, str); |
| } else { |
| Assert.assertTrue(str, str.contains(stringToMatch)); |
| } |
| } |
| |
| private void deleteVolume(String volumeName) throws IOException { |
| int exitC = execute(ozoneSh, new String[] {"volume", "delete", volumeName}); |
| checkOutput(out, "Volume " + volumeName + " is deleted\n", true); |
| checkOutput(err, "", true); |
| // Exit code should be 0 |
| Assert.assertEquals(0, exitC); |
| } |
| |
| @Test |
| public void testAssignAdmin() throws IOException { |
| |
| final String tenantName = "devaa"; |
| final String userName = "alice"; |
| |
| executeHA(tenantShell, new String[] {"create", tenantName}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| // Loop assign-revoke 4 times |
| for (int i = 0; i < 4; i++) { |
| executeHA(tenantShell, new String[] { |
| "user", "assign", userName, "--tenant=" + tenantName}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID=", false); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] {"--verbose", "user", "assign-admin", |
| tenantName + "$" + userName, "--tenant=" + tenantName, |
| "--delegated=true"}); |
| checkOutput(out, "{\n" + " \"accessId\": \"devaa$alice\",\n" |
| + " \"tenantId\": \"devaa\",\n" + " \"isAdmin\": true,\n" |
| + " \"isDelegatedAdmin\": true\n" + "}\n", true, true); |
| checkOutput(err, "", true); |
| |
| // Clean up |
| executeHA(tenantShell, new String[] {"--verbose", "user", "revoke-admin", |
| tenantName + "$" + userName, "--tenant=" + tenantName}); |
| checkOutput(out, "{\n" + " \"accessId\": \"devaa$alice\",\n" |
| + " \"tenantId\": \"devaa\",\n" + " \"isAdmin\": false,\n" |
| + " \"isDelegatedAdmin\": false\n" + "}\n", true, true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] { |
| "user", "revoke", tenantName + "$" + userName}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| } |
| |
| // Clean up |
| executeHA(tenantShell, new String[] {"delete", tenantName}); |
| checkOutput(out, "", true); |
| checkOutput(err, "Deleted tenant '" + tenantName + "'.\n", false); |
| deleteVolume(tenantName); |
| |
| // Sanity check: tenant list should be empty |
| executeHA(tenantShell, new String[] {"list"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| } |
| |
| /** |
| * Test tenant create, assign user, get user info, assign admin, revoke admin |
| * and revoke user flow. |
| */ |
| @Test |
| @SuppressWarnings("methodlength") |
| public void testOzoneTenantBasicOperations() throws IOException { |
| |
| List<String> lines = FileUtils.readLines(AUDIT_LOG_FILE, (String)null); |
| Assert.assertEquals(0, lines.size()); |
| |
| executeHA(tenantShell, new String[] {"list"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| // Create tenants |
| // Equivalent to `ozone tenant create finance` |
| executeHA(tenantShell, new String[] {"create", "finance"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] {"list"}); |
| checkOutput(out, "finance\n", true); |
| checkOutput(err, "", true); |
| |
| lines = FileUtils.readLines(AUDIT_LOG_FILE, (String)null); |
| Assert.assertTrue(lines.size() > 0); |
| checkOutput(lines.get(lines.size() - 1), "ret=SUCCESS", false); |
| |
| // Check volume creation |
| OmVolumeArgs volArgs = cluster.getOzoneManager().getVolumeInfo("finance"); |
| Assert.assertEquals("finance", volArgs.getVolume()); |
| |
| // Creating the tenant with the same name again should fail |
| executeHA(tenantShell, new String[] {"create", "finance"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "Tenant 'finance' already exists\n", true); |
| |
| executeHA(tenantShell, new String[] {"create", "research"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] {"create", "dev"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] {"ls"}); |
| checkOutput(out, "dev\nfinance\nresearch\n", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] {"list", "--json"}); |
| // Not checking the full output here |
| checkOutput(out, "\"tenantId\": \"dev\",", false); |
| checkOutput(err, "", true); |
| |
| // Attempt user getsecret before assignment, should fail |
| executeHA(tenantShell, new String[] { |
| "user", "getsecret", "finance$bob"}); |
| checkOutput(out, "", false); |
| checkOutput(err, "accessId 'finance$bob' doesn't exist\n", |
| true); |
| |
| // Assign user accessId |
| // Equivalent to `ozone tenant user assign bob --tenant=finance` |
| executeHA(tenantShell, new String[] { |
| "--verbose", "user", "assign", "bob", "--tenant=finance"}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID='finance$bob'\n" |
| + "export AWS_SECRET_ACCESS_KEY='", false); |
| checkOutput(err, "Assigned 'bob' to 'finance' with accessId" |
| + " 'finance$bob'.\n", true); |
| |
| // Try user getsecret again after assignment, should succeed |
| executeHA(tenantShell, new String[] { |
| "user", "getsecret", "finance$bob"}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID='finance$bob'\n", |
| false); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] { |
| "--verbose", "user", "assign", "bob", "--tenant=research"}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID='research$bob'\n" |
| + "export AWS_SECRET_ACCESS_KEY='", false); |
| checkOutput(err, "Assigned 'bob' to 'research' with accessId" |
| + " 'research$bob'.\n", true); |
| |
| executeHA(tenantShell, new String[] { |
| "user", "assign", "bob", "--tenant=dev"}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID='dev$bob'\n" |
| + "export AWS_SECRET_ACCESS_KEY='", false); |
| checkOutput(err, "", true); |
| |
| // accessId length exceeding limit, should fail |
| executeHA(tenantShell, new String[] { |
| "user", "assign", StringUtils.repeat('a', 100), "--tenant=dev"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "accessId length (104) exceeds the maximum length " |
| + "allowed (100)\n", true); |
| |
| // Get user info |
| // Equivalent to `ozone tenant user info bob` |
| executeHA(tenantShell, new String[] { |
| "user", "info", "bob"}); |
| checkOutput(out, "User 'bob' is assigned to:\n" |
| + "- Tenant 'research' with accessId 'research$bob'\n" |
| + "- Tenant 'finance' with accessId 'finance$bob'\n" |
| + "- Tenant 'dev' with accessId 'dev$bob'\n", true); |
| checkOutput(err, "", true); |
| |
| // Assign admin |
| executeHA(tenantShell, new String[] { |
| "user", "assign-admin", "dev$bob", "--tenant=dev", "--delegated"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] { |
| "user", "info", "bob"}); |
| checkOutput(out, "Tenant 'dev' delegated admin with accessId", false); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] { |
| "user", "info", "--json", "bob"}); |
| checkOutput(out, "{\n" + " \"user\": \"bob\",\n" + " \"tenants\": [\n" |
| + " {\n" + " \"accessId\": \"research$bob\",\n" |
| + " \"tenantId\": \"research\",\n" + " \"isAdmin\": false,\n" |
| + " \"isDelegatedAdmin\": false\n" + " },\n" + " {\n" |
| + " \"accessId\": \"finance$bob\",\n" |
| + " \"tenantId\": \"finance\",\n" + " \"isAdmin\": false,\n" |
| + " \"isDelegatedAdmin\": false\n" + " },\n" + " {\n" |
| + " \"accessId\": \"dev$bob\",\n" |
| + " \"tenantId\": \"dev\",\n" + " \"isAdmin\": true,\n" |
| + " \"isDelegatedAdmin\": true\n" + " }\n" + " ]\n" + "}\n", |
| true, true); |
| checkOutput(err, "", true); |
| |
| // Revoke admin |
| executeHA(tenantShell, new String[] { |
| "user", "revoke-admin", "dev$bob", "--tenant=dev"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] { |
| "user", "info", "bob"}); |
| checkOutput(out, "User 'bob' is assigned to:\n" |
| + "- Tenant 'research' with accessId 'research$bob'\n" |
| + "- Tenant 'finance' with accessId 'finance$bob'\n" |
| + "- Tenant 'dev' with accessId 'dev$bob'\n", true); |
| checkOutput(err, "", true); |
| |
| // Revoke user accessId |
| executeHA(tenantShell, new String[] { |
| "user", "revoke", "research$bob"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] { |
| "user", "info", "bob"}); |
| checkOutput(out, "User 'bob' is assigned to:\n" |
| + "- Tenant 'finance' with accessId 'finance$bob'\n" |
| + "- Tenant 'dev' with accessId 'dev$bob'\n", true); |
| checkOutput(err, "", true); |
| |
| // Assign user again |
| executeHA(tenantShell, new String[] { |
| "user", "assign", "bob", "--tenant=research"}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID='research$bob'\n" |
| + "export AWS_SECRET_ACCESS_KEY='", false); |
| checkOutput(err, "", true); |
| |
| // Attempt to assign the user to the tenant again |
| executeHA(tenantShell, new String[] { |
| "user", "assign", "bob", "--tenant=research", |
| "--accessId=research$bob"}); |
| checkOutput(out, "", false); |
| checkOutput(err, "accessId 'research$bob' already exists!\n", true); |
| |
| // Attempt to assign the user to the tenant with a custom accessId |
| executeHA(tenantShell, new String[] { |
| "user", "assign", "bob", "--tenant=research", |
| "--accessId=research$bob42"}); |
| checkOutput(out, "", false); |
| // HDDS-6366: Disallow specifying custom accessId. |
| checkOutput(err, "Invalid accessId 'research$bob42'. " |
| + "Specifying a custom access ID disallowed. " |
| + "Expected accessId to be assigned is 'research$bob'\n", true); |
| |
| executeHA(tenantShell, new String[] {"list"}); |
| checkOutput(out, "dev\nfinance\nresearch\n", true); |
| checkOutput(err, "", true); |
| |
| // Clean up |
| executeHA(tenantShell, new String[] { |
| "user", "revoke", "research$bob"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] {"delete", "research"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "Deleted tenant 'research'.\n", false); |
| deleteVolume("research"); |
| |
| executeHA(tenantShell, new String[] { |
| "user", "revoke", "finance$bob"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] {"list"}); |
| checkOutput(out, "dev\nfinance\n", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] {"delete", "finance"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "Deleted tenant 'finance'.\n", false); |
| deleteVolume("finance"); |
| |
| executeHA(tenantShell, new String[] {"list"}); |
| checkOutput(out, "dev\n", true); |
| checkOutput(err, "", true); |
| |
| // Attempt to delete tenant with accessIds still assigned to it, should fail |
| int exitCode = executeHA(tenantShell, new String[] {"delete", "dev"}); |
| Assert.assertTrue("Tenant delete should fail!", exitCode != 0); |
| checkOutput(out, "", true); |
| checkOutput(err, "Tenant 'dev' is not empty. All accessIds associated " |
| + "to this tenant must be revoked before the tenant can be deleted. " |
| + "See `ozone tenant user revoke`\n", true); |
| |
| // Delete dev volume should fail because the volume reference count > 0L |
| exitCode = execute(ozoneSh, new String[] {"volume", "delete", "dev"}); |
| Assert.assertTrue("Volume delete should fail!", exitCode != 0); |
| checkOutput(out, "", true); |
| checkOutput(err, "Volume reference count is not zero (1). " |
| + "Ozone features are enabled on this volume. " |
| + "Try `ozone tenant delete <tenantId>` first.\n", true); |
| |
| executeHA(tenantShell, new String[] {"list"}); |
| checkOutput(out, "dev\n", true); |
| checkOutput(err, "", true); |
| |
| // Revoke accessId first |
| executeHA(tenantShell, new String[] { |
| "user", "revoke", "dev$bob"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| // Then delete tenant, should succeed |
| executeHA(tenantShell, new String[] {"--verbose", "delete", "dev"}); |
| checkOutput(out, "{\n" + " \"tenantId\": \"dev\",\n" |
| + " \"volumeName\": \"dev\",\n" + " \"volumeRefCount\": 0\n" + "}\n", |
| true, true); |
| checkOutput(err, "Deleted tenant 'dev'.\n", false); |
| deleteVolume("dev"); |
| |
| // Sanity check: tenant list should be empty |
| executeHA(tenantShell, new String[] {"list"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| } |
| |
| @Test |
| public void testListTenantUsers() throws IOException { |
| executeHA(tenantShell, new String[] {"--verbose", "create", "tenant1"}); |
| checkOutput(out, "{\n" + |
| " \"tenantId\": \"tenant1\"\n" + "}\n", true, true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] { |
| "--verbose", "user", "assign", "alice", "--tenant=tenant1"}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID='tenant1$alice'\n" |
| + "export AWS_SECRET_ACCESS_KEY='", false); |
| checkOutput(err, "Assigned 'alice' to 'tenant1'" + |
| " with accessId 'tenant1$alice'.\n", true); |
| |
| executeHA(tenantShell, new String[] { |
| "user", "assign", "bob", "--tenant=tenant1"}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID='tenant1$bob'\n" |
| + "export AWS_SECRET_ACCESS_KEY='", false); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] { |
| "user", "list", "tenant1"}); |
| checkOutput(out, "- User 'bob' with accessId 'tenant1$bob'\n" + |
| "- User 'alice' with accessId 'tenant1$alice'\n", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] { |
| "user", "list", "tenant1", "--json"}); |
| checkOutput(out, "[\n" + " {\n" + " \"user\": \"bob\",\n" |
| + " \"accessId\": \"tenant1$bob\"\n" + " },\n" + " {\n" |
| + " \"user\": \"alice\",\n" + " \"accessId\": \"tenant1$alice\"\n" |
| + " }\n" + "]\n", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] { |
| "user", "list", "tenant1", "--prefix=b"}); |
| checkOutput(out, "- User 'bob' with accessId " + |
| "'tenant1$bob'\n", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] { |
| "user", "list", "tenant1", "--prefix=b", "--json"}); |
| checkOutput(out, "[\n" + " {\n" + " \"user\": \"bob\",\n" |
| + " \"accessId\": \"tenant1$bob\"\n" + " }\n" + "]\n", true); |
| checkOutput(err, "", true); |
| |
| int exitCode = executeHA(tenantShell, new String[] { |
| "user", "list", "unknown"}); |
| Assert.assertTrue("Expected non-zero exit code", exitCode != 0); |
| checkOutput(out, "", true); |
| checkOutput(err, "Tenant 'unknown' doesn't exist.\n", true); |
| |
| // Clean up |
| executeHA(tenantShell, new String[] { |
| "user", "revoke", "tenant1$alice"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] { |
| "--verbose", "user", "revoke", "tenant1$bob"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "Revoked accessId", false); |
| |
| executeHA(tenantShell, new String[] {"delete", "tenant1"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "Deleted tenant 'tenant1'.\n", false); |
| deleteVolume("tenant1"); |
| |
| // Sanity check: tenant list should be empty |
| executeHA(tenantShell, new String[] {"list"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| } |
| |
| @Test |
| public void testTenantSetSecret() throws IOException, InterruptedException { |
| |
| final String tenantName = "tenant-test-set-secret"; |
| |
| // Create test tenant |
| executeHA(tenantShell, new String[] {"create", tenantName}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| // Set secret for non-existent accessId. Expect failure |
| executeHA(tenantShell, new String[] { |
| "user", "set-secret", tenantName + "$alice", "--secret=somesecret0"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "accessId '" + tenantName + "$alice' not found.\n", true); |
| |
| // Assign a user to the tenant so that we have an accessId entry |
| executeHA(tenantShell, new String[] { |
| "user", "assign", "alice", "--tenant=" + tenantName}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID='" + tenantName + "$alice'\n" + |
| "export AWS_SECRET_ACCESS_KEY='", false); |
| checkOutput(err, "", true); |
| |
| // Set secret as OM admin should succeed |
| executeHA(tenantShell, new String[] { |
| "user", "setsecret", tenantName + "$alice", |
| "--secret=somesecret1"}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID='" + tenantName + "$alice'\n" + |
| "export AWS_SECRET_ACCESS_KEY='somesecret1'\n", true); |
| checkOutput(err, "", true); |
| |
| // Set empty secret key should fail |
| int exitCode = executeHA(tenantShell, new String[] { |
| "user", "setsecret", tenantName + "$alice", |
| "--secret=short"}); |
| Assert.assertTrue("Expected non-zero exit code", exitCode != 0); |
| checkOutput(out, "", true); |
| checkOutput(err, "Secret key length should be at least 8 characters\n", |
| true); |
| |
| // Get secret should still give the previous secret key |
| executeHA(tenantShell, new String[] { |
| "user", "getsecret", tenantName + "$alice"}); |
| checkOutput(out, "somesecret1", false); |
| checkOutput(err, "", true); |
| |
| // Set secret as alice should succeed |
| final UserGroupInformation ugiAlice = UserGroupInformation |
| .createUserForTesting("alice", new String[] {"usergroup"}); |
| |
| ugiAlice.doAs((PrivilegedExceptionAction<Void>) () -> { |
| executeHA(tenantShell, new String[] { |
| "user", "setsecret", tenantName + "$alice", |
| "--secret=somesecret2"}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID='" + tenantName + "$alice'\n" + |
| "export AWS_SECRET_ACCESS_KEY='somesecret2'\n", true); |
| checkOutput(err, "", true); |
| return null; |
| }); |
| |
| // Set secret as bob should fail |
| executeHA(tenantShell, new String[] { |
| "user", "assign", "bob", "--tenant=" + tenantName}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID='" + tenantName + "$bob'\n" + |
| "export AWS_SECRET_ACCESS_KEY='", false); |
| checkOutput(err, "", true); |
| |
| final UserGroupInformation ugiBob = UserGroupInformation |
| .createUserForTesting("bob", new String[] {"usergroup"}); |
| |
| ugiBob.doAs((PrivilegedExceptionAction<Void>) () -> { |
| int exitC = executeHA(tenantShell, new String[] { |
| "user", "setsecret", tenantName + "$alice", |
| "--secret=somesecret2"}); |
| Assert.assertTrue("Should return non-zero exit code!", exitC != 0); |
| checkOutput(out, "", true); |
| checkOutput(err, "Requested accessId 'tenant-test-set-secret$alice'" |
| + " doesn't belong to current user 'bob', nor does current user" |
| + " have Ozone or tenant administrator privilege\n", true); |
| return null; |
| }); |
| |
| // Once we make bob an admin of this tenant (non-delegated admin permission |
| // is sufficient in this case), set secret should succeed |
| executeHA(tenantShell, new String[] {"user", "assign-admin", |
| tenantName + "$" + ugiBob.getShortUserName(), |
| "--tenant=" + tenantName, "--delegated=false"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| // Set secret should succeed now |
| ugiBob.doAs((PrivilegedExceptionAction<Void>) () -> { |
| executeHA(tenantShell, new String[] { |
| "user", "setsecret", tenantName + "$alice", |
| "--secret=somesecret2"}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID='" + tenantName + "$alice'\n" + |
| "export AWS_SECRET_ACCESS_KEY='somesecret2'\n", true); |
| checkOutput(err, "", true); |
| return null; |
| }); |
| |
| // Clean up |
| executeHA(tenantShell, new String[] {"user", "revoke-admin", |
| tenantName + "$" + ugiBob.getShortUserName()}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] { |
| "user", "revoke", tenantName + "$bob"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] { |
| "user", "revoke", tenantName + "$alice"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] {"delete", tenantName}); |
| checkOutput(out, "", true); |
| checkOutput(err, "Deleted tenant '" + tenantName + "'.\n", false); |
| deleteVolume(tenantName); |
| |
| // Sanity check: tenant list should be empty |
| executeHA(tenantShell, new String[] {"list"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| } |
| |
| @Test |
| @SuppressWarnings("methodlength") |
| public void testTenantAdminOperations() |
| throws IOException, InterruptedException { |
| |
| final String tenantName = "tenant-test-tenant-admin-ops"; |
| final UserGroupInformation ugiAlice = UserGroupInformation |
| .createUserForTesting("alice", new String[] {"usergroup"}); |
| final UserGroupInformation ugiBob = UserGroupInformation |
| .createUserForTesting("bob", new String[] {"usergroup"}); |
| |
| // Preparation |
| |
| // Create test tenant |
| executeHA(tenantShell, new String[] {"create", tenantName}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| // Assign alice and bob as tenant users |
| executeHA(tenantShell, new String[] { |
| "user", "assign", "alice", "--tenant=" + tenantName}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID='" + tenantName + "$alice'\n" + |
| "export AWS_SECRET_ACCESS_KEY='", false); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] { |
| "user", "assign", "bob", "--tenant=" + tenantName}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID='" + tenantName + "$bob'\n" + |
| "export AWS_SECRET_ACCESS_KEY='", false); |
| checkOutput(err, "", true); |
| |
| // Make alice a delegated tenant admin |
| executeHA(tenantShell, new String[] {"user", "assign-admin", |
| tenantName + "$" + ugiAlice.getShortUserName(), |
| "--tenant=" + tenantName, "--delegated=true"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| // Make bob a non-delegated tenant admin |
| executeHA(tenantShell, new String[] {"user", "assign-admin", |
| tenantName + "$" + ugiBob.getShortUserName(), |
| "--tenant=" + tenantName, "--delegated=false"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| // Start test matrix |
| |
| // As a tenant delegated admin, alice can: |
| // - Assign and revoke user accessIds |
| // - Assign and revoke tenant admins |
| // - Set secret |
| ugiAlice.doAs((PrivilegedExceptionAction<Void>) () -> { |
| // Assign carol as a new tenant user |
| executeHA(tenantShell, new String[] { |
| "user", "assign", "carol", "--tenant=" + tenantName}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID='" + tenantName + "$carol'\n" |
| + "export AWS_SECRET_ACCESS_KEY='", false); |
| checkOutput(err, "", true); |
| |
| // Set secret should work |
| executeHA(tenantShell, new String[] { |
| "user", "setsecret", tenantName + "$alice", |
| "--secret=somesecret2"}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID='" + tenantName + "$alice'\n" + |
| "export AWS_SECRET_ACCESS_KEY='somesecret2'\n", true); |
| checkOutput(err, "", true); |
| |
| // Make carol a tenant delegated tenant admin |
| executeHA(tenantShell, new String[] {"user", "assign-admin", |
| tenantName + "$carol", |
| "--tenant=" + tenantName, "--delegated=true"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| // Revoke carol's tenant admin privilege |
| executeHA(tenantShell, new String[] {"user", "revoke-admin", |
| tenantName + "$carol"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| // Make carol a tenant non-delegated tenant admin |
| executeHA(tenantShell, new String[] {"user", "assign-admin", |
| tenantName + "$carol", |
| "--tenant=" + tenantName, "--delegated=false"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| // Revoke carol's tenant admin privilege |
| executeHA(tenantShell, new String[] {"user", "revoke-admin", |
| tenantName + "$carol"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| // Revoke carol's accessId from this tenant |
| executeHA(tenantShell, new String[] { |
| "user", "revoke", tenantName + "$carol"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| return null; |
| }); |
| |
| // As a tenant non-delegated admin, bob can: |
| // - Assign and revoke user accessIds |
| // - Set secret |
| // |
| // But bob can't: |
| // - Assign and revoke tenant admins |
| ugiBob.doAs((PrivilegedExceptionAction<Void>) () -> { |
| // Assign carol as a new tenant user |
| executeHA(tenantShell, new String[] { |
| "user", "assign", "carol", "--tenant=" + tenantName}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID='" + tenantName + "$carol'\n" |
| + "export AWS_SECRET_ACCESS_KEY='", false); |
| checkOutput(err, "", true); |
| |
| // Set secret should work, even for a non-delegated admin |
| executeHA(tenantShell, new String[] { |
| "user", "setsecret", tenantName + "$alice", |
| "--secret=somesecret2"}); |
| checkOutput(out, "export AWS_ACCESS_KEY_ID='" + tenantName + "$alice'\n" + |
| "export AWS_SECRET_ACCESS_KEY='somesecret2'\n", true); |
| checkOutput(err, "", true); |
| |
| // Attempt to make carol a tenant delegated tenant admin, should fail |
| executeHA(tenantShell, new String[] {"user", "assign-admin", |
| tenantName + "$carol", |
| "--tenant=" + tenantName, "--delegated=true"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "User 'bob' is neither an Ozone admin " |
| + "nor a delegated admin of tenant", false); |
| |
| // Attempt to make carol a tenant non-delegated tenant admin, should fail |
| executeHA(tenantShell, new String[] {"user", "assign-admin", |
| tenantName + "$carol", |
| "--tenant=" + tenantName, "--delegated=false"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "User 'bob' is neither an Ozone admin " |
| + "nor a delegated admin of tenant", false); |
| |
| // Attempt to revoke tenant admin, should fail at the permission check |
| executeHA(tenantShell, new String[] {"user", "revoke-admin", |
| tenantName + "$carol"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "User 'bob' is neither an Ozone admin " |
| + "nor a delegated admin of tenant", false); |
| |
| // Revoke carol's accessId from this tenant |
| executeHA(tenantShell, new String[] { |
| "user", "revoke", tenantName + "$carol"}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| return null; |
| }); |
| |
| // Clean up |
| executeHA(tenantShell, new String[] {"user", "revoke-admin", |
| tenantName + "$" + ugiAlice.getShortUserName()}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] { |
| "user", "revoke", tenantName + "$" + ugiAlice.getShortUserName()}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] {"user", "revoke-admin", |
| tenantName + "$" + ugiBob.getShortUserName()}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] { |
| "user", "revoke", tenantName + "$" + ugiBob.getShortUserName()}); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| executeHA(tenantShell, new String[] {"delete", tenantName}); |
| checkOutput(out, "", true); |
| checkOutput(err, "Deleted tenant '" + tenantName + "'.\n", false); |
| deleteVolume(tenantName); |
| } |
| |
| @Test |
| public void testCreateTenantOnExistingVolumeShouldFail() throws IOException { |
| final String testVolume = "existing-volume-1"; |
| int exitC = execute(ozoneSh, new String[] {"volume", "create", testVolume}); |
| // Volume create should succeed |
| Assert.assertEquals(0, exitC); |
| checkOutput(out, "", true); |
| checkOutput(err, "", true); |
| |
| // Try to create tenant on the same volume, should fail |
| executeHA(tenantShell, new String[] {"create", testVolume}); |
| checkOutput(out, "", true); |
| checkOutput(err, "Volume already exists\n", true); |
| |
| // Clean up |
| deleteVolume(testVolume); |
| } |
| } |