Superset的权限控制

关于这一章,我们主要从几个方面来看下Superset如何使用Flask提供的权限框架,如何去自定义认证流程。如何去明确用户是否有权限去访问接口,也就是权限的访问流程。

  1. 如何自定义认证框架

在Flask框架之中,支持多种认证方式

# AUTH_OID : Is for OpenID
# AUTH_DB : Is for database (username/password)
# AUTH_LDAP : Is for LDAP
# AUTH_REMOTE_USER : Is for using REMOTE_USER from web server

其配置默认是AUTH_DB,也即是声明了通过账号密码的方式进行登录。

在声明完使用的认证方式之后。相关的组合配置项位于

flask_appbuilder.security.sqla.manager.SecurityManager下

register_views之中,在其中,根据auth_type进行了判断

if self.auth_type == AUTH_DB:
self.user_view = self.userdbmodelview
self.auth_view = self.authdbview()

elif self.auth_type == AUTH_LDAP:
self.user_view = self.userldapmodelview
self.auth_view = self.authldapview()
elif self.auth_type == AUTH_OAUTH:
self.user_view = self.useroauthmodelview
self.auth_view = self.authoauthview()
elif self.auth_type == AUTH_REMOTE_USER:
self.user_view = self.userremoteusermodelview
self.auth_view = self.authremoteuserview()
else:
self.user_view = self.useroidmodelview
self.auth_view = self.authoidview()
if self.auth_user_registration:
pass
# self.registeruser_view = self.registeruseroidview()
# self.appbuilder.add_view_no_menu(self.registeruser_view)

如果我们想要客制化自己的登录流程,比如说复写一个AUTH_DB

那么就可以书写一个自己的类,继承AuthDBView

重写属于自己的login方法即可。

class CustomAuthDBView(AuthDBView):
@expose(“/login/”, methods=[“GET”, “POST”])
def login(self):
pass

之后在superset.security.manager.SupersetSecurityManager类中覆盖原本的authview属性即可。

class SupersetSecurityManager( # pylint: disable=too-many-public-methods
SecurityManager
):
userstatschartview = None
user_model = CustomUser
authdbview = CustomAuthDBView

这里我们还是以AuthDBView为例,看看他们内部做了什么

class AuthDBView(AuthView):
login_template = “appbuilder/general/security/login_db.html”

@expose(“/login/”, methods=[“GET”, “POST”])
def login(self):
if g.user is not None and g.user.is_authenticated:
return redirect(self.appbuilder.get_url_for_index)
form = LoginForm_db()
if form.validate_on_submit():
user = self.appbuilder.sm.auth_user_db(
form.username.data, form.password.data
)
if not user:
flash(as_unicode(self.invalid_login_message), “warning”)
return redirect(self.appbuilder.get_url_for_login)
login_user(user, remember=False)
next_url = request.args.get(“next”, “”)
return redirect(get_safe_redirect(next_url))
return self.render_template(
self.login_template, title=self.title, form=form, appbuilder=self.appbuilder
)

首先是判断用户是否已经登录了。

如果没有登录,才会利用数据库查询是否存在相关的user

之后是login_user,这个方法中会将user保存在session之中。

if not force and not user.is_active:
return False

user_id = getattr(user, current_app.login_manager.id_attribute)()
session[“_user_id”] = user_id
session[“_fresh”] = fresh
session[“_id”] = current_app.login_manager._session_identifier_generator()

if remember:
session[“_remember”] = “set”
if duration is not None:
try:
# equal to timedelta.total_seconds() but works with Python 2.6
session[“_remember_seconds”] = (
duration.microseconds
+ (duration.seconds + duration.days * 24 * 3600) * 10**6
) / 10.0**6
except AttributeError as e:
raise Exception(
f”duration must be a datetime.timedelta, instead got: {duration}”
) from e

current_app.login_manager._update_request_context_with_user(user)
user_logged_in.send(current_app._get_current_object(), user=_get_user())
return True

登录完成之后进行相关的回调。

在说完了如何设置使用怎么样的认证方式和Flask中数据库的登录过程之后,我们看下Flask是如何判断用户有访问菜单以及api权限的过程。

在Superset中,绘制页面中存在的菜单入口在base.py

下的common_bootstrap_payload函数中

def common_bootstrap_payload(user: User) -> Dict[str, Any]:
return {
**(cached_common_bootstrap_data(user)),
“flash_messages”: get_flashed_messages(with_categories=True),
}

在其中主要是 cached_common_bootstrap_data 函数之中

