背景
Spark如今已经是我们最常用的计算和查询引擎之一,但是很遗憾的是社区版的Spark本身没有任何权限控制手段(据说Spark的Thrift server服务支持create/drop权限控制,但我并未试过,而且只有这种层次的控制并不够),我们当然可以在应用层通过业务功能来从平台上控制用户的权限,但随着业务进化,从中间件层次对Spark做出和hive一样的权限控制也十分重要。由于目前还没有很成熟方案,下面是我的一些探索,给有同样需求的同学一些思路。
1. 通过控制hive权限来控制Spark权限
我们平时使用spark大多数是通过hive元数据来获取数仓中的库表信息,那么我们是否可以通过控制hive的权限来间接控制spark呢?
先说结论,不可行。下边是尝试过程。
首先我们通过root用户,在数仓的db_test1数据库下创建了一张test1表,并插入了简单的数据。结构很简单:
下面我们使用root用户通过hive cli查询:
可以正常查出数据,没有问题,接下来我们切换非同组的另一不相关的admin用户,同样通过hive cli来查询这张表:
提示我们没有查询权限(hive权限控制启用方法此处不再赘述),可见hive权限控制是成功的,下面我们仍然使用admin用户,进入spark-sql cli查询:
Spark仍然成功查到了数据,可见hive的权限控制是不会影响Spark查询的,猜测hive应该是在执行SQL的时候,通过HiveParser解析SQL时去进行权限控制的,而Spark只是使用了hive的元数据信息,所以不会受到权限影响。
2.改造Spark源码控制SQL权限
在hive权限不会影响spark权限的情况下,如果我们想要和hive共享同一套权限怎么办呢?我们可以尝试去改造Spark源码并重新编译,思路是通过Aspect拦截Spark的方法,实现权限控制。
下载Spark源码后,我们在sql module下新增一个aspect package,新建了一个名为SparkSqlAspect的Scala类
下面是我添加的代码:
@Aspect
class SparkSqlAspect {
val execution = "execution(public org.apache.spark.sql.Dataset<org.apache.spark.sql.Row> " +
"org.apache.spark.sql.SparkSession.sql(java.lang.String)) && args(sqlRaw)"
@Around(execution)
def around(pjp: ProceedingJoinPoint, sqlRaw: String): Dataset[Row] = {
val sql = sqlRaw.trim
val spark = pjp
.getThis
.asInstanceOf[SparkSession]
val userName = spark
.sparkContext
.sparkUser
if (accessControl(sql, userName, spark)) {
pjp
.proceed(pjp.getArgs)
.asInstanceOf[Dataset[Row]]
} else {
throw new IllegalAccessException("Permission denied")
}
}
/**
* 权限控制
* @param sql sql内容
* @param userName 用户名
* @param spark SparkSession
* @return Boolean
*/
def accessControl(sql: String, userName: String, spark: SparkSession): Boolean = {
val logicalPlan = spark.sessionState.sqlParser.parsePlan(sql)
import org.apache.spark.sql.catalyst.analysis.UnresolvedRelation
val tableSeq = logicalPlan
.collect { case r: UnresolvedRelation => r.tableName }
// 结合sql,tableSeq,userName进行权限控制
true
}
}
通过拦截spark.sql()等方法,来进行权限控制,比如此处可以链接到hive元数据结合校验。改造完成后,需要重新编译源码。这种方法是可行的,但是不是很推荐,因为对日后升级版本很不利,而且重新编译源码费时费力。
3.通过控制Hadoop的HDFS文件权限来控制Spark权限
HDFS文件系统本身是有rwx这种权限控制的,我们可以通过控制HDFS文件的权限来影响Spark权限,我们仍然使用1中的db_test1库测试,分别使用root和admin用户在这个库下建立两张表test1和test2。
可以看到默认的表权限为755,理论上我们通过Spark是可以任意查询数据的,下面我们使用root用户进入spark-sql cli,查询test2这张表:
成功查询出了数据,接下来我们修改test2的权限为700:
再次使用root用户在spark-sql cli下查询:
提示没有权限,可见权限控制成功了。
那么这个权限是否也会影响hive呢?答案是肯定的,我们使用root用户在hive cli下尝试:
同样的效果。主流的Sentry等框架,也是基于这个思路来进行权限控制的。
控制HDFS权限是一种更为底层的控制方式,假设root用户在HDFS上拥有了test2库的读取权限,但在hive上未赋权,那在hive中仍然是无法查询的:
但在Spark上是可以查询的,原因前边已经论述过了。
到此为止,看起来我们已经可以控制SparkSQL的权限了,但这种方式真的足够安全吗?
需要注意的是,Hadoop开发之初,是假定Hadoop工作在安全的集群环境下的,所以只有单纯的文件权限这种控制。实际这是比较薄弱的,并不是一种非常安全的控制方式,因为Hadoop在登陆时,获取用户名的方式是这样的:
在使用了kerberos的情况下,从javax.security.auth.kerberos.KerberosPrincipal的实例获取username。
在未使用kerberos的情况下,优先读取HADOOP_USER_NAME这个系统环境变量
如果没有读取到,则读取HADOOP_USER_NAME这个java环境变量。
如果没有读取到,则从com.sun.security.auth.NTUserPrincipal或com.sun.security.auth.UnixPrincipal的实例获取username。
如果以上尝试都失败,那么抛出异常LoginException(“Can’t find user name”)。
最终拿username构造org.apache.hadoop.security.User的实例添加到Subject中。
可见我们完全可以模拟一个用户来骗过服务器,我们写一个简单的Demo:
object StartTest {
val logger: Logger = LoggerFactory.getLogger(StartTest.getClass)
def main(args: Array[String]): Unit = {
System.setProperty("HADOOP_USER_NAME", "root")
SparkSession
.builder()
.appName("test")
.enableHiveSupport()
.master("local[*]")
.getOrCreate()
.sql("select * from db_test1.test1")
.rdd
.collect()
}
}
还是刚才的权限环境,我们通过 System.setProperty(“HADOOP_USER_NAME”, “root”),把用户名写进JAVA环境变量中,将自己的身份伪造成root用户,这样,无论使用哪个用户执行这段程序,都可以读取到db_test1.test1的内容。
可见如果想做到更强力的权限控制,还是应该考虑结合kerberos做身份鉴别,由kerberos保证登录到集群的用户就是他所声称的用户,由HDFS权限来决定用户的权限。