HBase分析之simple权限验证
知道了用户的机制,见HBase源码分析之用户,就可以对用户进行权限控制了,HBase提供了AccessController作为自带的认证方式,HBase称之为simple。
1. 配置AccessController
Simple方式的实现类是AccessController,是HBase中自带的,只要在conf/hbase-site.xml中设置好以下属性,即可生效。
<!-- HBase Superuser -->
<property>
<name>hbase.superuser</name>
<value>hbase, admin</value>
</property>
<property>
<name>hbase.security.authentication</name>
<value>simple</value>
</property>
<property>
<name>hbase.security.authorization</name>
<value>true</value>
</property>
<property>
<name>hbase.coprocessor.master.classes</name>
<value>org.apache.hadoop.hbase.security.access.AccessController</value>
</property>
<property>
<name>hbase.coprocessor.region.classes</name>
<value>org.apache.hadoop.hbase.security.access.AccessController</value>
</property>
<property>
<name>hbase.coprocessor.regionserver.classes</name>
<value>org.apache.hadoop.hbase.security.access.AccessController</value>
</property>
AccessController实现了CoprocessorService、AccessControlService.Interface,通过Java或者命令行执行grant、revoke操作时,会相应的调用AccessController的grant、revoke方法,方法中会将配置的权限存进hbase:acl中。
// AccessController
public void grant(RpcController controller,
AccessControlProtos.GrantRequest request,
RpcCallback<AccessControlProtos.GrantResponse> done) {
...
AccessControlLists.addUserPermission(regionEnv.getConfiguration(), perm);
...
}
// AccessControlLists
static void addUserPermission(Configuration conf, UserPermission userPerm)
throws IOException {
...
try (Connection connection = ConnectionFactory.createConnection(conf)) {
try (Table table = connection.getTable(ACL_TABLE_NAME)) {
table.put(p);
}
}
}
配置一条权限用于测试,给masa赋予表table_name的RW(读写)权限
hbase(main):001:0> grant 'masa', 'RW', 'table_name'
0 row(s) in 0.6260 seconds
scan一下hbase:acl的表,里面已经有刚配置的记录了。然而并没有superuser的相关记录,但是superuser确实拥有所有的权限,这个问题第2节会提到。
hbase(main):001:0> scan 'hbase:acl'
ROW COLUMN+CELL
table_name column=l:masa, timestamp=1505188428592, value=RW
1 row(s) in 0.2620 seconds
2. 验证权限
Coprocessor提供了在各个操作之前和之后的回调,相应的可以从方法名中看出,例如:preScannerOpen、postScannerOpen。AccessController继承了Coprocessor,在操作前回调pre里会调用AccessController的permissionGranted方法来判断是否有权限执行permRequest这个Action。
AuthResult permissionGranted(String request, User user, Action permRequest,
RegionCoprocessorEnvironment e,
Map<byte [], ? extends Collection<?>> families) {
HRegionInfo hri = e.getRegion().getRegionInfo();
TableName tableName = hri.getTable();
// 如果是访问的meta region,并且是读操作,则允许
if (hri.isMetaRegion()) {
if (permRequest == Action.READ) {
return AuthResult.allow(request, "All users allowed", user,
permRequest, tableName, families);
}
}
// 没有设置用户时,拒绝访问
if (user == null) {
return AuthResult.deny(request, "No user associated with request!", null,
permRequest, tableName, families);
}
// 判断是否有这张表的permRequest权限
if (authManager.authorize(user, tableName, (byte[])null, permRequest)) {
return AuthResult.allow(request, "Table permission granted", user,
permRequest, tableName, families);
}
// 判断是否有参数families的permRequest权限
if (families != null && families.size() > 0) {
// 所有family必须都有permRequest权限,才认为有执行permRequest的权限
for (Map.Entry<byte [], ? extends Collection<?>> family : families.entrySet()) {
// family是否有权限分两种情况,一种,这个family就是有权限
if (authManager.authorize(user, tableName, family.getKey(),
permRequest)) {
continue;
}
// 另一种,这个family下得所有qualifier都有权限
if ((family.getValue() != null) && (family.getValue().size() > 0)) {
if (family.getValue() instanceof Set) {
Set<byte[]> familySet = (Set<byte[]>)family.getValue();
for (byte[] qualifier : familySet) {
if (!authManager.authorize(user, tableName, family.getKey(),
qualifier, permRequest)) {
return AuthResult.deny(request, "Failed qualifier check", user,
permRequest, tableName, makeFamilyMap(family.getKey(), qualifier));
}
}
} else if (family.getValue() instanceof List) {
List<KeyValue> kvList = (List<KeyValue>)family.getValue();
for (KeyValue kv : kvList) {
if (!authManager.authorize(user, tableName, family.getKey(),
kv.getQualifier(), permRequest)) {
return AuthResult.deny(request, "Failed qualifier check", user,
permRequest, tableName, makeFamilyMap(family.getKey(), kv.getQualifier()));
}
}
}
} else {
return AuthResult.deny(request, "Failed family check", user, permRequest,
tableName, makeFamilyMap(family.getKey(), null));
}
}
return AuthResult.allow(request, "All family checks passed", user, permRequest,
tableName, families);
}
// 条件都不满足,还是deny
return AuthResult.deny(request, "No families to check and table permission failed",
user, permRequest, tableName, families);
}
上面这段代码调用了3次 TableAuthManager 的authorize方法,都是public boolean authorize(User user, TableName table, byte[] family, byte[] qualifier, Permission.Action action)方法参数不同的调用。在验证表权限时,调用family和qualifier传null,验证family时,qualifier传null,验证qualifier时,参数都传。
public boolean authorize(User user, TableName table, byte[] family,
byte[] qualifier, Permission.Action action) {
// 认证用户是否有权限
if (authorizeUser(user, table, family, qualifier, action)) {
return true;
}
// 认证用户的组是否有权限
String[] groups = user.getGroupNames();
if (groups != null) {
for (String group : groups) {
if (authorizeGroup(group, table, family, qualifier, action)) {
return true;
}
}
}
return false;
}
验证用户是否有权限,验证用户组的权限其实和用户是类似的,这里只看用户权限的认证。
public boolean authorizeUser(User user, TableName table, byte[] family,
byte[] qualifier, Permission.Action action) {
if (table == null) table = AccessControlLists.ACL_TABLE_NAME;
// 检查是否有Namespace权限
if (authorize(user, table.getNamespaceAsString(), action)) {
return true;
}
// 检查是否有表权限
return authorize(getTablePermissions(table).getUser(user.getShortName()), table, family,
qualifier, action);
}
Namespace权限的认证过程,认证过程第一步authorize(user, action),方法里通过globalCache判断用户是否是超级用户,这一步直接跳过了表级的验证过程,所以超级用户的权限是在hbase:acl里看不到的。第二步,从nsCache中拿到对应namespace的权限列表,认证权限。globalCache和nsCache相关内容在第三节。
public boolean authorize(User user, String namespace, Permission.Action action) {
// 认证用户是否是超级用户
if (authorize(user, action)) {
return true;
}
// 认证用户是否有Namespace权限,从Cache里拿到权限的列表
PermissionCache<TablePermission> tablePerms = nsCache.get(namespace);
if (tablePerms != null) {
List<TablePermission> userPerms = tablePerms.getUser(user.getShortName());
if (authorize(userPerms, namespace, action)) {
return true;
}
String[] groupNames = user.getGroupNames();
if (groupNames != null) {
for (String group : groupNames) {
List<TablePermission> groupPerms = tablePerms.getGroup(group);
if (authorize(groupPerms, namespace, action)) {
return true;
}
}
}
}
return false;
}
Table权限的认证过程,顺序遍历Cache中拿到的权限列表,寻找匹配的权限。找到了,return true,否则return false。
private boolean authorize(List<TablePermission> perms,
TableName table, byte[] family,
byte[] qualifier, Permission.Action action) {
if (perms != null) {
for (TablePermission p : perms) {
if (p.implies(table, family, qualifier, action)) {
return true;
}
}
}
return false;
}
单个权限的判断过程,各个值的比较,都符合返回true。
public boolean implies(TableName table, byte[] family, byte[] qualifier,
Action action) {
if (!this.table.equals(table)) {
return false;
}
if (this.family != null && (family == null || !Bytes.equals(this.family, family))) {
return false;
}
if (this.qualifier != null && (qualifier == null || !Bytes.equals(this.qualifier, qualifier))) {
return false;
}
// check actions
return super.implies(action);
}
3. 权限读取和更新
权限是从配置文件和hbase:acl表中读取出来的,都存在TableAuthManager中,分为globalCache、nsCache和tableCache。
private TableAuthManager(ZooKeeperWatcher watcher, Configuration conf)
throws IOException {
this.conf = conf;
// 读取globalCache
globalCache = initGlobal(conf);
this.zkperms = new ZKPermissionWatcher(watcher, this, conf);
try {
// 读取nsCache和tableCache,还有一部分globalCache
this.zkperms.start();
} catch (KeeperException ke) {
LOG.error("ZooKeeper initialization failed", ke);
}
}
3.1 读取globalCache
在创建TableAuthManager的时候,调用initGlobal,从配置中读取超级用户相关的权限信息,并把启动HBase的用户加入超级用户列表,从这里就可以看到,为什么在没有配置超级用户时,启动HBase的用户就是超级用户,当然,配置了超级用户,启动HBase的用户依然是超级用户。
private PermissionCache<Permission> initGlobal(Configuration conf) throws IOException {
// 获取当前用户
UserProvider userProvider = UserProvider.instantiate(conf);
User user = userProvider.getCurrent();
if (user == null) {
throw new IOException("Unable to obtain the current user, " +
"authorization checks for internal operations will not work correctly!");
}
PermissionCache<Permission> newCache = new PermissionCache<Permission>();
String currentUser = user.getShortName();
// 从配置中读取超级用户,并把系统当前用户加入列表
List<String> superusers = Lists.asList(currentUser, conf.getStrings(
Superusers.SUPERUSER_CONF_KEY, new String[0]));
if (superusers != null) {
for (String name : superusers) {
// 判断是用户还是组
if (AuthUtil.isGroupPrincipal(name)) {
newCache.putGroup(AuthUtil.getGroupName(name),
new Permission(Permission.Action.values()));
} else {
newCache.putUser(name, new Permission(Permission.Action.values()));
}
}
}
return newCache;
}
虽然看起来,在初始化超级用户列表的时候,只有配置的超级用户和系统当前用户加入了超级用户列表,实质上对hbase:acl表有读写权限的用户都会被加入超级用户列表。这个操作是在ZKPermissionWatcher的start方法中执行的,过程比较简单,最终会调用到refreshTableCacheFromWritable,这个方法在第3.2节中会详细说明。
3.2 读取nsCache和tableCache
在start中,首先将自己注册到watcher(ZooKeeperWatcher)中,然后读取acl的数据,写入缓存。
public void start() throws KeeperException {
try {
// 向ZooKeeperWatcher注册自己
watcher.registerListener(this);
// 读取acl,更新缓存
if (ZKUtil.watchAndCheckExists(watcher, aclZNode)) {
List<ZKUtil.NodeAndData> existing =
ZKUtil.getChildDataAndWatchForNewChildren(watcher, aclZNode);
if (existing != null) {
refreshNodes(existing);
}
}
} finally {
initialized.countDown();
}
}
refreshNodes方法中会遍历acl中所有的数据节点,分别调用refreshAuthManager方法。refreshAuthManager中判断节点是Namespace还是Table,分别写入TableAuthManager的nsCache和tableCache中。
private void refreshAuthManager(String entry, byte[] nodeData) throws IOException {
if(AccessControlLists.isNamespaceEntry(entry)) {
authManager.refreshNamespaceCacheFromWritable(
AccessControlLists.fromNamespaceEntry(entry), nodeData);
} else {
authManager.refreshTableCacheFromWritable(TableName.valueOf(entry), nodeData);
}
}
在更新表缓存时,判断了如果当前表是hbase:acl表,就把当前权限规则写入globalCache中,不是hbase:acl表,才会写入tableCache。这里就是之前提到的,对hbase:acl表有权限的用户,也是超级用户的源码所在。
public void refreshTableCacheFromWritable(TableName table,
byte[] data) throws IOException {
...
if (Bytes.equals(table.getName(), AccessControlLists.ACL_GLOBAL_NAME)) {
updateGlobalCache(perms);
} else {
updateTableCache(table, perms);
}
...
}
3.3 更新缓存
更新缓存同步的其实是hbase:acl表中的数据,配置文件不会更新。之前提到在start中,将自己注册到watcher(ZooKeeperWatcher)中,本质是实现ZooKeeperListener的监听。ZooKeeperListener有4个方法,
nodeCreated、nodeDataChanged、nodeChildrenChanged和nodeDataChanged。在这4个方法中都会调用refreshAuthManager来更新缓存,在3.2中也讲过,初始化时更新nsCache和tableCache用的是同一个方法。
private void refreshAuthManager(String entry, byte[] nodeData) throws IOException {
if(AccessControlLists.isNamespaceEntry(entry)) {
authManager.refreshNamespaceCacheFromWritable(
AccessControlLists.fromNamespaceEntry(entry), nodeData);
} else {
authManager.refreshTableCacheFromWritable(TableName.valueOf(entry), nodeData);
}
}
-END-