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-

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容