From 1f92fb5aaf008f140ffd27cd0ae82d9692b9e379 Mon Sep 17 00:00:00 2001 From: meiyi Date: Tue, 11 Jun 2019 19:09:14 +0800 Subject: [PATCH] HBASE-22271 Implement grant/revoke/delete table acls/delete namespace acls in Procedure --- .../org/apache/hadoop/hbase/client/Admin.java | 37 ++- .../hadoop/hbase/client/HBaseAdmin.java | 79 +++-- .../hbase/client/RawAsyncHBaseAdmin.java | 72 +++- .../hbase/procedure2/LockedResourceType.java | 2 +- .../src/main/protobuf/AccessControl.proto | 2 + .../src/main/protobuf/MasterProcedure.proto | 19 +- .../hadoop/hbase/executor/EventType.java | 9 +- .../hadoop/hbase/executor/ExecutorType.java | 3 +- .../hbase/master/MasterRpcServices.java | 33 +- .../hadoop/hbase/master/MasterServices.java | 6 +- .../procedure/AclProcedureInterface.java | 32 ++ .../hbase/master/procedure/AclQueue.java | 35 ++ .../procedure/MasterProcedureScheduler.java | 65 +++- .../hbase/master/procedure/SchemaLocking.java | 18 +- .../hbase/regionserver/HRegionServer.java | 8 +- .../hbase/regionserver/RSRpcServices.java | 21 +- .../regionserver/RegionServerServices.java | 6 +- .../security/access/AccessController.java | 177 ++++------ .../hbase/security/access/AuthManager.java | 40 ++- .../security/access/PermissionStorage.java | 91 +---- .../access/UpdatePermissionProcedure.java | 278 ++++++++++++++++ .../UpdatePermissionRemoteCallable.java | 79 +++++ .../UpdatePermissionRemoteProcedure.java | 112 +++++++ .../security/access/ZKPermissionStorage.java | 171 ++++++++++ .../security/access/ZKPermissionWatcher.java | 310 ------------------ .../hbase/MockRegionServerServices.java | 4 +- .../TestAsyncAccessControlAdminApi.java | 27 +- .../hbase/master/MockNoopMasterServices.java | 4 +- .../hadoop/hbase/master/MockRegionServer.java | 4 +- .../hbase/security/access/SecureTestUtil.java | 10 + .../security/access/TestAccessController.java | 21 ++ .../access/TestAccessController3.java | 136 +++----- .../security/access/TestTablePermissions.java | 2 +- .../access/TestZKPermissionWatcher.java | 177 ---------- .../hbase/thrift2/client/ThriftAdmin.java | 4 +- 35 files changed, 1240 insertions(+), 854 deletions(-) create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/AclProcedureInterface.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/AclQueue.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/UpdatePermissionProcedure.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/UpdatePermissionRemoteCallable.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/UpdatePermissionRemoteProcedure.java create mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/ZKPermissionStorage.java delete mode 100644 hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/ZKPermissionWatcher.java delete mode 100644 hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestZKPermissionWatcher.java diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Admin.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Admin.java index e2280df696aa..876ff46e5bed 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Admin.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Admin.java @@ -2140,14 +2140,47 @@ void cloneTableSchema(TableName tableName, TableName newTableName, boolean prese * permissions. * @throws IOException if a remote or network exception occurs */ - void grant(UserPermission userPermission, boolean mergeExistingPermissions) throws IOException; + default void grant(UserPermission userPermission, boolean mergeExistingPermissions) + throws IOException { + get(grantAsync(userPermission, mergeExistingPermissions)); + } + + /** + * Grants user specific permissions but does not block. You can use Future.get(long, TimeUnit) to + * wait on the operation to complete. It may throw ExecutionException if there was an error while + * executing the operation or TimeoutException in case the wait timeout was not long enough to + * allow the operation to complete. + * @param userPermission user name and the specific permission + * @param mergeExistingPermissions If set to false, later granted permissions will override + * previous granted permissions. otherwise, it'll merge with previous granted + * permissions. + * @throws IOException if a remote or network exception occurs + * @return the result of the async creation. You can use Future.get(long, TimeUnit) to wait on the + * operation to complete. + */ + Future grantAsync(UserPermission userPermission, boolean mergeExistingPermissions) + throws IOException; /** * Revokes user specific permissions * @param userPermission user name and the specific permission * @throws IOException if a remote or network exception occurs */ - void revoke(UserPermission userPermission) throws IOException; + default void revoke(UserPermission userPermission) throws IOException { + get(revokeAsync(userPermission)); + } + + /** + * Revokes user specific permissions but does not block. You can use Future.get(long, TimeUnit) to + * wait on the operation to complete. It may throw ExecutionException if there was an error while + * executing the operation or TimeoutException in case the wait timeout was not long enough to + * allow the operation to complete. + * @param userPermission user name and the specific permission + * @throws IOException if a remote or network exception occurs + * @return the result of the async creation. You can use Future.get(long, TimeUnit) to wait on the + * operation to complete. + */ + Future revokeAsync(UserPermission userPermission) throws IOException; /** * Get the global/namespace/table permissions for user diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HBaseAdmin.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HBaseAdmin.java index 5812bd873ad1..83b0375bed13 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HBaseAdmin.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HBaseAdmin.java @@ -3837,30 +3837,71 @@ protected SpaceQuotaSnapshot rpcCall() throws Exception { }); } + @InterfaceAudience.Private + @InterfaceStability.Evolving + private class UpdatePermissionFuture extends ProcedureFuture { + private final UserPermission userPermission; + private final Supplier getOperation; + + public UpdatePermissionFuture(HBaseAdmin admin, UserPermission userPermission, Long procId, + Supplier getOperation) { + super(admin, procId); + this.userPermission = userPermission; + this.getOperation = getOperation; + } + + @Override + public String toString() { + return "Operation: " + getOperation.get() + ", userPermission: " + userPermission; + } + } + + @InterfaceAudience.Private + @InterfaceStability.Evolving + private class GrantFuture extends UpdatePermissionFuture { + private final boolean mergeExistingPermissions; + + public GrantFuture(HBaseAdmin admin, UserPermission userPermission, + boolean mergeExistingPermissions, Long procId, Supplier getOperation) { + super(admin, userPermission, procId, getOperation); + this.mergeExistingPermissions = mergeExistingPermissions; + } + + @Override + public String toString() { + return super.toString() + ", mergeExistingPermissions: " + mergeExistingPermissions; + } + } + @Override - public void grant(UserPermission userPermission, boolean mergeExistingPermissions) + public Future grantAsync(UserPermission userPermission, boolean mergeExistingPermissions) throws IOException { - executeCallable(new MasterCallable(getConnection(), getRpcControllerFactory()) { - @Override - protected Void rpcCall() throws Exception { - GrantRequest req = - ShadedAccessControlUtil.buildGrantRequest(userPermission, mergeExistingPermissions); - this.master.grant(getRpcController(), req); - return null; - } - }); + AccessControlProtos.GrantResponse response = + executeCallable(new MasterCallable(getConnection(), + getRpcControllerFactory()) { + @Override + protected AccessControlProtos.GrantResponse rpcCall() throws Exception { + GrantRequest req = + ShadedAccessControlUtil.buildGrantRequest(userPermission, mergeExistingPermissions); + return this.master.grant(getRpcController(), req); + } + }); + return new GrantFuture(this, userPermission, mergeExistingPermissions, response.getProcId(), + () -> "GRANT"); } @Override - public void revoke(UserPermission userPermission) throws IOException { - executeCallable(new MasterCallable(getConnection(), getRpcControllerFactory()) { - @Override - protected Void rpcCall() throws Exception { - RevokeRequest req = ShadedAccessControlUtil.buildRevokeRequest(userPermission); - this.master.revoke(getRpcController(), req); - return null; - } - }); + public Future revokeAsync(UserPermission userPermission) throws IOException { + AccessControlProtos.RevokeResponse response = + executeCallable(new MasterCallable(getConnection(), + getRpcControllerFactory()) { + @Override + protected AccessControlProtos.RevokeResponse rpcCall() throws Exception { + RevokeRequest req = ShadedAccessControlUtil.buildRevokeRequest(userPermission); + return this.master.revoke(getRpcController(), req); + } + }); + return new UpdatePermissionFuture(this, userPermission, response.getProcId(), () -> "REVOKE"); } @Override diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RawAsyncHBaseAdmin.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RawAsyncHBaseAdmin.java index 3303fd39e424..c2caed6df515 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RawAsyncHBaseAdmin.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/RawAsyncHBaseAdmin.java @@ -3832,20 +3832,18 @@ public CompletableFuture getCurrentSpaceQuotaSnapshot(TableN @Override public CompletableFuture grant(UserPermission userPermission, boolean mergeExistingPermissions) { - return this. newMasterCaller() - .action((controller, stub) -> this. call(controller, - stub, ShadedAccessControlUtil.buildGrantRequest(userPermission, mergeExistingPermissions), - (s, c, req, done) -> s.grant(c, req, done), resp -> null)) - .call(); + return this. procedureCall( + ShadedAccessControlUtil.buildGrantRequest(userPermission, mergeExistingPermissions), + (s, c, req, done) -> s.grant(c, req, done), (resp) -> resp.getProcId(), + new GrantProcedureBiConsumer(userPermission, mergeExistingPermissions)); } @Override public CompletableFuture revoke(UserPermission userPermission) { - return this. newMasterCaller() - .action((controller, stub) -> this. call(controller, - stub, ShadedAccessControlUtil.buildRevokeRequest(userPermission), - (s, c, req, done) -> s.revoke(c, req, done), resp -> null)) - .call(); + return this. procedureCall( + ShadedAccessControlUtil.buildRevokeRequest(userPermission), + (s, c, req, done) -> s.revoke(c, req, done), (resp) -> resp.getProcId(), + new RevokeProcedureBiConsumer(userPermission)); } @Override @@ -3873,4 +3871,58 @@ public CompletableFuture> hasUserPermissions(String userName, resp -> resp.getHasUserPermissionList())) .call(); } + + private static abstract class UpdatePermissionProcedureBiConsumer extends ProcedureBiConsumer { + protected final UserPermission userPermission; + + UpdatePermissionProcedureBiConsumer(UserPermission userPermission) { + this.userPermission = userPermission; + } + + abstract String getOperationType(); + + String getDescription() { + return "Operation: " + getOperationType() + ", UserPermission: " + userPermission; + } + + @Override + void onFinished() { + LOG.info(getDescription() + " completed"); + } + + @Override + void onError(Throwable error) { + LOG.info(getDescription() + " failed with " + error.getMessage()); + } + } + + private static class RevokeProcedureBiConsumer extends UpdatePermissionProcedureBiConsumer { + RevokeProcedureBiConsumer(UserPermission userPermission) { + super(userPermission); + } + + @Override + String getOperationType() { + return "REVOKE"; + } + } + + private static class GrantProcedureBiConsumer extends UpdatePermissionProcedureBiConsumer { + protected final boolean mergeExistingPermissions; + + GrantProcedureBiConsumer(UserPermission userPermission, boolean mergeExistingPermissions) { + super(userPermission); + this.mergeExistingPermissions = mergeExistingPermissions; + } + + @Override + String getDescription() { + return super.getDescription() + ", mergeExistingPermissions: " + mergeExistingPermissions; + } + + @Override + String getOperationType() { + return "GRANT"; + } + } } diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/LockedResourceType.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/LockedResourceType.java index 55d195b3920f..8d7d816079f1 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/LockedResourceType.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/LockedResourceType.java @@ -22,5 +22,5 @@ @InterfaceAudience.Private public enum LockedResourceType { - SERVER, NAMESPACE, TABLE, REGION, PEER, META + SERVER, NAMESPACE, TABLE, REGION, PEER, META, ACL } diff --git a/hbase-protocol-shaded/src/main/protobuf/AccessControl.proto b/hbase-protocol-shaded/src/main/protobuf/AccessControl.proto index 3142a12b4ac9..d274a92c1d21 100644 --- a/hbase-protocol-shaded/src/main/protobuf/AccessControl.proto +++ b/hbase-protocol-shaded/src/main/protobuf/AccessControl.proto @@ -90,6 +90,7 @@ message GrantRequest { } message GrantResponse { + optional uint64 proc_id = 1; } message RevokeRequest { @@ -97,6 +98,7 @@ message RevokeRequest { } message RevokeResponse { + optional uint64 proc_id = 1; } message GetUserPermissionsRequest { diff --git a/hbase-protocol-shaded/src/main/protobuf/MasterProcedure.proto b/hbase-protocol-shaded/src/main/protobuf/MasterProcedure.proto index d5a390ca44a5..ba9c59bdd4f8 100644 --- a/hbase-protocol-shaded/src/main/protobuf/MasterProcedure.proto +++ b/hbase-protocol-shaded/src/main/protobuf/MasterProcedure.proto @@ -24,6 +24,7 @@ option java_generic_services = true; option java_generate_equals_and_hash = true; option optimize_for = SPEED; +import "AccessControl.proto"; import "HBase.proto"; import "RPC.proto"; import "Snapshot.proto"; @@ -607,4 +608,20 @@ enum SplitWALState{ ACQUIRE_SPLIT_WAL_WORKER = 1; DISPATCH_WAL_TO_WORKER = 2; RELEASE_SPLIT_WORKER = 3; -} \ No newline at end of file +} + +enum UpdatePermissionState { + UPDATE_PERMISSION_STORAGE = 1; + UPDATE_PERMISSION_CACHE_ON_RS = 2; + POST_UPDATE_PERMISSION = 3; +} + +message UpdatePermissionStateData { + required ServerName target_server = 1; + required string entry = 2; +} + +message UpdatePermissionRemoteStateData { + required ServerName target_server = 1; + required string entry = 2; +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/executor/EventType.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/executor/EventType.java index 19264d23fe0b..0e81673c4f8d 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/executor/EventType.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/executor/EventType.java @@ -294,7 +294,14 @@ public enum EventType { * * RS_REPLAY_SYNC_REPLICATION_WAL */ - RS_REPLAY_SYNC_REPLICATION_WAL(85, ExecutorType.RS_REPLAY_SYNC_REPLICATION_WAL); + RS_REPLAY_SYNC_REPLICATION_WAL(85, ExecutorType.RS_REPLAY_SYNC_REPLICATION_WAL), + + /** + * RS refresh permission cache.
+ * + * RS_REFRESH_PERMISSION_CACHE + */ + RS_REFRESH_PERMISSION_CACHE(86, ExecutorType.RS_REFRESH_PERMISSION_CACHE); private final int code; private final ExecutorType executor; diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/executor/ExecutorType.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/executor/ExecutorType.java index d354d62c901c..c9f76d1b8cdb 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/executor/ExecutorType.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/executor/ExecutorType.java @@ -50,7 +50,8 @@ public enum ExecutorType { RS_REFRESH_PEER(31), RS_REPLAY_SYNC_REPLICATION_WAL(32), RS_SWITCH_RPC_THROTTLE(33), - RS_IN_MEMORY_COMPACTION(34); + RS_IN_MEMORY_COMPACTION(34), + RS_REFRESH_PERMISSION_CACHE(35); ExecutorType(int value) { } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterRpcServices.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterRpcServices.java index b772fd736cfc..7777ec9b11aa 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterRpcServices.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterRpcServices.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.apache.hadoop.conf.Configuration; @@ -52,7 +53,6 @@ import org.apache.hadoop.hbase.client.RegionInfo; import org.apache.hadoop.hbase.client.RegionInfoBuilder; import org.apache.hadoop.hbase.client.Result; -import org.apache.hadoop.hbase.client.Table; import org.apache.hadoop.hbase.client.TableDescriptor; import org.apache.hadoop.hbase.client.TableState; import org.apache.hadoop.hbase.client.VersionInfoUtil; @@ -77,6 +77,7 @@ import org.apache.hadoop.hbase.master.procedure.MasterProcedureEnv; import org.apache.hadoop.hbase.master.procedure.MasterProcedureUtil; import org.apache.hadoop.hbase.master.procedure.MasterProcedureUtil.NonceProcedureRunnable; +import org.apache.hadoop.hbase.master.procedure.ProcedurePrepareLatch; import org.apache.hadoop.hbase.master.procedure.ServerCrashProcedure; import org.apache.hadoop.hbase.mob.MobUtils; import org.apache.hadoop.hbase.procedure.MasterProcedureManager; @@ -107,6 +108,7 @@ import org.apache.hadoop.hbase.security.access.Permission.Action; import org.apache.hadoop.hbase.security.access.PermissionStorage; import org.apache.hadoop.hbase.security.access.ShadedAccessControlUtil; +import org.apache.hadoop.hbase.security.access.UpdatePermissionProcedure; import org.apache.hadoop.hbase.security.access.UserPermission; import org.apache.hadoop.hbase.security.visibility.VisibilityController; import org.apache.hadoop.hbase.snapshot.ClientSnapshotDescriptionUtils; @@ -2727,14 +2729,13 @@ public GrantResponse grant(RpcController controller, GrantRequest request) if (master.cpHost != null) { master.cpHost.preGrant(perm, mergeExistingPermissions); } - try (Table table = master.getConnection().getTable(PermissionStorage.ACL_TABLE_NAME)) { - PermissionStorage.addUserPermission(getConfiguration(), perm, table, - mergeExistingPermissions); - } - if (master.cpHost != null) { - master.cpHost.postGrant(perm, mergeExistingPermissions); - } - return GrantResponse.getDefaultInstance(); + ProcedurePrepareLatch latch = ProcedurePrepareLatch.createLatch(2, 2); + UpdatePermissionProcedure procedure = new UpdatePermissionProcedure( + UpdatePermissionProcedure.UpdatePermissionType.GRANT, master.getServerName(), + Optional.of(perm), Optional.of(mergeExistingPermissions), Optional.empty(), latch); + long procId = master.getMasterProcedureExecutor().submitProcedure(procedure); + latch.await(); + return GrantResponse.newBuilder().setProcId(procId).build(); } catch (IOException ioe) { throw new ServiceException(ioe); } @@ -2749,13 +2750,13 @@ public RevokeResponse revoke(RpcController controller, RevokeRequest request) if (master.cpHost != null) { master.cpHost.preRevoke(userPermission); } - try (Table table = master.getConnection().getTable(PermissionStorage.ACL_TABLE_NAME)) { - PermissionStorage.removeUserPermission(master.getConfiguration(), userPermission, table); - } - if (master.cpHost != null) { - master.cpHost.postRevoke(userPermission); - } - return RevokeResponse.getDefaultInstance(); + ProcedurePrepareLatch latch = ProcedurePrepareLatch.createLatch(2, 2); + UpdatePermissionProcedure procedure = new UpdatePermissionProcedure( + UpdatePermissionProcedure.UpdatePermissionType.REVOKE, master.getServerName(), + Optional.of(userPermission), Optional.empty(), Optional.empty(), latch); + long procId = master.getMasterProcedureExecutor().submitProcedure(procedure); + latch.await(); + return RevokeResponse.newBuilder().setProcId(procId).build(); } catch (IOException ioe) { throw new ServiceException(ioe); } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterServices.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterServices.java index d5aa4fecd33c..4adf36eaac65 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterServices.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/MasterServices.java @@ -52,7 +52,7 @@ import org.apache.hadoop.hbase.replication.ReplicationPeerDescription; import org.apache.hadoop.hbase.replication.SyncReplicationState; import org.apache.hadoop.hbase.security.access.AccessChecker; -import org.apache.hadoop.hbase.security.access.ZKPermissionWatcher; +import org.apache.hadoop.hbase.security.access.ZKPermissionStorage; import org.apache.yetus.audience.InterfaceAudience; import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting; @@ -526,7 +526,7 @@ default SplitWALManager getSplitWALManager(){ AccessChecker getAccessChecker(); /** - * @return the {@link ZKPermissionWatcher} + * @return the {@link ZKPermissionStorage} */ - ZKPermissionWatcher getZKPermissionWatcher(); + ZKPermissionStorage getZKPermissionStorage(); } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/AclProcedureInterface.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/AclProcedureInterface.java new file mode 100644 index 000000000000..23048bdc94ff --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/AclProcedureInterface.java @@ -0,0 +1,32 @@ +/** + * 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.hbase.master.procedure; + +import org.apache.yetus.audience.InterfaceAudience; + +@InterfaceAudience.Private +public interface AclProcedureInterface { + + String getAclEntry(); + + enum AclOperationType { + UPDATE, REMOTE + } + + AclProcedureInterface.AclOperationType getAclOperationType(); +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/AclQueue.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/AclQueue.java new file mode 100644 index 000000000000..510f667444d9 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/AclQueue.java @@ -0,0 +1,35 @@ +/** + * 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.hbase.master.procedure; + +import org.apache.hadoop.hbase.procedure2.LockStatus; +import org.apache.hadoop.hbase.procedure2.Procedure; +import org.apache.yetus.audience.InterfaceAudience; + +@InterfaceAudience.Private +class AclQueue extends Queue { + + public AclQueue(String entry, LockStatus lockStatus) { + super(entry, lockStatus); + } + + @Override + public boolean requireExclusiveLock(Procedure proc) { + return false; + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/MasterProcedureScheduler.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/MasterProcedureScheduler.java index 4bf16ec15a9c..4bb4c3d26587 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/MasterProcedureScheduler.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/MasterProcedureScheduler.java @@ -105,16 +105,20 @@ public class MasterProcedureScheduler extends AbstractProcedureScheduler { (n, k) -> n.compareKey((String) k); private final static AvlKeyComparator META_QUEUE_KEY_COMPARATOR = (n, k) -> n.compareKey((TableName) k); + private final static AvlKeyComparator ACL_QUEUE_KEY_COMPARATOR = + (n, k) -> n.compareKey((String) k); private final FairQueue serverRunQueue = new FairQueue<>(); private final FairQueue tableRunQueue = new FairQueue<>(); private final FairQueue peerRunQueue = new FairQueue<>(); private final FairQueue metaRunQueue = new FairQueue<>(); + private final FairQueue aclRunQueue = new FairQueue<>(); private final ServerQueue[] serverBuckets = new ServerQueue[128]; private TableQueue tableMap = null; private PeerQueue peerMap = null; private MetaQueue metaMap = null; + private AclQueue aclMap = null; private final SchemaLocking locking; @@ -138,6 +142,8 @@ protected void enqueue(final Procedure proc, final boolean addFront) { doAdd(serverRunQueue, getServerQueue(spi.getServerName(), spi), proc, addFront); } else if (isPeerProcedure(proc)) { doAdd(peerRunQueue, getPeerQueue(getPeerId(proc)), proc, addFront); + } else if (isAclProcedure(proc)) { + doAdd(aclRunQueue, getAclQueue(getAcl(proc)), proc, addFront); } else { // TODO: at the moment we only have Table and Server procedures // if you are implementing a non-table/non-server procedure, you have two options: create @@ -173,8 +179,9 @@ private > void doAdd(FairQueue fairq, Queue queue, @Override protected boolean queueHasRunnables() { - return metaRunQueue.hasRunnables() || tableRunQueue.hasRunnables() || - serverRunQueue.hasRunnables() || peerRunQueue.hasRunnables(); + return metaRunQueue.hasRunnables() || tableRunQueue.hasRunnables() + || serverRunQueue.hasRunnables() || peerRunQueue.hasRunnables() + || aclRunQueue.hasRunnables(); } @Override @@ -193,6 +200,9 @@ protected Procedure dequeue() { if (pollResult == null) { pollResult = doPoll(tableRunQueue); } + if (pollResult == null) { + pollResult = doPoll(aclRunQueue); + } return pollResult; } @@ -278,6 +288,9 @@ private void clearQueue() { clear(peerMap, peerRunQueue, PEER_QUEUE_KEY_COMPARATOR); peerMap = null; + clear(aclMap, aclRunQueue, ACL_QUEUE_KEY_COMPARATOR); + aclMap = null; + assert size() == 0 : "expected queue size to be 0, got " + size(); } @@ -310,6 +323,7 @@ protected int queueSize() { count += queueSize(tableMap); count += queueSize(peerMap); count += queueSize(metaMap); + count += queueSize(aclMap); return count; } @@ -339,6 +353,8 @@ public void completionCleanup(final Procedure proc) { tryCleanupPeerQueue(getPeerId(proc), proc); } else if (proc instanceof ServerProcedureInterface) { tryCleanupServerQueue(getServerName(proc), proc); + } else if (proc instanceof AclProcedureInterface) { + tryCleanupAclQueue(getAcl(proc), proc); } else { // No cleanup for other procedure types, yet. return; @@ -495,6 +511,51 @@ private static String getPeerId(Procedure proc) { return ((PeerProcedureInterface) proc).getPeerId(); } + // ============================================================================ + // Acl Queue Lookup Helpers + // ============================================================================ + private AclQueue getAclQueue(String entry) { + AclQueue node = AvlTree.get(aclMap, entry, ACL_QUEUE_KEY_COMPARATOR); + if (node != null) { + return node; + } + node = new AclQueue(entry, locking.getAclLock(entry)); + aclMap = AvlTree.insert(aclMap, node); + return node; + } + + private void removeAclQueue(String acl) { + aclMap = AvlTree.remove(aclMap, acl, ACL_QUEUE_KEY_COMPARATOR); + locking.removeAclLock(acl); + } + + private void tryCleanupAclQueue(String acl, Procedure procedure) { + schedLock(); + try { + AclQueue queue = AvlTree.get(aclMap, acl, ACL_QUEUE_KEY_COMPARATOR); + if (queue == null) { + return; + } + + final LockAndQueue lock = locking.getAclLock(acl); + if (queue.isEmpty() && lock.tryExclusiveLock(procedure)) { + removeFromRunQueue(aclRunQueue, queue, + () -> "clean up acl queue after " + procedure + " completed"); + removeAclQueue(acl); + } + } finally { + schedUnlock(); + } + } + + private static boolean isAclProcedure(Procedure proc) { + return proc instanceof AclProcedureInterface; + } + + private static String getAcl(Procedure proc) { + return ((AclProcedureInterface) proc).getAclEntry(); + } + // ============================================================================ // Meta Queue Lookup Helpers // ============================================================================ diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/SchemaLocking.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/SchemaLocking.java index 70e7c592ca18..ab9c226db8af 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/SchemaLocking.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/master/procedure/SchemaLocking.java @@ -53,6 +53,7 @@ class SchemaLocking { // Single map for all regions irrespective of tables. Key is encoded region name. private final Map regionLocks = new HashMap<>(); private final Map peerLocks = new HashMap<>(); + private final Map aclLocks = new HashMap<>(); private final LockAndQueue metaLock; public SchemaLocking(Function> procedureRetriever) { @@ -114,6 +115,14 @@ LockAndQueue removePeerLock(String peerId) { return peerLocks.remove(peerId); } + LockAndQueue getAclLock(String entry) { + return getLock(aclLocks, entry); + } + + LockAndQueue removeAclLock(String acl) { + return aclLocks.remove(acl); + } + private LockedResource createLockedResource(LockedResourceType resourceType, String resourceName, LockAndQueue queue) { LockType lockType; @@ -162,6 +171,7 @@ List getLocks() { addToLockedResources(lockedResources, regionLocks, Function.identity(), LockedResourceType.REGION); addToLockedResources(lockedResources, peerLocks, Function.identity(), LockedResourceType.PEER); + addToLockedResources(lockedResources, aclLocks, Function.identity(), LockedResourceType.ACL); addToLockedResources(lockedResources, ImmutableMap.of(TableName.META_TABLE_NAME, metaLock), tn -> tn.getNameAsString(), LockedResourceType.META); return lockedResources; @@ -191,6 +201,10 @@ LockedResource getLockResource(LockedResourceType resourceType, String resourceN break; case META: queue = metaLock; + break; + case ACL: + queue = aclLocks.get(resourceName); + break; default: queue = null; break; @@ -208,6 +222,7 @@ void clear() { tableLocks.clear(); regionLocks.clear(); peerLocks.clear(); + aclLocks.clear(); } @Override @@ -216,7 +231,8 @@ public String toString() { filterUnlocked(this.namespaceLocks) + ", tableLocks=" + filterUnlocked(this.tableLocks) + ", regionLocks=" + filterUnlocked(this.regionLocks) + ", peerLocks=" + filterUnlocked(this.peerLocks) + ", metaLocks=" + - filterUnlocked(ImmutableMap.of(TableName.META_TABLE_NAME, metaLock)); + filterUnlocked(ImmutableMap.of(TableName.META_TABLE_NAME, metaLock)) + ", aclLocks=" + + filterUnlocked(this.aclLocks); } private String filterUnlocked(Map locks) { diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java index fc9712b3c1e8..31da5b983b68 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/HRegionServer.java @@ -148,7 +148,7 @@ import org.apache.hadoop.hbase.security.User; import org.apache.hadoop.hbase.security.UserProvider; import org.apache.hadoop.hbase.security.access.AccessChecker; -import org.apache.hadoop.hbase.security.access.ZKPermissionWatcher; +import org.apache.hadoop.hbase.security.access.ZKPermissionStorage; import org.apache.hadoop.hbase.trace.SpanReceiverHost; import org.apache.hadoop.hbase.trace.TraceUtil; import org.apache.hadoop.hbase.util.Addressing; @@ -1983,6 +1983,8 @@ private void startServices() throws IOException { conf.getInt("hbase.regionserver.executor.replay.sync.replication.wal.threads", 1)); this.executorService.startExecutorService(ExecutorType.RS_SWITCH_RPC_THROTTLE, conf.getInt("hbase.regionserver.executor.switch.rpc.throttle.threads", 1)); + this.executorService.startExecutorService(ExecutorType.RS_REFRESH_PERMISSION_CACHE, + conf.getInt("hbase.regionserver.executor.refresh.permission.cache.threads", 1)); Threads.setDaemonThreadRunning(this.walRoller.getThread(), getName() + ".logRoller", uncaughtExceptionHandler); @@ -3678,8 +3680,8 @@ public AccessChecker getAccessChecker() { } @Override - public ZKPermissionWatcher getZKPermissionWatcher() { - return rpcServices.getZkPermissionWatcher(); + public ZKPermissionStorage getZKPermissionStorage() { + return rpcServices.getZKPermissionStorage(); } /** diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RSRpcServices.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RSRpcServices.java index 1586f1c9f052..65136b80dc38 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RSRpcServices.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RSRpcServices.java @@ -133,7 +133,7 @@ import org.apache.hadoop.hbase.security.access.AccessChecker; import org.apache.hadoop.hbase.security.access.NoopAccessChecker; import org.apache.hadoop.hbase.security.access.Permission; -import org.apache.hadoop.hbase.security.access.ZKPermissionWatcher; +import org.apache.hadoop.hbase.security.access.ZKPermissionStorage; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.DNS; import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; @@ -147,7 +147,6 @@ import org.apache.hadoop.hbase.wal.WALSplitUtil.MutationReplay; import org.apache.hadoop.hbase.zookeeper.ZKWatcher; import org.apache.yetus.audience.InterfaceAudience; -import org.apache.zookeeper.KeeperException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -349,7 +348,7 @@ public class RSRpcServices implements HBaseRPCErrorHandler, final AtomicBoolean clearCompactionQueues = new AtomicBoolean(false); private AccessChecker accessChecker; - private ZKPermissionWatcher zkPermissionWatcher; + private ZKPermissionStorage zkPermissionStorage; /** * Services launched in RSRpcServices. By default they are on but you can use the below @@ -1487,22 +1486,14 @@ void start(ZKWatcher zkWatcher) { accessChecker = new NoopAccessChecker(getConfiguration()); } if (!getConfiguration().getBoolean("hbase.testing.nocluster", false) && zkWatcher != null) { - zkPermissionWatcher = - new ZKPermissionWatcher(zkWatcher, accessChecker.getAuthManager(), getConfiguration()); - try { - zkPermissionWatcher.start(); - } catch (KeeperException e) { - LOG.error("ZooKeeper permission watcher initialization failed", e); - } + zkPermissionStorage = new ZKPermissionStorage(zkWatcher, getConfiguration()); + zkPermissionStorage.reloadPermissionsToAuthManager(accessChecker.getAuthManager()); } this.scannerIdGenerator = new ScannerIdGenerator(this.regionServer.serverName); rpcServer.start(); } void stop() { - if (zkPermissionWatcher != null) { - zkPermissionWatcher.close(); - } closeAllScanners(); rpcServer.stop(); } @@ -3800,7 +3791,7 @@ protected AccessChecker getAccessChecker() { return accessChecker; } - protected ZKPermissionWatcher getZkPermissionWatcher() { - return zkPermissionWatcher; + protected ZKPermissionStorage getZKPermissionStorage() { + return zkPermissionStorage; } } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RegionServerServices.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RegionServerServices.java index 04a1bc647c35..25646a834af8 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RegionServerServices.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/RegionServerServices.java @@ -41,7 +41,7 @@ import org.apache.hadoop.hbase.regionserver.compactions.CompactionRequester; import org.apache.hadoop.hbase.regionserver.throttle.ThroughputController; import org.apache.hadoop.hbase.security.access.AccessChecker; -import org.apache.hadoop.hbase.security.access.ZKPermissionWatcher; +import org.apache.hadoop.hbase.security.access.ZKPermissionStorage; import org.apache.hadoop.hbase.wal.WAL; import org.apache.yetus.audience.InterfaceAudience; @@ -313,7 +313,7 @@ boolean reportFileArchivalForQuotas( AccessChecker getAccessChecker(); /** - * @return {@link ZKPermissionWatcher} + * @return the {@link ZKPermissionStorage} */ - ZKPermissionWatcher getZKPermissionWatcher(); + ZKPermissionStorage getZKPermissionStorage(); } \ No newline at end of file diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/AccessController.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/AccessController.java index 1dfd7e66025d..b4e887f3f848 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/AccessController.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/AccessController.java @@ -35,7 +35,6 @@ import java.util.Optional; import java.util.Set; import java.util.TreeMap; -import java.util.TreeSet; import java.util.stream.Collectors; import org.apache.hadoop.conf.Configuration; @@ -51,6 +50,7 @@ import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.KeyValue; import org.apache.hadoop.hbase.KeyValue.Type; +import org.apache.hadoop.hbase.MetaTableAccessor; import org.apache.hadoop.hbase.NamespaceDescriptor; import org.apache.hadoop.hbase.PrivateCellUtil; import org.apache.hadoop.hbase.ServerName; @@ -98,6 +98,11 @@ import org.apache.hadoop.hbase.ipc.CoprocessorRpcUtils; import org.apache.hadoop.hbase.ipc.RpcServer; import org.apache.hadoop.hbase.master.MasterServices; +import org.apache.hadoop.hbase.master.RegionState; +import org.apache.hadoop.hbase.master.procedure.MasterProcedureEnv; +import org.apache.hadoop.hbase.master.procedure.ProcedurePrepareLatch; +import org.apache.hadoop.hbase.master.procedure.ProcedureSyncWait; +import org.apache.hadoop.hbase.procedure2.ProcedureExecutor; import org.apache.hadoop.hbase.protobuf.ProtobufUtil; import org.apache.hadoop.hbase.protobuf.generated.AccessControlProtos; import org.apache.hadoop.hbase.protobuf.generated.AccessControlProtos.AccessControlService; @@ -192,7 +197,7 @@ public class AccessController implements MasterCoprocessor, RegionCoprocessor, private static final byte[] TRUE = Bytes.toBytes(true); private AccessChecker accessChecker; - private ZKPermissionWatcher zkPermissionWatcher; + private ZKPermissionStorage zkPermissionStorage; /** flags if we are running on a region of the _acl_ table */ private boolean aclRegion = false; @@ -243,61 +248,6 @@ public AuthManager getAuthManager() { return accessChecker.getAuthManager(); } - private void initialize(RegionCoprocessorEnvironment e) throws IOException { - final Region region = e.getRegion(); - Configuration conf = e.getConfiguration(); - Map> tables = PermissionStorage.loadAll(region); - // For each table, write out the table's permissions to the respective - // znode for that table. - for (Map.Entry> t: - tables.entrySet()) { - byte[] entry = t.getKey(); - ListMultimap perms = t.getValue(); - byte[] serialized = PermissionStorage.writePermissionsAsBytes(perms, conf); - zkPermissionWatcher.writeToZookeeper(entry, serialized); - } - initialized = true; - } - - /** - * Writes all table ACLs for the tables in the given Map up into ZooKeeper - * znodes. This is called to synchronize ACL changes following {@code _acl_} - * table updates. - */ - private void updateACL(RegionCoprocessorEnvironment e, - final Map> familyMap) { - Set entries = new TreeSet<>(Bytes.BYTES_RAWCOMPARATOR); - for (Map.Entry> f : familyMap.entrySet()) { - List cells = f.getValue(); - for (Cell cell: cells) { - if (CellUtil.matchingFamily(cell, PermissionStorage.ACL_LIST_FAMILY)) { - entries.add(CellUtil.cloneRow(cell)); - } - } - } - Configuration conf = regionEnv.getConfiguration(); - byte [] currentEntry = null; - // TODO: Here we are already on the ACL region. (And it is single - // region) We can even just get the region from the env and do get - // directly. The short circuit connection would avoid the RPC overhead - // so no socket communication, req write/read .. But we have the PB - // to and fro conversion overhead. get req is converted to PB req - // and results are converted to PB results 1st and then to POJOs - // again. We could have avoided such at least in ACL table context.. - try (Table t = e.getConnection().getTable(PermissionStorage.ACL_TABLE_NAME)) { - for (byte[] entry : entries) { - currentEntry = entry; - ListMultimap perms = - PermissionStorage.getPermissions(conf, entry, t, null, null, null, false); - byte[] serialized = PermissionStorage.writePermissionsAsBytes(perms, conf); - zkPermissionWatcher.writeToZookeeper(entry, serialized); - } - } catch(IOException ex) { - LOG.error("Failed updating permissions mirror for '" + - (currentEntry == null? "null": Bytes.toString(currentEntry)) + "'", ex); - } - } - /** * Check the current user for authorization to perform a specific action * against the given set of row data. @@ -691,7 +641,7 @@ public void start(CoprocessorEnvironment env) throws IOException { MasterCoprocessorEnvironment mEnv = (MasterCoprocessorEnvironment) env; if (mEnv instanceof HasMasterServices) { MasterServices masterServices = ((HasMasterServices) mEnv).getMasterServices(); - zkPermissionWatcher = masterServices.getZKPermissionWatcher(); + zkPermissionStorage = masterServices.getZKPermissionStorage(); accessChecker = masterServices.getAccessChecker(); } } else if (env instanceof RegionServerCoprocessorEnvironment) { @@ -699,7 +649,7 @@ public void start(CoprocessorEnvironment env) throws IOException { if (rsEnv instanceof HasRegionServerServices) { RegionServerServices rsServices = ((HasRegionServerServices) rsEnv).getRegionServerServices(); - zkPermissionWatcher = rsServices.getZKPermissionWatcher(); + zkPermissionStorage = rsServices.getZKPermissionStorage(); accessChecker = rsServices.getAccessChecker(); } } else if (env instanceof RegionCoprocessorEnvironment) { @@ -711,12 +661,12 @@ public void start(CoprocessorEnvironment env) throws IOException { if (regionEnv instanceof HasRegionServerServices) { RegionServerServices rsServices = ((HasRegionServerServices) regionEnv).getRegionServerServices(); - zkPermissionWatcher = rsServices.getZKPermissionWatcher(); + zkPermissionStorage = rsServices.getZKPermissionStorage(); accessChecker = rsServices.getAccessChecker(); } } - if (zkPermissionWatcher == null) { + if (zkPermissionStorage == null) { throw new NullPointerException("ZKPermissionWatcher is null"); } else if (accessChecker == null) { throw new NullPointerException("AccessChecker is null"); @@ -796,7 +746,7 @@ public void postCompletedCreateTableAction( // creating acl table, getting delayed and by that time another table creation got over and // this hook is getting called. In such a case, we will need a wait logic here which will // wait till the acl table is created. - if (PermissionStorage.isAclTable(desc)) { + if (PermissionStorage.isAclTable(desc.getTableName())) { this.aclTabAvailable = true; } else { if (!aclTabAvailable) { @@ -814,11 +764,7 @@ public void postCompletedCreateTableAction( User.runAsLoginUser(new PrivilegedExceptionAction() { @Override public Void run() throws Exception { - try (Table table = - c.getEnvironment().getConnection().getTable(PermissionStorage.ACL_TABLE_NAME)) { - PermissionStorage.addUserPermission(c.getEnvironment().getConfiguration(), - userPermission, table); - } + c.getEnvironment().getConnection().getAdmin().grant(userPermission, false); return null; } }); @@ -836,18 +782,20 @@ public void preDeleteTable(ObserverContext c, Tabl @Override public void postDeleteTable(ObserverContext c, final TableName tableName) throws IOException { - final Configuration conf = c.getEnvironment().getConfiguration(); - User.runAsLoginUser(new PrivilegedExceptionAction() { - @Override - public Void run() throws Exception { - try (Table table = - c.getEnvironment().getConnection().getTable(PermissionStorage.ACL_TABLE_NAME)) { - PermissionStorage.removeTablePermissions(conf, tableName, table); - } + if (c.getEnvironment() instanceof HasMasterServices) { + User.runAsLoginUser(() -> { + ProcedureExecutor masterProcedureExecutor = + ((HasMasterServices) c.getEnvironment()).getMasterServices() + .getMasterProcedureExecutor(); + ProcedurePrepareLatch latch = ProcedurePrepareLatch.getNoopLatch(); + UpdatePermissionProcedure procedure = new UpdatePermissionProcedure( + UpdatePermissionProcedure.UpdatePermissionType.DELETE_TABLE, + c.getEnvironment().getServerName(), Optional.empty(), Optional.empty(), + Optional.of(tableName.getNameAsString()), latch); + ProcedureSyncWait.submitAndWaitProcedure(masterProcedureExecutor, procedure); return null; - } - }); - zkPermissionWatcher.deleteTableACLNode(tableName); + }); + } } @Override @@ -1032,13 +980,34 @@ public void preStopMaster(ObserverContext c) public void postStartMaster(ObserverContext ctx) throws IOException { try (Admin admin = ctx.getEnvironment().getConnection().getAdmin()) { + MasterServices masterServices = + ((HasMasterServices) ctx.getEnvironment()).getMasterServices(); if (!admin.tableExists(PermissionStorage.ACL_TABLE_NAME)) { createACLTable(admin); } else { this.aclTabAvailable = true; + List tableRegions = MetaTableAccessor.getTableRegions( + masterServices.getConnection(), PermissionStorage.ACL_TABLE_NAME, true); + // wait until acl table is online + for (RegionInfo region : tableRegions) { + RegionState regionState = + masterServices.getAssignmentManager().getRegionStates().getRegionState(region); + if (regionState.getState() != RegionState.State.OPEN) { + masterServices.getAssignmentManager().assign(region); + } + } } + ProcedureExecutor masterProcedureExecutor = + masterServices.getMasterProcedureExecutor(); + ProcedurePrepareLatch latch = ProcedurePrepareLatch.getNoopLatch(); + UpdatePermissionProcedure procedure = + new UpdatePermissionProcedure(UpdatePermissionProcedure.UpdatePermissionType.RELOAD, + ctx.getEnvironment().getServerName(), Optional.empty(), Optional.empty(), + Optional.empty(), latch); + masterProcedureExecutor.submitProcedure(procedure); } } + /** * Create the ACL table * @throws IOException @@ -1146,19 +1115,20 @@ public void preDeleteNamespace(ObserverContext ctx @Override public void postDeleteNamespace(ObserverContext ctx, final String namespace) throws IOException { - final Configuration conf = ctx.getEnvironment().getConfiguration(); - User.runAsLoginUser(new PrivilegedExceptionAction() { - @Override - public Void run() throws Exception { - try (Table table = - ctx.getEnvironment().getConnection().getTable(PermissionStorage.ACL_TABLE_NAME)) { - PermissionStorage.removeNamespacePermissions(conf, namespace, table); - } + if (ctx.getEnvironment() instanceof HasMasterServices) { + User.runAsLoginUser(() -> { + ProcedureExecutor masterProcedureExecutor = + ((HasMasterServices) ctx.getEnvironment()).getMasterServices() + .getMasterProcedureExecutor(); + ProcedurePrepareLatch latch = ProcedurePrepareLatch.getNoopLatch(); + UpdatePermissionProcedure procedure = new UpdatePermissionProcedure( + UpdatePermissionProcedure.UpdatePermissionType.DELETE_NAMESPACE, + ctx.getEnvironment().getServerName(), Optional.empty(), Optional.empty(), + Optional.of(namespace), latch); + ProcedureSyncWait.submitAndWaitProcedure(masterProcedureExecutor, procedure); return null; - } - }); - zkPermissionWatcher.deleteNamespaceACLNode(namespace); - LOG.info(namespace + " entry deleted in " + PermissionStorage.ACL_TABLE_NAME + " table."); + }); + } } @Override @@ -1271,16 +1241,8 @@ public void postOpen(ObserverContext c) { } if (PermissionStorage.isAclRegion(region)) { aclRegion = true; - try { - initialize(env); - } catch (IOException ex) { - // if we can't obtain permissions, it's better to fail - // than perform checks incorrectly - throw new RuntimeException("Failed to initialize permissions cache", ex); - } - } else { - initialized = true; } + initialized = true; } @Override @@ -1452,14 +1414,6 @@ public void prePut(final ObserverContext c, } } - @Override - public void postPut(final ObserverContext c, - final Put put, final WALEdit edit, final Durability durability) { - if (aclRegion) { - updateACL(c.getEnvironment(), put.getFamilyCellMap()); - } - } - @Override public void preDelete(final ObserverContext c, final Delete delete, final WALEdit edit, final Durability durability) @@ -1526,15 +1480,6 @@ public void preBatchMutate(ObserverContext c, } } - @Override - public void postDelete(final ObserverContext c, - final Delete delete, final WALEdit edit, final Durability durability) - throws IOException { - if (aclRegion) { - updateACL(c.getEnvironment(), delete.getFamilyCellMap()); - } - } - @Override public boolean preCheckAndPut(final ObserverContext c, final byte [] row, final byte [] family, final byte [] qualifier, diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/AuthManager.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/AuthManager.java index 3ced725e0ad7..93ac63996bcd 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/AuthManager.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/AuthManager.java @@ -91,8 +91,9 @@ void clear() { } } } - PermissionCache NS_NO_PERMISSION = new PermissionCache<>(); - PermissionCache TBL_NO_PERMISSION = new PermissionCache<>(); + + private PermissionCache NS_NO_PERMISSION = new PermissionCache<>(); + private PermissionCache TBL_NO_PERMISSION = new PermissionCache<>(); /** * Cache for global permission excluding superuser and supergroup. @@ -481,6 +482,7 @@ public boolean authorizeCell(User user, TableName table, Cell cell, Permission.A */ public void removeNamespace(byte[] ns) { namespaceCache.remove(Bytes.toString(ns)); + mtime.incrementAndGet(); } /** @@ -489,6 +491,7 @@ public void removeNamespace(byte[] ns) { */ public void removeTable(TableName table) { tableCache.remove(table); + mtime.incrementAndGet(); } /** @@ -498,4 +501,37 @@ public void removeTable(TableName table) { public long getMTime() { return mtime.get(); } + + /** + * Refresh permission cache for entry + * @param entry the given entry, it's '@namespace', 'hbase:acl' or 'tablename'. + * @param data the updated user permissions data + * @throws IOException exception when deserialize data + */ + public void refresh(String entry, byte[] data) throws IOException { + if (LOG.isDebugEnabled()) { + LOG.debug("Updating permissions cache for {} with data {}", entry, + Bytes.toStringBinary(data)); + } + if (PermissionStorage.isNamespaceEntry(entry)) { + refreshNamespaceCacheFromWritable(PermissionStorage.fromNamespaceEntry(entry), data); + } else { + refreshTableCacheFromWritable(TableName.valueOf(entry), data); + } + } + + /** + * Remove permission cache for entry + * @param entry the given entry, it's '@namespace', 'hbase:acl' or 'tablename'. + */ + public void remove(String entry) { + if (LOG.isDebugEnabled()) { + LOG.debug("Removing permissions cache for {}", entry); + } + if (PermissionStorage.isNamespaceEntry(entry)) { + removeNamespace(PermissionStorage.fromNamespaceEntry(Bytes.toBytes(entry))); + } else { + removeTable(TableName.valueOf(entry)); + } + } } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/PermissionStorage.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/PermissionStorage.java index bcf070a93004..e905edb2f1a4 100644 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/PermissionStorage.java +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/PermissionStorage.java @@ -54,13 +54,11 @@ import org.apache.hadoop.hbase.client.ResultScanner; import org.apache.hadoop.hbase.client.Scan; import org.apache.hadoop.hbase.client.Table; -import org.apache.hadoop.hbase.client.TableDescriptor; import org.apache.hadoop.hbase.exceptions.DeserializationException; import org.apache.hadoop.hbase.filter.QualifierFilter; import org.apache.hadoop.hbase.filter.RegexStringComparator; import org.apache.hadoop.hbase.protobuf.ProtobufUtil; import org.apache.hadoop.hbase.protobuf.generated.AccessControlProtos; -import org.apache.hadoop.hbase.regionserver.InternalScanner; import org.apache.hadoop.hbase.regionserver.Region; import org.apache.hadoop.hbase.security.User; import org.apache.hadoop.hbase.util.Bytes; @@ -394,67 +392,35 @@ static boolean isAclRegion(Region region) { /** * Returns {@code true} if the given table is {@code _acl_} metadata table. */ - static boolean isAclTable(TableDescriptor desc) { - return ACL_TABLE_NAME.equals(desc.getTableName()); + static boolean isAclTable(TableName tableName) { + return ACL_TABLE_NAME.equals(tableName); } /** - * Loads all of the permission grants stored in a region of the {@code _acl_} - * table. + * Loads all of the permission grants stored in the table of {@code _acl_} * - * @param aclRegion the acl region + * @param aclTable the acl table * @return a map of the permissions for this table. * @throws IOException if an error occurs */ - static Map> loadAll(Region aclRegion) + public static Map> loadAll(Table aclTable) throws IOException { - if (!isAclRegion(aclRegion)) { - throw new IOException("Can only load permissions from "+ACL_TABLE_NAME); + if (!isAclTable(aclTable.getName())) { + throw new IOException("Can only load permissions from " + ACL_TABLE_NAME); } - Map> allPerms = - new TreeMap<>(Bytes.BYTES_RAWCOMPARATOR); + new TreeMap<>(Bytes.BYTES_RAWCOMPARATOR); // do a full scan of _acl_ table - Scan scan = new Scan(); scan.addFamily(ACL_LIST_FAMILY); - - InternalScanner iScanner = null; - try { - iScanner = aclRegion.getScanner(scan); - - while (true) { - List row = new ArrayList<>(); - - boolean hasNext = iScanner.next(row); - ListMultimap perms = ArrayListMultimap.create(); - byte[] entry = null; - for (Cell kv : row) { - if (entry == null) { - entry = CellUtil.cloneRow(kv); - } - Pair permissionsOfUserOnTable = - parsePermissionRecord(entry, kv, null, null, false, null); - if (permissionsOfUserOnTable != null) { - String username = permissionsOfUserOnTable.getFirst(); - Permission permission = permissionsOfUserOnTable.getSecond(); - perms.put(username, new UserPermission(username, permission)); - } - } - if (entry != null) { - allPerms.put(entry, perms); - } - if (!hasNext) { - break; - } - } - } finally { - if (iScanner != null) { - iScanner.close(); + try (ResultScanner scanner = aclTable.getScanner(scan)) { + for (Result row : scanner) { + ListMultimap resultPerms = + parsePermissions(row.getRow(), row, null, null, null, false); + allPerms.put(row.getRow(), resultPerms); } } - return allPerms; } @@ -462,36 +428,14 @@ static Map> loadAll(Region aclRegio * Load all permissions from the region server holding {@code _acl_}, * primarily intended for testing purposes. */ - static Map> loadAll( - Configuration conf) throws IOException { - Map> allPerms = - new TreeMap<>(Bytes.BYTES_RAWCOMPARATOR); - - // do a full scan of _acl_, filtering on only first table region rows - - Scan scan = new Scan(); - scan.addFamily(ACL_LIST_FAMILY); - - ResultScanner scanner = null; + static Map> loadAll(Configuration conf) + throws IOException { // TODO: Pass in a Connection rather than create one each time. try (Connection connection = ConnectionFactory.createConnection(conf)) { try (Table table = connection.getTable(ACL_TABLE_NAME)) { - scanner = table.getScanner(scan); - try { - for (Result row : scanner) { - ListMultimap resultPerms = - parsePermissions(row.getRow(), row, null, null, null, false); - allPerms.put(row.getRow(), resultPerms); - } - } finally { - if (scanner != null) { - scanner.close(); - } - } + return loadAll(table); } } - - return allPerms; } public static ListMultimap getTablePermissions(Configuration conf, @@ -744,8 +688,7 @@ private static boolean validateCFAndCQ(byte[] permFamily, byte[] cf, byte[] perm * Writes a set of permissions as {@link org.apache.hadoop.io.Writable} instances and returns the * resulting byte array. Writes a set of permission [user: table permission] */ - public static byte[] writePermissionsAsBytes(ListMultimap perms, - Configuration conf) { + public static byte[] writePermissionsAsBytes(ListMultimap perms) { return ProtobufUtil .prependPBMagic(AccessControlUtil.toUserTablePermissions(perms).toByteArray()); } diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/UpdatePermissionProcedure.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/UpdatePermissionProcedure.java new file mode 100644 index 000000000000..ff997ef075b9 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/UpdatePermissionProcedure.java @@ -0,0 +1,278 @@ +/* + * 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.hbase.security.access; + +import java.io.IOException; +import java.util.Map; +import java.util.Optional; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.ServerName; +import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.client.Table; +import org.apache.hadoop.hbase.master.MasterCoprocessorHost; +import org.apache.hadoop.hbase.master.procedure.AclProcedureInterface; +import org.apache.hadoop.hbase.master.procedure.MasterProcedureEnv; +import org.apache.hadoop.hbase.master.procedure.ProcedurePrepareLatch; +import org.apache.hadoop.hbase.procedure2.ProcedureSuspendedException; +import org.apache.hadoop.hbase.procedure2.ProcedureUtil; +import org.apache.hadoop.hbase.procedure2.ProcedureYieldException; +import org.apache.hadoop.hbase.procedure2.StateMachineProcedure; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.hadoop.hbase.util.RetryCounter; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hbase.thirdparty.com.google.common.collect.ListMultimap; +import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos.UpdatePermissionState; +import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos; + +@InterfaceAudience.Private +public class UpdatePermissionProcedure + extends StateMachineProcedure + implements AclProcedureInterface { + private static Logger LOG = LoggerFactory.getLogger(UpdatePermissionProcedure.class); + // the entry represents that need to reload all kinds of permission cache + // because namespace and table can not contain a '#' character + static final byte[] RELOAD_ALL_ENTRY = Bytes.toBytes("#Reload"); + + public enum UpdatePermissionType { + GRANT, REVOKE, DELETE_TABLE, DELETE_NAMESPACE, RELOAD + } + + private UpdatePermissionType updatePermissionType; + private ProcedurePrepareLatch procedurePrepareLatch; + private ServerName serverName; + private RetryCounter retryCounter; + + private byte[] entry; + private UserPermission userPermission; + private boolean mergeExistingPermissions; + + public UpdatePermissionProcedure() { + } + + public UpdatePermissionProcedure(UpdatePermissionType type, ServerName serverName, + Optional userPermission, Optional mergeExistingPermissions, + Optional deleteEntry, ProcedurePrepareLatch latch) { + this.updatePermissionType = type; + this.serverName = serverName; + this.procedurePrepareLatch = latch; + if (updatePermissionType == UpdatePermissionType.GRANT) { + if (!userPermission.isPresent()) { + throw new IllegalArgumentException("UserPermission is empty"); + } + if (!mergeExistingPermissions.isPresent()) { + throw new IllegalArgumentException("mergeExistingPermissions is empty"); + } + this.userPermission = userPermission.get(); + this.mergeExistingPermissions = mergeExistingPermissions.get(); + this.entry = PermissionStorage.userPermissionRowKey(this.userPermission.getPermission()); + } else if (updatePermissionType == UpdatePermissionType.REVOKE) { + if (!userPermission.isPresent()) { + throw new IllegalArgumentException("UserPermission is empty"); + } + this.userPermission = userPermission.get(); + this.entry = PermissionStorage.userPermissionRowKey(this.userPermission.getPermission()); + } else if (updatePermissionType == UpdatePermissionType.DELETE_NAMESPACE) { + if (!deleteEntry.isPresent()) { + throw new IllegalArgumentException("Namespace is empty"); + } + this.entry = Bytes.toBytes(PermissionStorage.toNamespaceEntry(deleteEntry.get())); + } else if (updatePermissionType == UpdatePermissionType.DELETE_TABLE) { + if (!deleteEntry.isPresent()) { + throw new IllegalArgumentException("Table is empty"); + } + this.entry = Bytes.toBytes(deleteEntry.get()); + } else if (updatePermissionType == UpdatePermissionType.RELOAD) { + this.entry = RELOAD_ALL_ENTRY; + LOG.info("Reload all permission cache"); + } else { + throw new IllegalArgumentException("Unknown update permission type"); + } + } + + @Override + protected Flow executeFromState(MasterProcedureEnv env, UpdatePermissionState state) + throws ProcedureSuspendedException, ProcedureYieldException, InterruptedException { + switch (state) { + case UPDATE_PERMISSION_STORAGE: + try { + // update permission in acl table and znode, refresh master auth manager + updateStorageAndRefreshAuthManager(env); + } catch (IOException e) { + if (retryCounter == null) { + retryCounter = ProcedureUtil.createRetryCounter(env.getMasterConfiguration()); + } + long backoff = retryCounter.getBackoffTimeAndIncrementAttempts(); + LOG.warn("Failed to update user permission, type {}, entry {}, sleep {} secs and retry", + updatePermissionType, Bytes.toString(entry), backoff / 1000, e); + setTimeout(Math.toIntExact(backoff)); + setState(ProcedureProtos.ProcedureState.WAITING_TIMEOUT); + skipPersistence(); + throw new ProcedureSuspendedException(); + } + setNextState(UpdatePermissionState.UPDATE_PERMISSION_CACHE_ON_RS); + return Flow.HAS_MORE_STATE; + case UPDATE_PERMISSION_CACHE_ON_RS: + // update permission in RS auth manager cache + UpdatePermissionRemoteProcedure[] subProcedures = + env.getMasterServices().getServerManager().getOnlineServersList().stream() + .map(sn -> new UpdatePermissionRemoteProcedure(sn, Bytes.toString(entry))) + .toArray(UpdatePermissionRemoteProcedure[]::new); + addChildProcedure(subProcedures); + setNextState(UpdatePermissionState.POST_UPDATE_PERMISSION); + return Flow.HAS_MORE_STATE; + case POST_UPDATE_PERMISSION: + try { + postUpdatePermission(env); + } catch (IOException e) { + LOG.warn( + "{} failed to call post CP hook after updating permission, type {}, userPermission {} " + + "ignore since the procedure has already done", + getClass().getName(), updatePermissionType, e); + } + releaseProcedureLatch(); + return Flow.NO_MORE_STATE; + default: + throw new UnsupportedOperationException("unhandled state=" + state); + } + } + + private void postUpdatePermission(MasterProcedureEnv env) throws IOException { + MasterCoprocessorHost cpHost = env.getMasterCoprocessorHost(); + if (cpHost != null) { + if (updatePermissionType == UpdatePermissionType.GRANT) { + cpHost.postGrant(userPermission, mergeExistingPermissions); + } else if (updatePermissionType == UpdatePermissionType.REVOKE) { + cpHost.postRevoke(userPermission); + } + } + } + + private void releaseProcedureLatch() { + ProcedurePrepareLatch.releaseLatch(procedurePrepareLatch, this); + } + + private void updateStorageAndRefreshAuthManager(MasterProcedureEnv env) throws IOException { + try (Table table = + env.getMasterServices().getConnection().getTable(PermissionStorage.ACL_TABLE_NAME)) { + Configuration conf = env.getMasterConfiguration(); + AuthManager authManager = env.getMasterServices().getAccessChecker().getAuthManager(); + ZKPermissionStorage zkPermissionStorage = env.getMasterServices().getZKPermissionStorage(); + if (updatePermissionType == UpdatePermissionType.GRANT) { + // add user permission to acl table + PermissionStorage.addUserPermission(conf, userPermission, table, mergeExistingPermissions); + // get permissions from acl table and write to zk + ListMultimap permissions = + PermissionStorage.getPermissions(conf, entry, table, null, null, null, false); + byte[] userPermissions = PermissionStorage.writePermissionsAsBytes(permissions); + zkPermissionStorage.writePermission(entry, userPermissions); + // update permission in master auth manager cache + authManager.refresh(Bytes.toString(entry), userPermissions); + } else if (updatePermissionType == UpdatePermissionType.REVOKE) { + // remove user permission from acl table + PermissionStorage.removeUserPermission(conf, userPermission, table); + // get permissions from acl table and write to zk + ListMultimap permissions = + PermissionStorage.getPermissions(conf, entry, table, null, null, null, false); + byte[] userPermissions = PermissionStorage.writePermissionsAsBytes(permissions); + zkPermissionStorage.writePermission(entry, userPermissions); + // update permission in master auth manager cache + authManager.refresh(Bytes.toString(entry), userPermissions); + } else if (updatePermissionType == UpdatePermissionType.DELETE_NAMESPACE) { + String namespace = Bytes.toString(PermissionStorage.fromNamespaceEntry(entry)); + // remove all namespace permissions from acl table + PermissionStorage.removeNamespacePermissions(env.getMasterConfiguration(), namespace, + table); + // remove namespace acl znode from zk + zkPermissionStorage.deleteNamespacePermission(namespace); + // remove all namespace permission from master auth manager cache + env.getMasterServices().getAccessChecker().getAuthManager().remove(Bytes.toString(entry)); + } else if (updatePermissionType == UpdatePermissionType.DELETE_TABLE) { + TableName tableName = TableName.valueOf(entry); + // remove all table permissions from acl table + PermissionStorage.removeTablePermissions(env.getMasterConfiguration(), tableName, table); + // remove table acl znode from zk + zkPermissionStorage.deleteTablePermission(tableName); + // remove all table permission from master auth manager cache + env.getMasterServices().getAccessChecker().getAuthManager().remove(Bytes.toString(entry)); + } else if (updatePermissionType == UpdatePermissionType.RELOAD) { + // load all permissions from acl table + Map> permissions = + PermissionStorage.loadAll(table); + // write all permissions to zk + zkPermissionStorage.reloadPermissions(permissions); + // reload master auth manager permission cache + zkPermissionStorage.reloadPermissionsToAuthManager( + env.getMasterServices().getAccessChecker().getAuthManager()); + } + } + } + + @Override + protected void rollbackState(MasterProcedureEnv env, UpdatePermissionState state) + throws IOException, InterruptedException { + if (state == getInitialState()) { + return; + } + throw new UnsupportedOperationException(); + } + + @Override + protected UpdatePermissionState getState(int stateId) { + return UpdatePermissionState.forNumber(stateId); + } + + @Override + protected int getStateId(UpdatePermissionState accessControlState) { + return accessControlState.getNumber(); + } + + @Override + protected UpdatePermissionState getInitialState() { + return UpdatePermissionState.UPDATE_PERMISSION_STORAGE; + } + + @Override + protected void toStringClassDetails(StringBuilder sb) { + sb.append(getClass().getSimpleName()); + sb.append(" server=").append(serverName); + sb.append(", type=").append(updatePermissionType); + if (updatePermissionType == UpdatePermissionType.GRANT) { + sb.append(", user permission=").append(userPermission); + sb.append(", mergeExistingPermissions=").append(mergeExistingPermissions); + } else if (updatePermissionType == UpdatePermissionType.REVOKE) { + sb.append(", user permission=").append(userPermission); + } else if (updatePermissionType == UpdatePermissionType.DELETE_NAMESPACE + || updatePermissionType == UpdatePermissionType.DELETE_TABLE) { + sb.append(", delete permission entry=").append(Bytes.toString(entry)); + } + } + + @Override + public String getAclEntry() { + return Bytes.toString(entry); + } + + @Override + public AclOperationType getAclOperationType() { + return AclOperationType.UPDATE; + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/UpdatePermissionRemoteCallable.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/UpdatePermissionRemoteCallable.java new file mode 100644 index 000000000000..92d06460f370 --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/UpdatePermissionRemoteCallable.java @@ -0,0 +1,79 @@ +/* + * 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.hbase.security.access; + +import org.apache.hadoop.hbase.executor.EventType; +import org.apache.hadoop.hbase.procedure2.RSProcedureCallable; +import org.apache.hadoop.hbase.regionserver.HRegionServer; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hbase.thirdparty.com.google.protobuf.InvalidProtocolBufferException; +import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos.UpdatePermissionStateData; + +@InterfaceAudience.Private +public class UpdatePermissionRemoteCallable implements RSProcedureCallable { + private static Logger LOG = LoggerFactory.getLogger(UpdatePermissionRemoteCallable.class); + private HRegionServer rs; + private String entry; + private Exception initError; + + public UpdatePermissionRemoteCallable() { + } + + @Override + public void init(byte[] parameter, HRegionServer rs) { + this.rs = rs; + try { + UpdatePermissionStateData param = UpdatePermissionStateData.parseFrom(parameter); + entry = param.getEntry(); + } catch (InvalidProtocolBufferException e) { + initError = e; + } + } + + @Override + public EventType getEventType() { + return EventType.RS_REFRESH_PERMISSION_CACHE; + } + + @Override + public Void call() throws Exception { + if (initError != null) { + throw initError; + } + if (Bytes.equals(Bytes.toBytes(entry), UpdatePermissionProcedure.RELOAD_ALL_ENTRY)) { + // If entry is #Reload, then reload all permission cache. + rs.getZKPermissionStorage() + .reloadPermissionsToAuthManager(rs.getAccessChecker().getAuthManager()); + } else { + byte[] entryBytes = Bytes.toBytes(entry); + byte[] permission = rs.getZKPermissionStorage().getPermission(entryBytes); + if (permission != null) { + // refresh the entry permission in cache + rs.getAccessChecker().getAuthManager().refresh(entry, permission); + } else { + // if entry permission is null, then remove the entry permission in cache + rs.getAccessChecker().getAuthManager().remove(entry); + } + } + return null; + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/UpdatePermissionRemoteProcedure.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/UpdatePermissionRemoteProcedure.java new file mode 100644 index 000000000000..be90e9ea983a --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/UpdatePermissionRemoteProcedure.java @@ -0,0 +1,112 @@ +/* + * 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.hbase.security.access; + +import java.io.IOException; +import java.util.Optional; + +import org.apache.hadoop.hbase.ServerName; +import org.apache.hadoop.hbase.master.procedure.AclProcedureInterface; +import org.apache.hadoop.hbase.master.procedure.MasterProcedureEnv; +import org.apache.hadoop.hbase.master.procedure.RSProcedureDispatcher; +import org.apache.hadoop.hbase.master.procedure.ServerRemoteProcedure; +import org.apache.hadoop.hbase.procedure2.ProcedureStateSerializer; +import org.apache.hadoop.hbase.procedure2.RemoteProcedureDispatcher.RemoteOperation; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil; +import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProcedureProtos.UpdatePermissionRemoteStateData; + +@InterfaceAudience.Private +public class UpdatePermissionRemoteProcedure extends ServerRemoteProcedure + implements AclProcedureInterface { + private static final Logger LOG = LoggerFactory.getLogger(UpdatePermissionRemoteProcedure.class); + private String entry; + + public UpdatePermissionRemoteProcedure() { + } + + public UpdatePermissionRemoteProcedure(ServerName serverName, String entry) { + this.targetServer = serverName; + this.entry = entry; + } + + @Override + protected void complete(MasterProcedureEnv env, Throwable error) { + if (error != null) { + LOG.warn("Failed to update permissions for entry {} on server {}", entry, targetServer, + error); + this.succ = false; + } else { + this.succ = true; + } + } + + @Override + protected void rollback(MasterProcedureEnv env) throws IOException, InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + protected boolean abort(MasterProcedureEnv env) { + return false; + } + + @Override + protected void serializeStateData(ProcedureStateSerializer serializer) throws IOException { + UpdatePermissionRemoteStateData data = UpdatePermissionRemoteStateData.newBuilder() + .setTargetServer(ProtobufUtil.toServerName(targetServer)).setEntry(entry).build(); + serializer.serialize(data); + } + + @Override + protected void deserializeStateData(ProcedureStateSerializer serializer) throws IOException { + UpdatePermissionRemoteStateData data = + serializer.deserialize(UpdatePermissionRemoteStateData.class); + targetServer = ProtobufUtil.toServerName(data.getTargetServer()); + entry = data.getEntry(); + } + + @Override + public Optional remoteCallBuild(MasterProcedureEnv env, ServerName serverName) { + assert targetServer.equals(serverName); + return Optional.of(new RSProcedureDispatcher.ServerOperation(this, getProcId(), + UpdatePermissionRemoteCallable.class, + UpdatePermissionRemoteStateData.newBuilder() + .setTargetServer(ProtobufUtil.toServerName(serverName)).setEntry(entry).build() + .toByteArray())); + } + + @Override + protected void toStringClassDetails(StringBuilder sb) { + sb.append(getClass().getSimpleName()).append(" server=").append(targetServer).append(", entry=") + .append(entry); + } + + @Override + public String getAclEntry() { + return entry; + } + + @Override + public AclOperationType getAclOperationType() { + return AclOperationType.REMOTE; + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/ZKPermissionStorage.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/ZKPermissionStorage.java new file mode 100644 index 000000000000..8e320416934b --- /dev/null +++ b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/ZKPermissionStorage.java @@ -0,0 +1,171 @@ +/* + * 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.hbase.security.access; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.hadoop.hbase.zookeeper.ZKUtil; +import org.apache.hadoop.hbase.zookeeper.ZKWatcher; +import org.apache.hadoop.hbase.zookeeper.ZNodePaths; +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.zookeeper.KeeperException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hbase.thirdparty.com.google.common.collect.ListMultimap; + +/** + * Store user permissions in zk. Used when cluster startup, because the acl table is not online. + * There are three types znodes under '/hbase/acl' znode: + * '/hbase/acl/hbase:acl' znode: contains a serialized list of global user permissions; + * '/hbase/acl/@namespace' znode: contains a serialized list of namespace user permissions; + * '/hbase/acl/tableName' znode: contains a serialized list of table user permissions. + */ +@InterfaceAudience.Private +public class ZKPermissionStorage { + private static final Logger LOG = LoggerFactory.getLogger(ZKPermissionStorage.class); + // parent node for permissions lists + static final String ACL_NODE = "acl"; + + private ZKWatcher watcher; + private final String aclZNode; + + public ZKPermissionStorage(ZKWatcher watcher, Configuration conf) { + this.watcher = watcher; + this.aclZNode = ZNodePaths.joinZNode(watcher.getZNodePaths().baseZNode, + conf.get("zookeeper.znode.acl.parent", ACL_NODE)); + } + + public ZKWatcher getWatcher() { + return watcher; + } + + /** + * Reload permission data to acl znode + * @param permissions the permission map + */ + public void reloadPermissions(Map> permissions) { + for (Map.Entry> permission : permissions + .entrySet()) { + byte[] entry = permission.getKey(); + ListMultimap perms = permission.getValue(); + byte[] serialized = PermissionStorage.writePermissionsAsBytes(perms); + writePermission(entry, serialized); + } + } + + /** + * Reload permission data from acl znode to auth manager cache + * @param authManager the auth manager instance + */ + public void reloadPermissionsToAuthManager(AuthManager authManager) { + try { + if (ZKUtil.checkExists(watcher, aclZNode) != -1) { + List nodes = ZKUtil.listChildrenNoWatch(watcher, aclZNode); + if (nodes != null) { + for (String node : nodes) { + byte[] data = ZKUtil.getData(watcher, ZNodePaths.joinZNode(aclZNode, node)); + if (data != null) { + try { + authManager.refresh(node, data); + } catch (IOException e) { + LOG.error("Failed deserialize permission data for {}", node, e); + } + } + } + } + } + } catch (KeeperException | InterruptedException e) { + LOG.error("Failed loading permission cache from {}", aclZNode, e); + watcher.abort("Failed loading permission cache from " + aclZNode, e); + } + } + + /*** + * Write a table's access controls to the permissions mirror in zookeeper + * @param entry + * @param permsData + */ + public void writePermission(byte[] entry, byte[] permsData) { + String entryName = Bytes.toString(entry); + String zkNode = ZNodePaths.joinZNode(aclZNode, entryName); + try { + ZKUtil.createSetData(watcher, zkNode, permsData); + } catch (KeeperException e) { + LOG.error("Failed updating permissions for entry '{}'", entryName, e); + watcher.abort("Failed writing node " + zkNode + " to zookeeper", e); + } + } + + /** + * Get the permissions for the entry + * @param entry the given entry, it's '@namespace', 'hbase:acl' or 'tablename'. + * @return the user permissions in bytes or null if the entry node does not exist in zk + */ + public byte[] getPermission(byte[] entry) { + String entryName = Bytes.toString(entry); + String zkNode = ZNodePaths.joinZNode(aclZNode, entryName); + try { + if (ZKUtil.checkExists(watcher, zkNode) != -1) { + byte[] data = ZKUtil.getData(watcher, zkNode); + return data; + } else { + return null; + } + } catch (KeeperException | InterruptedException e) { + LOG.error("Failed getting permissions for entry '{}'", entryName, e); + watcher.abort("Failed getting node " + zkNode + " from zookeeper", e); + return null; + } + } + + /** + * Delete the acl node of table + * @param tableName the table name + */ + public void deleteTablePermission(final TableName tableName) { + String zkNode = ZNodePaths.joinZNode(aclZNode, tableName.getNameAsString()); + deletePermission(zkNode); + } + + /** + * Delete the acl node of namespace + * @param namespace the namespace + */ + public void deleteNamespacePermission(final String namespace) { + String zkNode = ZNodePaths.joinZNode(aclZNode, PermissionStorage.NAMESPACE_PREFIX + namespace); + deletePermission(zkNode); + } + + private void deletePermission(final String zkNode) { + try { + ZKUtil.deleteNode(watcher, zkNode); + } catch (KeeperException.NoNodeException e) { + LOG.warn("No acl node '{}'", zkNode); + } catch (KeeperException e) { + LOG.error("Failed deleting acl node '{}'", zkNode, e); + watcher.abort("Failed deleting node " + zkNode, e); + } + } +} diff --git a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/ZKPermissionWatcher.java b/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/ZKPermissionWatcher.java deleted file mode 100644 index b410719cb4bd..000000000000 --- a/hbase-server/src/main/java/org/apache/hadoop/hbase/security/access/ZKPermissionWatcher.java +++ /dev/null @@ -1,310 +0,0 @@ -/* - * 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.hbase.security.access; - -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.hbase.DaemonThreadFactory; -import org.apache.hadoop.hbase.TableName; -import org.apache.hadoop.hbase.util.Bytes; -import org.apache.hadoop.hbase.zookeeper.ZKListener; -import org.apache.hadoop.hbase.zookeeper.ZKUtil; -import org.apache.hadoop.hbase.zookeeper.ZKWatcher; -import org.apache.hadoop.hbase.zookeeper.ZNodePaths; -import org.apache.yetus.audience.InterfaceAudience; -import org.apache.zookeeper.KeeperException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.Closeable; -import java.io.IOException; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.RejectedExecutionException; - -/** - * Handles synchronization of access control list entries and updates - * throughout all nodes in the cluster. The {@link AccessController} instance - * on the {@code _acl_} table regions, creates a znode for each table as - * {@code /hbase/acl/tablename}, with the znode data containing a serialized - * list of the permissions granted for the table. The {@code AccessController} - * instances on all other cluster hosts watch the znodes for updates, which - * trigger updates in the {@link AuthManager} permission cache. - */ -@InterfaceAudience.Private -public class ZKPermissionWatcher extends ZKListener implements Closeable { - private static final Logger LOG = LoggerFactory.getLogger(ZKPermissionWatcher.class); - // parent node for permissions lists - static final String ACL_NODE = "acl"; - private final AuthManager authManager; - private final String aclZNode; - private final CountDownLatch initialized = new CountDownLatch(1); - private final ExecutorService executor; - private Future childrenChangedFuture; - - public ZKPermissionWatcher(ZKWatcher watcher, - AuthManager authManager, Configuration conf) { - super(watcher); - this.authManager = authManager; - String aclZnodeParent = conf.get("zookeeper.znode.acl.parent", ACL_NODE); - this.aclZNode = ZNodePaths.joinZNode(watcher.getZNodePaths().baseZNode, aclZnodeParent); - executor = Executors.newSingleThreadExecutor( - new DaemonThreadFactory("zk-permission-watcher")); - } - - public void start() throws KeeperException { - try { - watcher.registerListener(this); - if (ZKUtil.watchAndCheckExists(watcher, aclZNode)) { - try { - executor.submit(new Callable() { - @Override - public Void call() throws KeeperException { - List existing = - ZKUtil.getChildDataAndWatchForNewChildren(watcher, aclZNode); - if (existing != null) { - refreshNodes(existing); - } - return null; - } - }).get(); - } catch (ExecutionException ex) { - if (ex.getCause() instanceof KeeperException) { - throw (KeeperException)ex.getCause(); - } else { - throw new RuntimeException(ex.getCause()); - } - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } - } finally { - initialized.countDown(); - } - } - - @Override - public void close() { - executor.shutdown(); - } - - private void waitUntilStarted() { - try { - initialized.await(); - } catch (InterruptedException e) { - LOG.warn("Interrupted while waiting for start", e); - Thread.currentThread().interrupt(); - } - } - - @Override - public void nodeCreated(String path) { - waitUntilStarted(); - if (path.equals(aclZNode)) { - asyncProcessNodeUpdate(new Runnable() { - @Override - public void run() { - try { - List nodes = - ZKUtil.getChildDataAndWatchForNewChildren(watcher, aclZNode); - refreshNodes(nodes); - } catch (KeeperException ke) { - LOG.error("Error reading data from zookeeper", ke); - // only option is to abort - watcher.abort("ZooKeeper error obtaining acl node children", ke); - } - } - }); - } - } - - @Override - public void nodeDeleted(final String path) { - waitUntilStarted(); - if (aclZNode.equals(ZKUtil.getParent(path))) { - asyncProcessNodeUpdate(new Runnable() { - @Override - public void run() { - String table = ZKUtil.getNodeName(path); - if (PermissionStorage.isNamespaceEntry(table)) { - authManager.removeNamespace(Bytes.toBytes(table)); - } else { - authManager.removeTable(TableName.valueOf(table)); - } - } - }); - } - } - - @Override - public void nodeDataChanged(final String path) { - waitUntilStarted(); - if (aclZNode.equals(ZKUtil.getParent(path))) { - asyncProcessNodeUpdate(new Runnable() { - @Override - public void run() { - // update cache on an existing table node - String entry = ZKUtil.getNodeName(path); - try { - byte[] data = ZKUtil.getDataAndWatch(watcher, path); - refreshAuthManager(entry, data); - } catch (KeeperException ke) { - LOG.error("Error reading data from zookeeper for node " + entry, ke); - // only option is to abort - watcher.abort("ZooKeeper error getting data for node " + entry, ke); - } catch (IOException ioe) { - LOG.error("Error reading permissions writables", ioe); - } - } - }); - } - } - - - @Override - public void nodeChildrenChanged(final String path) { - waitUntilStarted(); - if (path.equals(aclZNode)) { - try { - final List nodeList = - ZKUtil.getChildDataAndWatchForNewChildren(watcher, aclZNode); - // preempt any existing nodeChildrenChanged event processing - if (childrenChangedFuture != null && !childrenChangedFuture.isDone()) { - boolean cancelled = childrenChangedFuture.cancel(true); - if (!cancelled) { - // task may have finished between our check and attempted cancel, this is fine. - if (! childrenChangedFuture.isDone()) { - LOG.warn("Could not cancel processing node children changed event, " + - "please file a JIRA and attach logs if possible."); - } - } - } - childrenChangedFuture = asyncProcessNodeUpdate(() -> refreshNodes(nodeList)); - } catch (KeeperException ke) { - LOG.error("Error reading data from zookeeper for path "+path, ke); - watcher.abort("ZooKeeper error get node children for path "+path, ke); - } - } - } - - private Future asyncProcessNodeUpdate(Runnable runnable) { - if (!executor.isShutdown()) { - try { - return executor.submit(runnable); - } catch (RejectedExecutionException e) { - if (executor.isShutdown()) { - LOG.warn("aclZNode changed after ZKPermissionWatcher was shutdown"); - } else { - throw e; - } - } - } - return null; // No task launched so there will be nothing to cancel later - } - - private void refreshNodes(List nodes) { - for (ZKUtil.NodeAndData n : nodes) { - if (Thread.interrupted()) { - // Use Thread.interrupted so that we clear interrupt status - break; - } - if (n.isEmpty()) continue; - String path = n.getNode(); - String entry = (ZKUtil.getNodeName(path)); - try { - refreshAuthManager(entry, n.getData()); - } catch (IOException ioe) { - LOG.error("Failed parsing permissions for table '" + entry + - "' from zk", ioe); - } - } - } - - private void refreshAuthManager(String entry, byte[] nodeData) throws IOException { - if (LOG.isDebugEnabled()) { - LOG.debug("Updating permissions cache from {} with data {}", entry, - Bytes.toStringBinary(nodeData)); - } - if (PermissionStorage.isNamespaceEntry(entry)) { - authManager.refreshNamespaceCacheFromWritable(PermissionStorage.fromNamespaceEntry(entry), - nodeData); - } else { - authManager.refreshTableCacheFromWritable(TableName.valueOf(entry), nodeData); - } - } - - /*** - * Write a table's access controls to the permissions mirror in zookeeper - * @param entry - * @param permsData - */ - public void writeToZookeeper(byte[] entry, byte[] permsData) { - String entryName = Bytes.toString(entry); - String zkNode = ZNodePaths.joinZNode(watcher.getZNodePaths().baseZNode, ACL_NODE); - zkNode = ZNodePaths.joinZNode(zkNode, entryName); - - try { - ZKUtil.createWithParents(watcher, zkNode); - ZKUtil.updateExistingNodeData(watcher, zkNode, permsData, -1); - } catch (KeeperException e) { - LOG.error("Failed updating permissions for entry '" + - entryName + "'", e); - watcher.abort("Failed writing node "+zkNode+" to zookeeper", e); - } - } - - /*** - * Delete the acl notify node of table - * @param tableName - */ - public void deleteTableACLNode(final TableName tableName) { - String zkNode = ZNodePaths.joinZNode(watcher.getZNodePaths().baseZNode, ACL_NODE); - zkNode = ZNodePaths.joinZNode(zkNode, tableName.getNameAsString()); - - try { - ZKUtil.deleteNode(watcher, zkNode); - } catch (KeeperException.NoNodeException e) { - LOG.warn("No acl notify node of table '" + tableName + "'"); - } catch (KeeperException e) { - LOG.error("Failed deleting acl node of table '" + tableName + "'", e); - watcher.abort("Failed deleting node " + zkNode, e); - } - } - - /*** - * Delete the acl notify node of namespace - */ - public void deleteNamespaceACLNode(final String namespace) { - String zkNode = ZNodePaths.joinZNode(watcher.getZNodePaths().baseZNode, ACL_NODE); - zkNode = ZNodePaths.joinZNode(zkNode, PermissionStorage.NAMESPACE_PREFIX + namespace); - - try { - ZKUtil.deleteNode(watcher, zkNode); - } catch (KeeperException.NoNodeException e) { - LOG.warn("No acl notify node of namespace '" + namespace + "'"); - } catch (KeeperException e) { - LOG.error("Failed deleting acl node of namespace '" + namespace + "'", e); - watcher.abort("Failed deleting node " + zkNode, e); - } - } -} diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/MockRegionServerServices.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/MockRegionServerServices.java index f5e2793e0c01..d5f5bb2e9d98 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/MockRegionServerServices.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/MockRegionServerServices.java @@ -57,7 +57,7 @@ import org.apache.hadoop.hbase.regionserver.compactions.CompactionRequester; import org.apache.hadoop.hbase.regionserver.throttle.ThroughputController; import org.apache.hadoop.hbase.security.access.AccessChecker; -import org.apache.hadoop.hbase.security.access.ZKPermissionWatcher; +import org.apache.hadoop.hbase.security.access.ZKPermissionStorage; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.wal.WAL; import org.apache.hadoop.hbase.zookeeper.ZKWatcher; @@ -377,7 +377,7 @@ public AccessChecker getAccessChecker() { } @Override - public ZKPermissionWatcher getZKPermissionWatcher() { + public ZKPermissionStorage getZKPermissionStorage() { return null; } } diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestAsyncAccessControlAdminApi.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestAsyncAccessControlAdminApi.java index 9182e6fb9fb2..fc708ec7f837 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestAsyncAccessControlAdminApi.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/client/TestAsyncAccessControlAdminApi.java @@ -17,9 +17,13 @@ import static org.junit.Assert.fail; import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + import org.apache.hadoop.hbase.HBaseClassTestRule; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.security.User; +import org.apache.hadoop.hbase.security.access.AuthManager; import org.apache.hadoop.hbase.security.access.GetUserPermissionsRequest; import org.apache.hadoop.hbase.security.access.Permission; import org.apache.hadoop.hbase.security.access.PermissionStorage; @@ -53,7 +57,7 @@ public static void setUpBeforeClass() throws Exception { } @Test - public void test() throws Exception { + public void testAclApi() throws Exception { TableName tableName = TableName.valueOf("test-table"); String userName1 = "user1"; String userName2 = "user2"; @@ -124,4 +128,25 @@ public Object run() throws Exception { }; assertFalse((Boolean) user2.runAs(checkPermissionsAction)); } + + @Test + public void testRegionServerPermissionCache() throws Exception { + TableName tableName = TableName.valueOf("test-table"); + String userName1 = "user1"; + User user = User.createUserForTesting(TEST_UTIL.getConfiguration(), userName1, new String[0]); + Permission permission = + Permission.newBuilder(tableName).withActions(Permission.Action.READ).build(); + UserPermission userPermission = new UserPermission(userName1, permission); + // grant user1 table permission + CompletableFuture future = admin.grant(userPermission, false); + future.get(); + + Set authManagers = SecureTestUtil.getAuthManagers(TEST_UTIL.getHBaseCluster()); + for (AuthManager authManager : authManagers) { + boolean result = authManager.authorizeUserTable(user, tableName, Permission.Action.WRITE); + assertFalse(result); + result = authManager.authorizeUserTable(user, tableName, Permission.Action.READ); + assertTrue(result); + } + } } diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockNoopMasterServices.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockNoopMasterServices.java index 35d53c546cce..003d8a162dbc 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockNoopMasterServices.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockNoopMasterServices.java @@ -55,7 +55,7 @@ import org.apache.hadoop.hbase.replication.ReplicationPeerDescription; import org.apache.hadoop.hbase.replication.SyncReplicationState; import org.apache.hadoop.hbase.security.access.AccessChecker; -import org.apache.hadoop.hbase.security.access.ZKPermissionWatcher; +import org.apache.hadoop.hbase.security.access.ZKPermissionStorage; import org.apache.hadoop.hbase.zookeeper.ZKWatcher; public class MockNoopMasterServices implements MasterServices { @@ -482,7 +482,7 @@ public AccessChecker getAccessChecker() { } @Override - public ZKPermissionWatcher getZKPermissionWatcher() { + public ZKPermissionStorage getZKPermissionStorage() { return null; } } \ No newline at end of file diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockRegionServer.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockRegionServer.java index 2afb45626b73..fd896f2cf808 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockRegionServer.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/master/MockRegionServer.java @@ -70,7 +70,7 @@ import org.apache.hadoop.hbase.regionserver.compactions.CompactionRequester; import org.apache.hadoop.hbase.regionserver.throttle.ThroughputController; import org.apache.hadoop.hbase.security.access.AccessChecker; -import org.apache.hadoop.hbase.security.access.ZKPermissionWatcher; +import org.apache.hadoop.hbase.security.access.ZKPermissionStorage; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.wal.WAL; import org.apache.hadoop.hbase.zookeeper.ZKWatcher; @@ -730,7 +730,7 @@ public AccessChecker getAccessChecker() { } @Override - public ZKPermissionWatcher getZKPermissionWatcher() { + public ZKPermissionStorage getZKPermissionStorage() { return null; } } diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/SecureTestUtil.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/SecureTestUtil.java index a84b492ebb41..7cf789ae71b5 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/SecureTestUtil.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/SecureTestUtil.java @@ -23,9 +23,11 @@ import java.lang.reflect.UndeclaredThrowableException; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; @@ -846,4 +848,12 @@ private static void checkPermissions(Configuration conf, Permission... perms) th } } } + + public static Set getAuthManagers(MiniHBaseCluster cluster) { + Set result = new HashSet<>(); + for (AccessController ac : getAccessControllers(cluster)) { + result.add(ac.getAuthManager()); + } + return result; + } } diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessController.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessController.java index 31f2adf790e7..5dacf8677826 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessController.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessController.java @@ -37,6 +37,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Set; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.CommonConfigurationKeys; import org.apache.hadoop.fs.FileStatus; @@ -2266,6 +2267,13 @@ public Object run() throws Exception { verifyDenied(deleteTableAction, USER_RW, USER_RO, USER_NONE, USER_GROUP_READ, USER_GROUP_WRITE); verifyAllowed(deleteTableAction, TABLE_ADMIN); + + // check table permissions is deleted when table is deleted + Set authManagers = SecureTestUtil.getAuthManagers(TEST_UTIL.getHBaseCluster()); + for (AuthManager authManager : authManagers) { + boolean result = authManager.authorizeUserTable(TABLE_ADMIN, tableName, Action.ADMIN); + assertFalse(result); + } } private void createTestTable(TableName tname) throws Exception { @@ -2740,6 +2748,9 @@ public void testGetNamespacePermission() throws Exception { NamespaceDescriptor desc = NamespaceDescriptor.create(namespace).build(); createNamespace(TEST_UTIL, desc); grantOnNamespace(TEST_UTIL, USER_NONE.getShortName(), namespace, Permission.Action.READ); + AuthManager masterAuthManager = + TEST_UTIL.getMiniHBaseCluster().getMaster().getAccessChecker().getAuthManager(); + assertTrue(masterAuthManager.authorizeUserNamespace(USER_NONE, namespace, Action.READ)); // Test 1: A specific namespace getNamespacePermissionsAndVerify(namespace, 1, namespace); @@ -2751,6 +2762,16 @@ public void testGetNamespacePermission() throws Exception { getNamespacePermissionsAndVerify("^test[a-zA-Z]*", 1, namespace); deleteNamespace(TEST_UTIL, namespace); + + // check namespace permission is deleted from master auth manager + masterAuthManager = + TEST_UTIL.getMiniHBaseCluster().getMaster().getAccessChecker().getAuthManager(); + assertFalse(masterAuthManager.authorizeUserNamespace(USER_NONE, namespace, Action.READ)); + // check namespace permission is deleted from RS auth managers + Set authManagers = SecureTestUtil.getAuthManagers(TEST_UTIL.getHBaseCluster()); + for (AuthManager authManager : authManagers) { + assertFalse(authManager.authorizeUserNamespace(USER_NONE, namespace, Action.READ)); + } } /** diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessController3.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessController3.java index d4ae32f06e27..4d30f825a55d 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessController3.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestAccessController3.java @@ -20,10 +20,10 @@ import static org.apache.hadoop.hbase.AuthUtil.toGroupEntry; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import java.util.Set; import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.hbase.Coprocessor; -import org.apache.hadoop.hbase.CoprocessorEnvironment; import org.apache.hadoop.hbase.HBaseClassTestRule; import org.apache.hadoop.hbase.HBaseTestingUtility; import org.apache.hadoop.hbase.HColumnDescriptor; @@ -31,21 +31,17 @@ import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.TableNotFoundException; import org.apache.hadoop.hbase.client.Connection; -import org.apache.hadoop.hbase.coprocessor.CoprocessorHost; -import org.apache.hadoop.hbase.coprocessor.MasterCoprocessorEnvironment; -import org.apache.hadoop.hbase.coprocessor.ObserverContextImpl; -import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment; -import org.apache.hadoop.hbase.coprocessor.RegionServerCoprocessorEnvironment; -import org.apache.hadoop.hbase.master.MasterCoprocessorHost; -import org.apache.hadoop.hbase.regionserver.HRegion; import org.apache.hadoop.hbase.regionserver.HRegionServer; -import org.apache.hadoop.hbase.regionserver.RegionCoprocessorHost; import org.apache.hadoop.hbase.regionserver.RegionServerCoprocessorHost; import org.apache.hadoop.hbase.security.User; +import org.apache.hadoop.hbase.security.access.Permission.Action; import org.apache.hadoop.hbase.testclassification.MediumTests; import org.apache.hadoop.hbase.testclassification.SecurityTests; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.JVMClusterUtil; +import org.apache.hadoop.hbase.zookeeper.ZKUtil; +import org.apache.hadoop.hbase.zookeeper.ZKWatcher; +import org.apache.hadoop.hbase.zookeeper.ZNodePaths; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.ClassRule; @@ -57,9 +53,6 @@ import org.slf4j.LoggerFactory; /** - * Performs checks for reference counting w.r.t. AuthManager which is used by - * AccessController. - * * NOTE: Only one test in here. In AMv2, there is problem deleting because * we are missing auth. For now disabled. See the cleanup method. */ @@ -80,9 +73,6 @@ public class TestAccessController3 extends SecureTestUtil { * gets eclipsed by the system user. */ private static Connection systemUserConnection; - - // user with all permissions - private static User SUPERUSER; // user granted with all global permission private static User USER_ADMIN; // user with rw permissions on column family. @@ -93,8 +83,6 @@ public class TestAccessController3 extends SecureTestUtil { private static User USER_OWNER; // user with create table permissions alone private static User USER_CREATE; - // user with no permissions - private static User USER_NONE; // user with admin rights on the column family private static User USER_ADMIN_CF; @@ -103,51 +91,21 @@ public class TestAccessController3 extends SecureTestUtil { private static final String GROUP_READ = "group_read"; private static final String GROUP_WRITE = "group_write"; - private static User USER_GROUP_ADMIN; - private static User USER_GROUP_CREATE; - private static User USER_GROUP_READ; - private static User USER_GROUP_WRITE; - // TODO: convert this test to cover the full matrix in // https://hbase.apache.org/book/appendix_acl_matrix.html // creating all Scope x Permission combinations private static byte[] TEST_FAMILY = Bytes.toBytes("f1"); - private static MasterCoprocessorEnvironment CP_ENV; - private static AccessController ACCESS_CONTROLLER; - private static RegionServerCoprocessorEnvironment RSCP_ENV; - private static RegionCoprocessorEnvironment RCP_ENV; - - private static boolean callSuperTwice = true; - @Rule public TestName name = new TestName(); - // class with faulty stop() method, controlled by flag - public static class FaultyAccessController extends AccessController { - public FaultyAccessController() { - } - - @Override - public void stop(CoprocessorEnvironment env) { - super.stop(env); - if (callSuperTwice) { - super.stop(env); - } - } - } - @BeforeClass public static void setupBeforeClass() throws Exception { // setup configuration conf = TEST_UTIL.getConfiguration(); // Enable security enableSecurity(conf); - String accessControllerClassName = FaultyAccessController.class.getName(); - // In this particular test case, we can't use SecureBulkLoadEndpoint because its doAs will fail - // to move a file for a random user - conf.set(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, accessControllerClassName); // Verify enableSecurity sets up what we require verifyConfiguration(conf); @@ -155,40 +113,23 @@ public static void setupBeforeClass() throws Exception { conf.setBoolean(AccessControlConstants.EXEC_PERMISSION_CHECKS_KEY, true); TEST_UTIL.startMiniCluster(); - MasterCoprocessorHost cpHost = - TEST_UTIL.getMiniHBaseCluster().getMaster().getMasterCoprocessorHost(); - cpHost.load(FaultyAccessController.class, Coprocessor.PRIORITY_HIGHEST, conf); - ACCESS_CONTROLLER = (AccessController) cpHost.findCoprocessor(accessControllerClassName); - CP_ENV = cpHost.createEnvironment(ACCESS_CONTROLLER, Coprocessor.PRIORITY_HIGHEST, 1, conf); RegionServerCoprocessorHost rsHost; do { rsHost = TEST_UTIL.getMiniHBaseCluster().getRegionServer(0) .getRegionServerCoprocessorHost(); } while (rsHost == null); - RSCP_ENV = rsHost.createEnvironment(ACCESS_CONTROLLER, Coprocessor.PRIORITY_HIGHEST, 1, conf); // Wait for the ACL table to become available TEST_UTIL.waitUntilAllRegionsAssigned(PermissionStorage.ACL_TABLE_NAME); // create a set of test users - SUPERUSER = User.createUserForTesting(conf, "admin", new String[] { "supergroup" }); USER_ADMIN = User.createUserForTesting(conf, "admin2", new String[0]); USER_RW = User.createUserForTesting(conf, "rwuser", new String[0]); USER_RO = User.createUserForTesting(conf, "rouser", new String[0]); USER_OWNER = User.createUserForTesting(conf, "owner", new String[0]); USER_CREATE = User.createUserForTesting(conf, "tbl_create", new String[0]); - USER_NONE = User.createUserForTesting(conf, "nouser", new String[0]); USER_ADMIN_CF = User.createUserForTesting(conf, "col_family_admin", new String[0]); - USER_GROUP_ADMIN = - User.createUserForTesting(conf, "user_group_admin", new String[] { GROUP_ADMIN }); - USER_GROUP_CREATE = - User.createUserForTesting(conf, "user_group_create", new String[] { GROUP_CREATE }); - USER_GROUP_READ = - User.createUserForTesting(conf, "user_group_read", new String[] { GROUP_READ }); - USER_GROUP_WRITE = - User.createUserForTesting(conf, "user_group_write", new String[] { GROUP_WRITE }); - systemUserConnection = TEST_UTIL.getConnection(); setUpTableAndUserPermissions(); } @@ -213,12 +154,7 @@ private static void setUpTableAndUserPermissions() throws Exception { htd.setOwner(USER_OWNER); createTable(TEST_UTIL, htd, new byte[][] { Bytes.toBytes("s") }); - HRegion region = TEST_UTIL.getHBaseCluster().getRegions(TEST_TABLE).get(0); - RegionCoprocessorHost rcpHost = region.getCoprocessorHost(); - RCP_ENV = rcpHost.createEnvironment(ACCESS_CONTROLLER, Coprocessor.PRIORITY_HIGHEST, 1, conf); - // Set up initial grants - grantGlobal(TEST_UTIL, USER_ADMIN.getShortName(), Permission.Action.ADMIN, Permission.Action.CREATE, @@ -277,23 +213,49 @@ private static void cleanUp() throws Exception { } @Test - public void testTableCreate() throws Exception { - AccessTestAction createTable = new AccessTestAction() { - @Override - public Object run() throws Exception { - HTableDescriptor htd = new HTableDescriptor(TableName.valueOf(name.getMethodName())); - htd.addFamily(new HColumnDescriptor(TEST_FAMILY)); - ACCESS_CONTROLLER.preCreateTable(ObserverContextImpl.createAndPrepare(CP_ENV), htd, null); - return null; - } - }; - - // verify that superuser can create tables - verifyAllowed(createTable, SUPERUSER, USER_ADMIN, USER_GROUP_CREATE); - - // all others should be denied - verifyDenied(createTable, USER_CREATE, USER_RW, USER_RO, USER_NONE, USER_GROUP_ADMIN, - USER_GROUP_READ, USER_GROUP_WRITE); + public void testRestartCluster() throws Exception { + // Restart cluster and load permission cache from zk + TEST_UTIL.shutdownMiniHBaseCluster(); + Thread.sleep(2000); + TEST_UTIL.restartHBaseCluster(1); + TEST_UTIL.waitTableAvailable(PermissionStorage.ACL_TABLE_NAME); + AuthManager masterAuthManager = + TEST_UTIL.getMiniHBaseCluster().getMaster().getAccessChecker().getAuthManager(); + assertTrue(masterAuthManager.authorizeUserGlobal(USER_ADMIN, Action.ADMIN)); + assertTrue(masterAuthManager.authorizeUserTable(USER_CREATE, TEST_TABLE, Action.READ)); + assertTrue(masterAuthManager.authorizeUserTable(USER_CREATE, TEST_TABLE, Action.WRITE)); + Set authManagers = SecureTestUtil.getAuthManagers(TEST_UTIL.getHBaseCluster()); + for (AuthManager authManager : authManagers) { + assertTrue(authManager.authorizeUserGlobal(USER_ADMIN, Action.ADMIN)); + assertTrue(authManager.authorizeUserTable(USER_CREATE, TEST_TABLE, Action.READ)); + assertTrue(authManager.authorizeUserTable(USER_CREATE, TEST_TABLE, Action.WRITE)); + } } + @Test + public void testDeleteAclZnodeAndRestartCluster() throws Exception { + // Delete acl znode, restart cluster and master execute a UpdatePermissionProcedure + // to reload acl from table to zk and refresh auth manager cache + TEST_UTIL.shutdownMiniHBaseCluster(); + ZKWatcher watcher = TEST_UTIL.getZooKeeperWatcher(); + String aclZNode = ZNodePaths.joinZNode(watcher.getZNodePaths().baseZNode, + conf.get("zookeeper.znode.acl.parent", ZKPermissionStorage.ACL_NODE)); + ZKUtil.deleteNodeRecursively(watcher, aclZNode); + TEST_UTIL.shutdownMiniHBaseCluster(); + Thread.sleep(2000); + TEST_UTIL.restartHBaseCluster(1); + TEST_UTIL.waitTableAvailable(PermissionStorage.ACL_TABLE_NAME); + Thread.sleep(5000); + AuthManager masterAuthManager = + TEST_UTIL.getMiniHBaseCluster().getMaster().getAccessChecker().getAuthManager(); + assertTrue(masterAuthManager.authorizeUserGlobal(USER_ADMIN, Action.ADMIN)); + assertTrue(masterAuthManager.authorizeUserTable(USER_CREATE, TEST_TABLE, Action.READ)); + assertTrue(masterAuthManager.authorizeUserTable(USER_CREATE, TEST_TABLE, Action.WRITE)); + Set authManagers = SecureTestUtil.getAuthManagers(TEST_UTIL.getHBaseCluster()); + for (AuthManager authManager : authManagers) { + assertTrue(authManager.authorizeUserGlobal(USER_ADMIN, Action.ADMIN)); + assertTrue(authManager.authorizeUserTable(USER_CREATE, TEST_TABLE, Action.READ)); + assertTrue(authManager.authorizeUserTable(USER_CREATE, TEST_TABLE, Action.WRITE)); + } + } } diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestTablePermissions.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestTablePermissions.java index 14ea0b34526e..0d4053c081c6 100644 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestTablePermissions.java +++ b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestTablePermissions.java @@ -303,7 +303,7 @@ public void testPersistence() throws Exception { public void testSerialization() throws Exception { Configuration conf = UTIL.getConfiguration(); ListMultimap permissions = createPermissions(); - byte[] permsData = PermissionStorage.writePermissionsAsBytes(permissions, conf); + byte[] permsData = PermissionStorage.writePermissionsAsBytes(permissions); ListMultimap copy = PermissionStorage.readUserPermission(permsData, conf); diff --git a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestZKPermissionWatcher.java b/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestZKPermissionWatcher.java deleted file mode 100644 index 67c2612caf81..000000000000 --- a/hbase-server/src/test/java/org/apache/hadoop/hbase/security/access/TestZKPermissionWatcher.java +++ /dev/null @@ -1,177 +0,0 @@ -/** - * 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.hbase.security.access; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import org.apache.hadoop.conf.Configuration; -import org.apache.hadoop.hbase.Abortable; -import org.apache.hadoop.hbase.HBaseClassTestRule; -import org.apache.hadoop.hbase.HBaseTestingUtility; -import org.apache.hadoop.hbase.TableName; -import org.apache.hadoop.hbase.Waiter.Predicate; -import org.apache.hadoop.hbase.security.User; -import org.apache.hadoop.hbase.testclassification.LargeTests; -import org.apache.hadoop.hbase.testclassification.SecurityTests; -import org.apache.hadoop.hbase.zookeeper.ZKWatcher; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.ClassRule; -import org.junit.Test; -import org.junit.experimental.categories.Category; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.hbase.thirdparty.com.google.common.collect.ArrayListMultimap; -import org.apache.hbase.thirdparty.com.google.common.collect.ListMultimap; - -/** - * Test the reading and writing of access permissions to and from zookeeper. - */ -@Category({SecurityTests.class, LargeTests.class}) -public class TestZKPermissionWatcher { - - @ClassRule - public static final HBaseClassTestRule CLASS_RULE = - HBaseClassTestRule.forClass(TestZKPermissionWatcher.class); - - private static final Logger LOG = LoggerFactory.getLogger(TestZKPermissionWatcher.class); - private static final HBaseTestingUtility UTIL = new HBaseTestingUtility(); - private static AuthManager AUTH_A; - private static AuthManager AUTH_B; - private static ZKPermissionWatcher WATCHER_A; - private static ZKPermissionWatcher WATCHER_B; - private final static Abortable ABORTABLE = new Abortable() { - private final AtomicBoolean abort = new AtomicBoolean(false); - - @Override - public void abort(String why, Throwable e) { - LOG.info(why, e); - abort.set(true); - } - - @Override - public boolean isAborted() { - return abort.get(); - } - }; - - private static TableName TEST_TABLE = - TableName.valueOf("perms_test"); - - @BeforeClass - public static void beforeClass() throws Exception { - // setup configuration - Configuration conf = UTIL.getConfiguration(); - SecureTestUtil.enableSecurity(conf); - - // start minicluster - UTIL.startMiniCluster(); - AUTH_A = new AuthManager(conf); - AUTH_B = new AuthManager(conf); - WATCHER_A = new ZKPermissionWatcher( - new ZKWatcher(conf, "TestZKPermissionsWatcher_1", ABORTABLE), AUTH_A, conf); - WATCHER_B = new ZKPermissionWatcher( - new ZKWatcher(conf, "TestZKPermissionsWatcher_2", ABORTABLE), AUTH_B, conf); - WATCHER_A.start(); - WATCHER_B.start(); - } - - @AfterClass - public static void afterClass() throws Exception { - WATCHER_A.close(); - WATCHER_B.close(); - UTIL.shutdownMiniCluster(); - } - - @Test - public void testPermissionsWatcher() throws Exception { - Configuration conf = UTIL.getConfiguration(); - User george = User.createUserForTesting(conf, "george", new String[] { }); - User hubert = User.createUserForTesting(conf, "hubert", new String[] { }); - - assertFalse(AUTH_A.authorizeUserTable(george, TEST_TABLE, Permission.Action.READ)); - assertFalse(AUTH_A.authorizeUserTable(george, TEST_TABLE, Permission.Action.WRITE)); - assertFalse(AUTH_A.authorizeUserTable(hubert, TEST_TABLE, Permission.Action.READ)); - assertFalse(AUTH_A.authorizeUserTable(hubert, TEST_TABLE, Permission.Action.WRITE)); - - assertFalse(AUTH_B.authorizeUserTable(george, TEST_TABLE, Permission.Action.READ)); - assertFalse(AUTH_B.authorizeUserTable(george, TEST_TABLE, Permission.Action.WRITE)); - assertFalse(AUTH_B.authorizeUserTable(hubert, TEST_TABLE, Permission.Action.READ)); - assertFalse(AUTH_B.authorizeUserTable(hubert, TEST_TABLE, Permission.Action.WRITE)); - - // update ACL: george RW - List acl = new ArrayList<>(1); - acl.add(new UserPermission(george.getShortName(), Permission.newBuilder(TEST_TABLE) - .withActions(Permission.Action.READ, Permission.Action.WRITE).build())); - ListMultimap multimap = ArrayListMultimap.create(); - multimap.putAll(george.getShortName(), acl); - byte[] serialized = PermissionStorage.writePermissionsAsBytes(multimap, conf); - WATCHER_A.writeToZookeeper(TEST_TABLE.getName(), serialized); - final long mtimeB = AUTH_B.getMTime(); - // Wait for the update to propagate - UTIL.waitFor(10000, 100, new Predicate() { - @Override - public boolean evaluate() throws Exception { - return AUTH_B.getMTime() > mtimeB; - } - }); - Thread.sleep(1000); - - // check it - assertTrue(AUTH_A.authorizeUserTable(george, TEST_TABLE, Permission.Action.READ)); - assertTrue(AUTH_A.authorizeUserTable(george, TEST_TABLE, Permission.Action.WRITE)); - assertTrue(AUTH_B.authorizeUserTable(george, TEST_TABLE, Permission.Action.READ)); - assertTrue(AUTH_B.authorizeUserTable(george, TEST_TABLE, Permission.Action.WRITE)); - assertFalse(AUTH_A.authorizeUserTable(hubert, TEST_TABLE, Permission.Action.READ)); - assertFalse(AUTH_A.authorizeUserTable(hubert, TEST_TABLE, Permission.Action.WRITE)); - assertFalse(AUTH_B.authorizeUserTable(hubert, TEST_TABLE, Permission.Action.READ)); - assertFalse(AUTH_B.authorizeUserTable(hubert, TEST_TABLE, Permission.Action.WRITE)); - - // update ACL: hubert R - List acl2 = new ArrayList<>(1); - acl2.add(new UserPermission(hubert.getShortName(), - Permission.newBuilder(TEST_TABLE).withActions(TablePermission.Action.READ).build())); - final long mtimeA = AUTH_A.getMTime(); - multimap.putAll(hubert.getShortName(), acl2); - byte[] serialized2 = PermissionStorage.writePermissionsAsBytes(multimap, conf); - WATCHER_B.writeToZookeeper(TEST_TABLE.getName(), serialized2); - // Wait for the update to propagate - UTIL.waitFor(10000, 100, new Predicate() { - @Override - public boolean evaluate() throws Exception { - return AUTH_A.getMTime() > mtimeA; - } - }); - Thread.sleep(1000); - - // check it - assertTrue(AUTH_A.authorizeUserTable(george, TEST_TABLE, Permission.Action.READ)); - assertTrue(AUTH_A.authorizeUserTable(george, TEST_TABLE, Permission.Action.WRITE)); - assertTrue(AUTH_B.authorizeUserTable(george, TEST_TABLE, Permission.Action.READ)); - assertTrue(AUTH_B.authorizeUserTable(george, TEST_TABLE, Permission.Action.WRITE)); - assertTrue(AUTH_A.authorizeUserTable(hubert, TEST_TABLE, Permission.Action.READ)); - assertFalse(AUTH_A.authorizeUserTable(hubert, TEST_TABLE, Permission.Action.WRITE)); - assertTrue(AUTH_B.authorizeUserTable(hubert, TEST_TABLE, Permission.Action.READ)); - assertFalse(AUTH_B.authorizeUserTable(hubert, TEST_TABLE, Permission.Action.WRITE)); - } -} diff --git a/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift2/client/ThriftAdmin.java b/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift2/client/ThriftAdmin.java index 1884fb06ea41..c336bca297c0 100644 --- a/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift2/client/ThriftAdmin.java +++ b/hbase-thrift/src/main/java/org/apache/hadoop/hbase/thrift2/client/ThriftAdmin.java @@ -1117,12 +1117,12 @@ public SpaceQuotaSnapshot getCurrentSpaceQuotaSnapshot(TableName tableName) thro } @Override - public void grant(UserPermission userPermission, boolean mergeExistingPermissions) { + public Future grantAsync(UserPermission userPermission, boolean mergeExistingPermissions) { throw new NotImplementedException("grant not supported in ThriftAdmin"); } @Override - public void revoke(UserPermission userPermission) { + public Future revokeAsync(UserPermission userPermission) { throw new NotImplementedException("revoke not supported in ThriftAdmin"); }