This is an automated email from the ASF dual-hosted git repository. dlmarion pushed a commit to branch 2.1 in repository https://gitbox.apache.org/repos/asf/accumulo.git
The following commit(s) were added to refs/heads/2.1 by this push: new f02d6a880e Fix src and dest namespace handling in clone table operation (#5334) f02d6a880e is described below commit f02d6a880ed822820881f2fdf7d0827fcbd8db52 Author: Dave Marion <dlmar...@apache.org> AuthorDate: Thu Apr 3 07:33:38 2025 -0400 Fix src and dest namespace handling in clone table operation (#5334) Updates clone table fate operation to use both the source and dest namespaces when checking permissions. Closes #5324 --- .../accumulo/manager/FateServiceHandler.java | 19 ++- .../accumulo/manager/tableOps/clone/CloneInfo.java | 79 ++++++++++-- .../manager/tableOps/clone/CloneMetadata.java | 11 +- .../manager/tableOps/clone/ClonePermissions.java | 8 +- .../manager/tableOps/clone/CloneTable.java | 28 ++--- .../manager/tableOps/clone/CloneZookeeper.java | 41 +++---- .../manager/tableOps/clone/FinishCloneTable.java | 25 ++-- .../accumulo/test/functional/CloneTestIT.java | 132 +++++++++++++++++++++ 8 files changed, 268 insertions(+), 75 deletions(-) diff --git a/server/manager/src/main/java/org/apache/accumulo/manager/FateServiceHandler.java b/server/manager/src/main/java/org/apache/accumulo/manager/FateServiceHandler.java index 0444099675..c9ecd1d4d8 100644 --- a/server/manager/src/main/java/org/apache/accumulo/manager/FateServiceHandler.java +++ b/server/manager/src/main/java/org/apache/accumulo/manager/FateServiceHandler.java @@ -286,12 +286,21 @@ class FateServiceHandler implements FateService.Iface { keepOffline = Boolean.parseBoolean(ByteBufferUtil.toString(arguments.get(2))); } + NamespaceId srcNamespaceId; + try { + srcNamespaceId = manager.getContext().getNamespaceId(srcTableId); + } catch (TableNotFoundException e) { + // could happen if the table was deleted while processing this request + throw new ThriftTableOperationException(srcTableId.canonical(), null, tableOp, + TableOperationExceptionType.NOTFOUND, ""); + } + NamespaceId namespaceId; try { namespaceId = Namespaces.getNamespaceId(manager.getContext(), TableNameUtil.qualify(tableName).getFirst()); } catch (NamespaceNotFoundException e) { - // shouldn't happen, but possible once cloning between namespaces is supported + // dest namespace does not exist yet, needs to be created throw new ThriftTableOperationException(null, tableName, tableOp, TableOperationExceptionType.NAMESPACE_NOTFOUND, ""); } @@ -299,7 +308,7 @@ class FateServiceHandler implements FateService.Iface { final boolean canCloneTable; try { canCloneTable = - manager.security.canCloneTable(c, srcTableId, tableName, namespaceId, namespaceId); + manager.security.canCloneTable(c, srcTableId, tableName, namespaceId, srcNamespaceId); } catch (ThriftSecurityException e) { throwIfTableMissingSecurityException(e, srcTableId, null, TableOperation.CLONE); throw e; @@ -336,9 +345,9 @@ class FateServiceHandler implements FateService.Iface { goalMessage += " and keep offline."; } - manager.fate().seedTransaction( - op.toString(), opid, new TraceRepo<>(new CloneTable(c.getPrincipal(), namespaceId, - srcTableId, tableName, propertiesToSet, propertiesToExclude, keepOffline)), + manager.fate().seedTransaction(op.toString(), opid, + new TraceRepo<>(new CloneTable(c.getPrincipal(), srcNamespaceId, srcTableId, + namespaceId, tableName, propertiesToSet, propertiesToExclude, keepOffline)), autoCleanup, goalMessage); break; diff --git a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/CloneInfo.java b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/CloneInfo.java index 676e4d88e7..2a697827ad 100644 --- a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/CloneInfo.java +++ b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/CloneInfo.java @@ -29,14 +29,73 @@ class CloneInfo implements Serializable { private static final long serialVersionUID = 1L; - TableId srcTableId; - String tableName; - TableId tableId; - NamespaceId namespaceId; - NamespaceId srcNamespaceId; - Map<String,String> propertiesToSet; - Set<String> propertiesToExclude; - boolean keepOffline; - - public String user; + private final TableId srcTableId; + private final String tableName; + private TableId tableId; + // TODO: Make final in 3.1 + private NamespaceId namespaceId; + private final NamespaceId srcNamespaceId; + private final Map<String,String> propertiesToSet; + private final Set<String> propertiesToExclude; + private final boolean keepOffline; + private final String user; + + public CloneInfo(NamespaceId srcNamespaceId, TableId srcTableId, NamespaceId dstNamespaceId, + String dstTableName, Map<String,String> propertiesToSet, Set<String> propertiesToExclude, + boolean keepOffline, String user) { + super(); + this.srcNamespaceId = srcNamespaceId; + this.srcTableId = srcTableId; + this.tableName = dstTableName; + this.namespaceId = dstNamespaceId; + this.propertiesToSet = propertiesToSet; + this.propertiesToExclude = propertiesToExclude; + this.keepOffline = keepOffline; + this.user = user; + } + + public TableId getSrcTableId() { + return srcTableId; + } + + public String getTableName() { + return tableName; + } + + public void setTableId(TableId dstTableId) { + this.tableId = dstTableId; + } + + public TableId getTableId() { + return tableId; + } + + public NamespaceId getNamespaceId() { + return namespaceId; + } + + public void setNamespaceId(NamespaceId nid) { + this.namespaceId = nid; + } + + public NamespaceId getSrcNamespaceId() { + return srcNamespaceId; + } + + public Map<String,String> getPropertiesToSet() { + return propertiesToSet; + } + + public Set<String> getPropertiesToExclude() { + return propertiesToExclude; + } + + public boolean isKeepOffline() { + return keepOffline; + } + + public String getUser() { + return user; + } + } diff --git a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/CloneMetadata.java b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/CloneMetadata.java index 0616edb1eb..82f73d0383 100644 --- a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/CloneMetadata.java +++ b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/CloneMetadata.java @@ -41,19 +41,20 @@ class CloneMetadata extends ManagerRepo { @Override public Repo<Manager> call(long tid, Manager environment) throws Exception { LoggerFactory.getLogger(CloneMetadata.class) - .info(String.format("Cloning %s with tableId %s from srcTableId %s", cloneInfo.tableName, - cloneInfo.tableId, cloneInfo.srcTableId)); + .info(String.format("Cloning %s with tableId %s from srcTableId %s", + cloneInfo.getTableName(), cloneInfo.getTableId(), cloneInfo.getSrcTableId())); // need to clear out any metadata entries for tableId just in case this // died before and is executing again - MetadataTableUtil.deleteTable(cloneInfo.tableId, false, environment.getContext(), + MetadataTableUtil.deleteTable(cloneInfo.getTableId(), false, environment.getContext(), environment.getManagerLock()); - MetadataTableUtil.cloneTable(environment.getContext(), cloneInfo.srcTableId, cloneInfo.tableId); + MetadataTableUtil.cloneTable(environment.getContext(), cloneInfo.getSrcTableId(), + cloneInfo.getTableId()); return new FinishCloneTable(cloneInfo); } @Override public void undo(long tid, Manager environment) throws Exception { - MetadataTableUtil.deleteTable(cloneInfo.tableId, false, environment.getContext(), + MetadataTableUtil.deleteTable(cloneInfo.getTableId(), false, environment.getContext(), environment.getManagerLock()); } diff --git a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/ClonePermissions.java b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/ClonePermissions.java index e448e289ef..ac1432c3d2 100644 --- a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/ClonePermissions.java +++ b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/ClonePermissions.java @@ -50,8 +50,8 @@ class ClonePermissions extends ManagerRepo { for (TablePermission permission : TablePermission.values()) { try { environment.getContext().getSecurityOperation().grantTablePermission( - environment.getContext().rpcCreds(), cloneInfo.user, cloneInfo.tableId, - cloneInfo.tableName, permission, cloneInfo.namespaceId); + environment.getContext().rpcCreds(), cloneInfo.getUser(), cloneInfo.getTableId(), + cloneInfo.getTableName(), permission, cloneInfo.getNamespaceId()); } catch (ThriftSecurityException e) { LoggerFactory.getLogger(ClonePermissions.class).error("{}", e.getMessage(), e); throw e; @@ -64,7 +64,7 @@ class ClonePermissions extends ManagerRepo { try { return new CloneZookeeper(cloneInfo, environment.getContext()); } catch (NamespaceNotFoundException e) { - throw new AcceptableThriftTableOperationException(null, cloneInfo.tableName, + throw new AcceptableThriftTableOperationException(null, cloneInfo.getTableName(), TableOperation.CLONE, TableOperationExceptionType.NAMESPACE_NOTFOUND, "Namespace for target table not found"); } @@ -73,6 +73,6 @@ class ClonePermissions extends ManagerRepo { @Override public void undo(long tid, Manager environment) throws Exception { environment.getContext().getSecurityOperation().deleteTable(environment.getContext().rpcCreds(), - cloneInfo.tableId, cloneInfo.namespaceId); + cloneInfo.getTableId(), cloneInfo.getNamespaceId()); } } diff --git a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/CloneTable.java b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/CloneTable.java index 0a03526c38..c5587ee692 100644 --- a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/CloneTable.java +++ b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/CloneTable.java @@ -34,23 +34,18 @@ public class CloneTable extends ManagerRepo { private static final long serialVersionUID = 1L; private final CloneInfo cloneInfo; - public CloneTable(String user, NamespaceId namespaceId, TableId srcTableId, String tableName, - Map<String,String> propertiesToSet, Set<String> propertiesToExclude, boolean keepOffline) { - cloneInfo = new CloneInfo(); - cloneInfo.user = user; - cloneInfo.srcTableId = srcTableId; - cloneInfo.tableName = tableName; - cloneInfo.propertiesToExclude = propertiesToExclude; - cloneInfo.propertiesToSet = propertiesToSet; - cloneInfo.srcNamespaceId = namespaceId; - cloneInfo.keepOffline = keepOffline; + public CloneTable(String user, NamespaceId srcNamespaceId, TableId srcTableId, + NamespaceId namespaceId, String tableName, Map<String,String> propertiesToSet, + Set<String> propertiesToExclude, boolean keepOffline) { + cloneInfo = new CloneInfo(srcNamespaceId, srcTableId, namespaceId, tableName, propertiesToSet, + propertiesToExclude, keepOffline, user); } @Override public long isReady(long tid, Manager environment) throws Exception { - long val = Utils.reserveNamespace(environment, cloneInfo.srcNamespaceId, tid, false, true, + long val = Utils.reserveNamespace(environment, cloneInfo.getNamespaceId(), tid, false, true, TableOperation.CLONE); - val += Utils.reserveTable(environment, cloneInfo.srcTableId, tid, false, true, + val += Utils.reserveTable(environment, cloneInfo.getSrcTableId(), tid, false, true, TableOperation.CLONE); return val; } @@ -60,9 +55,8 @@ public class CloneTable extends ManagerRepo { Utils.getIdLock().lock(); try { - cloneInfo.tableId = - Utils.getNextId(cloneInfo.tableName, environment.getContext(), TableId::of); - + cloneInfo.setTableId( + Utils.getNextId(cloneInfo.getTableName(), environment.getContext(), TableId::of)); return new ClonePermissions(cloneInfo); } finally { Utils.getIdLock().unlock(); @@ -71,8 +65,8 @@ public class CloneTable extends ManagerRepo { @Override public void undo(long tid, Manager environment) { - Utils.unreserveNamespace(environment, cloneInfo.srcNamespaceId, tid, false); - Utils.unreserveTable(environment, cloneInfo.srcTableId, tid, false); + Utils.unreserveNamespace(environment, cloneInfo.getNamespaceId(), tid, false); + Utils.unreserveTable(environment, cloneInfo.getSrcTableId(), tid, false); } } diff --git a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/CloneZookeeper.java b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/CloneZookeeper.java index 77ef41df12..fe9c9fcd13 100644 --- a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/CloneZookeeper.java +++ b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/CloneZookeeper.java @@ -37,20 +37,24 @@ class CloneZookeeper extends ManagerRepo { public CloneZookeeper(CloneInfo cloneInfo, ClientContext context) throws NamespaceNotFoundException { this.cloneInfo = cloneInfo; - this.cloneInfo.namespaceId = Namespaces.getNamespaceId(context, - TableNameUtil.qualify(this.cloneInfo.tableName).getFirst()); + if (this.cloneInfo.getNamespaceId() == null) { + // Prior to 2.1.4 the namespaceId was calculated in this + // step and set on the cloneInfo object here. If for some + // reason we are processing a pre-2.1.3 CloneTable operation, + // then we need to continue to set this here as it will be + // null in the deserialized CloneInfo object. + // + // TODO: Remove this check in 3.1 as Fate operations + // need to be cleaned up before a major upgrade. + this.cloneInfo.setNamespaceId(Namespaces.getNamespaceId(context, + TableNameUtil.qualify(this.cloneInfo.getTableName()).getFirst())); + } } @Override public long isReady(long tid, Manager environment) throws Exception { - long val = 0; - if (!cloneInfo.srcNamespaceId.equals(cloneInfo.namespaceId)) { - val += Utils.reserveNamespace(environment, cloneInfo.namespaceId, tid, false, true, - TableOperation.CLONE); - } - val += - Utils.reserveTable(environment, cloneInfo.tableId, tid, true, false, TableOperation.CLONE); - return val; + return Utils.reserveTable(environment, cloneInfo.getTableId(), tid, true, false, + TableOperation.CLONE); } @Override @@ -59,12 +63,12 @@ class CloneZookeeper extends ManagerRepo { try { // write tableName & tableId to zookeeper - Utils.checkTableNameDoesNotExist(environment.getContext(), cloneInfo.tableName, - cloneInfo.namespaceId, cloneInfo.tableId, TableOperation.CLONE); + Utils.checkTableNameDoesNotExist(environment.getContext(), cloneInfo.getTableName(), + cloneInfo.getNamespaceId(), cloneInfo.getTableId(), TableOperation.CLONE); - environment.getTableManager().cloneTable(cloneInfo.srcTableId, cloneInfo.tableId, - cloneInfo.tableName, cloneInfo.namespaceId, cloneInfo.propertiesToSet, - cloneInfo.propertiesToExclude); + environment.getTableManager().cloneTable(cloneInfo.getSrcTableId(), cloneInfo.getTableId(), + cloneInfo.getTableName(), cloneInfo.getNamespaceId(), cloneInfo.getPropertiesToSet(), + cloneInfo.getPropertiesToExclude()); environment.getContext().clearTableListCache(); return new CloneMetadata(cloneInfo); @@ -75,11 +79,8 @@ class CloneZookeeper extends ManagerRepo { @Override public void undo(long tid, Manager environment) throws Exception { - environment.getTableManager().removeTable(cloneInfo.tableId); - if (!cloneInfo.srcNamespaceId.equals(cloneInfo.namespaceId)) { - Utils.unreserveNamespace(environment, cloneInfo.namespaceId, tid, false); - } - Utils.unreserveTable(environment, cloneInfo.tableId, tid, true); + environment.getTableManager().removeTable(cloneInfo.getTableId()); + Utils.unreserveTable(environment, cloneInfo.getTableId(), tid, true); environment.getContext().clearTableListCache(); } diff --git a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/FinishCloneTable.java b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/FinishCloneTable.java index 8eed682359..5fedbcfed7 100644 --- a/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/FinishCloneTable.java +++ b/server/manager/src/main/java/org/apache/accumulo/manager/tableOps/clone/FinishCloneTable.java @@ -50,26 +50,23 @@ class FinishCloneTable extends ManagerRepo { // that are not used... tablet will create directories as needed final EnumSet<TableState> expectedCurrStates = EnumSet.of(TableState.NEW); - if (cloneInfo.keepOffline) { - environment.getTableManager().transitionTableState(cloneInfo.tableId, TableState.OFFLINE, + if (cloneInfo.isKeepOffline()) { + environment.getTableManager().transitionTableState(cloneInfo.getTableId(), TableState.OFFLINE, expectedCurrStates); } else { - environment.getTableManager().transitionTableState(cloneInfo.tableId, TableState.ONLINE, + environment.getTableManager().transitionTableState(cloneInfo.getTableId(), TableState.ONLINE, expectedCurrStates); } + Utils.unreserveTable(environment, cloneInfo.getTableId(), tid, true); + Utils.unreserveNamespace(environment, cloneInfo.getNamespaceId(), tid, false); + Utils.unreserveTable(environment, cloneInfo.getSrcTableId(), tid, false); - Utils.unreserveNamespace(environment, cloneInfo.srcNamespaceId, tid, false); - if (!cloneInfo.srcNamespaceId.equals(cloneInfo.namespaceId)) { - Utils.unreserveNamespace(environment, cloneInfo.namespaceId, tid, false); - } - Utils.unreserveTable(environment, cloneInfo.srcTableId, tid, false); - Utils.unreserveTable(environment, cloneInfo.tableId, tid, true); - - environment.getEventCoordinator().event("Cloned table %s from %s", cloneInfo.tableName, - cloneInfo.srcTableId); + environment.getEventCoordinator().event("Cloned table %s from %s", cloneInfo.getTableName(), + cloneInfo.getSrcTableId()); - LoggerFactory.getLogger(FinishCloneTable.class).debug("Cloned table " + cloneInfo.srcTableId - + " " + cloneInfo.tableId + " " + cloneInfo.tableName); + LoggerFactory.getLogger(FinishCloneTable.class) + .debug("Cloned table " + cloneInfo.getSrcTableId() + " " + cloneInfo.getTableId() + " " + + cloneInfo.getTableName()); return null; } diff --git a/test/src/main/java/org/apache/accumulo/test/functional/CloneTestIT.java b/test/src/main/java/org/apache/accumulo/test/functional/CloneTestIT.java index 6390a2ca1c..7963043049 100644 --- a/test/src/main/java/org/apache/accumulo/test/functional/CloneTestIT.java +++ b/test/src/main/java/org/apache/accumulo/test/functional/CloneTestIT.java @@ -42,12 +42,15 @@ import org.apache.accumulo.cluster.AccumuloCluster; import org.apache.accumulo.core.client.Accumulo; import org.apache.accumulo.core.client.AccumuloClient; import org.apache.accumulo.core.client.AccumuloException; +import org.apache.accumulo.core.client.AccumuloSecurityException; import org.apache.accumulo.core.client.BatchWriter; import org.apache.accumulo.core.client.Scanner; +import org.apache.accumulo.core.client.TableExistsException; import org.apache.accumulo.core.client.TableNotFoundException; import org.apache.accumulo.core.client.admin.CloneConfiguration; import org.apache.accumulo.core.client.admin.DiskUsage; import org.apache.accumulo.core.client.admin.NewTableConfiguration; +import org.apache.accumulo.core.client.security.tokens.PasswordToken; import org.apache.accumulo.core.clientImpl.ClientContext; import org.apache.accumulo.core.conf.Property; import org.apache.accumulo.core.data.Key; @@ -61,6 +64,8 @@ import org.apache.accumulo.core.metadata.RootTable; import org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.DataFileColumnFamily; import org.apache.accumulo.core.metadata.schema.MetadataSchema.TabletsSection.ServerColumnFamily; import org.apache.accumulo.core.security.Authorizations; +import org.apache.accumulo.core.security.NamespacePermission; +import org.apache.accumulo.core.security.TablePermission; import org.apache.accumulo.harness.AccumuloClusterHarness; import org.apache.accumulo.miniclusterImpl.MiniAccumuloClusterImpl; import org.apache.hadoop.fs.FileStatus; @@ -350,4 +355,131 @@ public class CloneTestIT extends AccumuloClusterHarness { "mc1", CloneConfiguration.empty())); } } + + private void baseCloneNamespace(AccumuloClient client, String src, String dest) throws Exception { + writeData(src, client).close(); + // Don't force a flush on the table, let's make sure the + // clone operation does this + client.tableOperations().clone(src, dest, CloneConfiguration.empty()); + checkData(dest, client); + } + + @Test + public void testCloneSameNamespace() throws Exception { + try (AccumuloClient client = Accumulo.newClient().from(getClientProps()).build()) { + String tableName = getUniqueNames(1)[0]; + client.namespaceOperations().create("old"); + client.tableOperations().create("old." + tableName); + assertThrows(TableExistsException.class, + () -> baseCloneNamespace(client, "old." + tableName, "old." + tableName)); + } + } + + @Test + public void testCloneIntoDiffNamespace() throws Exception { + try (AccumuloClient client = Accumulo.newClient().from(getClientProps()).build()) { + String tableName = getUniqueNames(1)[0]; + client.namespaceOperations().create("old"); + client.tableOperations().create("old." + tableName); + client.namespaceOperations().create("new"); + baseCloneNamespace(client, "old." + tableName, "new." + tableName); + } + } + + @Test + public void testCloneIntoDiffNamespaceTableExists() throws Exception { + try (AccumuloClient client = Accumulo.newClient().from(getClientProps()).build()) { + String tableName = getUniqueNames(1)[0]; + client.namespaceOperations().create("old"); + client.tableOperations().create("old." + tableName); + client.namespaceOperations().create("new"); + client.tableOperations().create("new." + tableName); + assertThrows(TableExistsException.class, + () -> baseCloneNamespace(client, "old." + tableName, "new." + tableName)); + } + } + + @Test + public void testCloneIntoDiffNamespaceDoesntExist() throws Exception { + try (AccumuloClient client = Accumulo.newClient().from(getClientProps()).build()) { + String tableName = getUniqueNames(1)[0]; + client.namespaceOperations().create("old"); + client.tableOperations().create("old." + tableName); + assertThrows(AccumuloException.class, + () -> baseCloneNamespace(client, "old." + tableName, "missing." + tableName)); + } + } + + @Test + public void testCloneIntoAccumuloNamespace() throws Exception { + try (AccumuloClient client = Accumulo.newClient().from(getClientProps()).build()) { + String tableName = getUniqueNames(1)[0]; + client.namespaceOperations().create("old"); + client.tableOperations().create("old." + tableName); + assertThrows(AccumuloException.class, + () -> baseCloneNamespace(client, "old." + tableName, "accumulo." + tableName)); + } + } + + @Test + public void testCloneNamespaceIncorrectPermissions() throws Exception { + final String tableName = getUniqueNames(1)[0]; + final String newUserName = "NEW_USER"; + final String srcNs = "src"; + final String srcTableName = srcNs + "." + tableName; + final String destNs = "dst"; + final String dstTableName = destNs + "." + tableName; + + try (AccumuloClient client = Accumulo.newClient().from(getClientProps()).build()) { + client.namespaceOperations().create(srcNs); + client.tableOperations().create(srcTableName); + client.namespaceOperations().create(destNs); + client.securityOperations().createLocalUser(newUserName, new PasswordToken(newUserName)); + // User needs WRITE or ALTER_TABLE on the src table to flush it as part of the clone operation + client.securityOperations().grantTablePermission(newUserName, srcTableName, + TablePermission.ALTER_TABLE); + client.securityOperations().grantNamespacePermission(newUserName, destNs, + NamespacePermission.READ); + client.securityOperations().grantNamespacePermission(newUserName, destNs, + NamespacePermission.CREATE_TABLE); + } + + try (AccumuloClient client = + Accumulo.newClient().to(getCluster().getInstanceName(), getCluster().getZooKeepers()) + .as(newUserName, newUserName).build()) { + // READ permission is needed on the src table, not the dst namespace + assertThrows(AccumuloSecurityException.class, () -> client.tableOperations() + .clone(srcTableName, dstTableName, CloneConfiguration.empty())); + } + } + + @Test + public void testCloneNamespacePermissions() throws Exception { + final String tableName = getUniqueNames(1)[0]; + final String newUserName = "NEW_USER"; + final String srcNs = "src"; + final String srcTableName = srcNs + "." + tableName; + final String destNs = "dst"; + final String dstTableName = destNs + "." + tableName; + + try (AccumuloClient client = Accumulo.newClient().from(getClientProps()).build()) { + client.namespaceOperations().create(srcNs); + client.tableOperations().create(srcTableName); + client.namespaceOperations().create(destNs); + client.securityOperations().createLocalUser(newUserName, new PasswordToken(newUserName)); + // User needs WRITE or ALTER_TABLE on the src table to flush it as part of the clone operation + client.securityOperations().grantTablePermission(newUserName, srcTableName, + TablePermission.ALTER_TABLE); + client.securityOperations().grantTablePermission(newUserName, srcTableName, + TablePermission.READ); + client.securityOperations().grantNamespacePermission(newUserName, destNs, + NamespacePermission.CREATE_TABLE); + } + + try (AccumuloClient client = + Accumulo.newClient().to(getCluster().getInstanceName(), getCluster().getZooKeepers()) + .as(newUserName, newUserName).build()) { + client.tableOperations().clone(srcTableName, dstTableName, CloneConfiguration.empty()); + } + } }