而在cached_common_bootstrap_data函数中,则是主要利用了menu = appbuilder.menu.get_data() 这一句代码获取到菜单。

而在appbuilder.menu.get_data之中,则利用get_user_menu_access函数获取到了所有有权限的菜单。

在内部调用了_get_user_permission_view_menus函数

在函数之中,根据user所拥有的role,role所挂载的permission_view来判断是否具有menu的访问权限。

这里我们展开说说Flask之中的权限框架

user绑定role

role绑定 permission_view

permission_view 主要由两部分组成,permission,view_menu

其中view_menu可以看作是资源,比如Superset中的Dataset,DataSource,Chart这一类的资源概念。

而permission则是可以对资源进行什么操作,比如can_read, can_write, can_get

这一类的实际操作

实际拼接起来,则显示为

can_read on Dataset

诸如这样的permission_view

那么上面的_get_user_permission_view_menus 函数就是查询当前用户所有可以menu_access的permission_view。

在获取到所有有权限的菜单之后,将所有的menu对象进行遍历,判断是否有权限,并进行递归拼接”childs”: self.get_data(menu=item.childs),最后进行返回。

for i, item in enumerate(menu):
if not item.should_render():
continue

if item.name == “-” and not i == len(menu) – 1:
ret_list.append(“-“)
elif item.name not in allowed_menus:
continue
elif item.childs:
ret_list.append(
{
“name”: item.name,
“icon”: item.icon,
“label”: __(str(item.label)),
“childs”: self.get_data(menu=item.childs),
}
)
else:
ret_list.append(
{
“name”: item.name,
“icon”: item.icon,
“label”: __(str(item.label)),
“url”: item.get_url(),
}
)
return ret_list

通过这种方式,获取到了当前用户可以看到的menu。

我们当然可以在menu = appbuilder.menu.get_data()之后做一些工作,从而修改或者添加我们想要客制化的菜单。

在说完了菜单之后,我们可以看下superset如何判断一个用户如何有权限访问一个api接口的,这里我们以

DashboardApi中的get_list为例,看下如何判断用户是否可以调用这个api

在这个api之上存在注解@has_access

has_access这个装饰器会将代码进行包装

def wraps(self, *args, **kwargs):
permission_str = f”{PERMISSION_PREFIX}{f._permission_name}”
if self.method_permission_name:
_permission_name = self.method_permission_name.get(f.__name__)
if _permission_name:
permission_str = f”{PERMISSION_PREFIX}{_permission_name}”
if permission_str in self.base_permissions and self.appbuilder.sm.has_access(
permission_str, self.class_permission_name
):
return f(self, *args, **kwargs)
else:
log.warning(
LOGMSG_ERR_SEC_ACCESS_DENIED.format(
permission_str, self.__class__.__name__
)
)
flash(as_unicode(FLAMSG_ERR_SEC_ACCESS_DENIED), “danger”)
return redirect(
url_for(
self.appbuilder.sm.auth_view.__class__.__name__ + “.login”,
next=request.url,
)
)

f._permission_name = permission_str
return functools.update_wrapper(wraps, f)

其中的重点在前半段

permission_str = f”{PERMISSION_PREFIX}{f._permission_name}”
if self.method_permission_name:
_permission_name = self.method_permission_name.get(f.__name__)
if _permission_name:
permission_str = f”{PERMISSION_PREFIX}{_permission_name}”
if permission_str in self.base_permissions and self.appbuilder.sm.has_access(
permission_str, self.class_permission_name
):
return f(self, *args, **kwargs)

就是拼接了permission_str 和 class_permission_name

然后调用了sm.has_access 来判断是否有权限,sm中的has_access类似上面的菜单判断权限,就是根据用户的role,来判断是否有这一条权限,从而返回true或false。

主要是拼接上面的permission_str 和 class_permission_name

class_permission_name中则是在类中声明的字段

class_permission_name = “Dashboard”

而声明_permission_name,会尝试读取类中的method_permission_name字段

method_permission_name = MODEL_API_RW_METHOD_PERMISSION_MAP

利用Map进行转换,如果读取不到,则会使用方法名作为permission_name

最后拼接为

can_list on Dashboard

如果不想要声明在method_permission_name字段之中,可以利用

@permission_name(“get”)

注解来修改_permission_name.

那么这样就是框架的认证方式选择,以及如何去判断用户能看到哪些菜单,以及用户能访问哪些api的全过程总结。其中基本上都是Superset使用的Flask框架本身的功能。

发表评论

邮箱地址不会被公开。 必填项已用*标注