要做一个python web系统,做简单的信息管理和案例展示,还要考虑后续功能的扩展。就想着自己搭建一个python web框架。本来想着像.net、java一样,有现成的脚手架项目,拿来改吧改吧就用了,结果发现Python web这一块还真是奇葩:
- 很多python web应用或脚手架的维护3~4年前就已经停止维护了
- 只有python web的基础框架,像flask、Django,倒是维护的挺活跃。但是flask太轻,需要自己集成;Diango太重,想要的不想要的都给你集成在一起了。
那要不我们自己造个轮子吧。本篇造轮子之-casbin访问控制集成。
关于casbin是什么,你可以点击链接自己查看。总的来说,Casbin 是一个强大和高效的开放源码访问控制库,它支持各种访问控制模型以强制全面执行授权。简单来说,就是访问控制,看你有没有权限访问请求的资源。
1.Flask集成访问控制
-
首先有一个Casbin官方提供的基础实现库PyCasbin,实现了casbin的两大基本概念模型和策略,以及模型和策略的加载和验证。功能够用,但是集成到flask的话,需要我们手动设置flask与casbin实例的关联,需要实现一些注解,方便权限配置。因此我们就找到了flask-authz。有个flask-casbin,名字看上去更像是casbin到flask的集成库,但是已经合并到flask-authz了。
-
flask-authz如我们所愿,提供以下功能
- 实例化casbin,根据配置加载权限模型和策略,并进行实例绑定
- 提供访问控制注解,方便进行权限判断
flask-authz会从flask配置中读取CASBIN_MODEL配置项,用以初始化casbin框架的模型信息。模型初始化完成,那另一大概念策略,怎么初始化呢?
# casbin config
CASBIN_MODEL = os.path.join(basedir, 'casbinmodel.conf')
def create_app(config_name):
app = Flask(__name__)
app.config["CASBIN_MODEL"] = CASBIN_MODEL
register_extensions(app)
# init casbin
casbin_enforcer = CasbinEnforcer(app)
app.casbin_enforcer = casbin_enforcer
...
- 简单的加载策略的方式,可以从csv加载。但是这种方式在系统访问权限维护更新时,使用起来不是很方便。casbin_sqlalchemy_adapter提供了一种基于数据库的策略加载和保存方式。
SQLAlchemy Adapter is the SQLAlchemy adapter for PyCasbin. With this library, Casbin can load policy from SQLAlchemy supported database or save policy to it.
- casbin_sqlalchemy_adapter提供的数据结构是多元组的形式,比如三元组:sub,obj,act。数据加载和存储都是直接从数据库多元组加载数据。
v0 | v1 | v2 | v3 | v4 | v5 |
---|---|---|---|---|---|
p | bob | data2 | write | allow | |
p | data2_admin | data2 | read | allow | |
p | data2_admin | data2 | write | allow | |
p | alice | data2 | write | deny | |
g | alice | data2_admin |
- 这种存储形式是casbin的设计理念的直接体现。但是对于与传统基于关系数据库设计的RBAC权限管理框架来说,却不能直接实现对接。不管是用户权限数据的获取,还是权限数据的更新,都需要进行转化。因此我觉得casbin_sqlalchemy_adapter并没有解决我的问题,我需要一个可以跟我的RBAC管理框架兼容的策略加载和更新方式。
2.初始化策略加载
基于RBAC设计的表结构,如何与casbin三元组权限对接。找了一圈没有找到可用的集成库,那就只有自己动手了,看上去也没有那么复杂。
- user表
id | name |
---|---|
1 | bob |
2 | alice |
- role表
id | name |
---|---|
1 | role1 |
2 | role2 |
- permission表
id | name |
---|---|
1 | resource1 |
2 | resource2 |
- user_role表
id | user_id | role_id |
---|---|---|
1 | 1 | 1 |
2 | 1 | 2 |
- role_permission表
id | role_id | permission_id |
---|---|---|
1 | 1 | 1 |
2 | 1 | 2 |
- flask应用初始化时触发策略的加载
def create_app(config_name):
...
# load policy
with app.app_context(): # 手动创建app_context,用于数据查询获取casbin初始化policy
AuthService.load_all_role_policy()
AuthService.load_all_user_policy()
...
这里的实现有个缺陷,就是手动创建了一个app_context。这样看上去不太优雅,其实完全可以像flask扩展初始化的形式,构建init_**方法,然后在方法参数中传入app实例即可。大概像这样
def create_app(config_name):
app = Flask(__name__)
app.config.from_object(config_by_name[config_name])
...
# load policy
casbin_policy.init_policy(app)
...
当然要是这样,角色权限、用户权限初始加载查询方法的app也不再访问current_app。这里暂未修改,有兴趣的可以考虑一下。
- 角色权限、用户权限初始加载查询方法
from flask import current_app
from ..model import User, Role
class AuthService:
# 加载所有用户权限casbin
@classmethod
def load_all_user_policy(cls):
users = User.query.filter_by(status=1).all()
for user in users:
if user.roles is not None and len(user.roles) > 0:
cls.load_user_policy(user)
current_app.logger.info("user_roles:{0}".format(current_app.casbin_enforcer.e.get_grouping_policy()))
# 加载单个用户权限到casbin
@staticmethod
def load_user_policy(user):
current_app.casbin_enforcer.e.delete_roles_for_user(user.employee_no)
for role in user.roles:
current_app.casbin_enforcer.e.add_role_for_user(user.employee_no, role.name)
current_app.logger.info("update user_roles:{0}".format(current_app.casbin_enforcer.e.get_grouping_policy()))
# 加载所有角色权限到casbin
@classmethod
def load_all_role_policy(cls):
roles = Role.query.filter_by(status=1).all()
for role in roles:
cls.load_role_policy(role)
current_app.logger.info("role_permissions:{0}".format(current_app.casbin_enforcer.e.get_policy()))
# 加载单个角色权限到casbin
@staticmethod
def load_role_policy(role):
current_app.casbin_enforcer.e.delete_role(role.name)
for permission in role.permissions:
current_app.casbin_enforcer.e.add_permission_for_user(role.name, permission.path, permission.method)
current_app.logger.info("update role_permissions:{0}".format(current_app.casbin_enforcer.e.get_policy()))
这样就实现了,应用初始化时,从关系数据库用户权限相关记录中,加载casbin策略信息,实现对RBAC记录的无缝对接。做为一个功能完备的web系统,用户权限更新是再正常不过的事情,那用户权限更新时,如何同步更新casbin模式中的策略信息呢?
3.权限更新
其实思路是挺简单的,就是达到信息的同步更新嘛。
- 找到casbin策略更新接口
接口名称 | 接口描述 |
---|---|
add_role_for_user | 添加用户角色 |
delete_role_for_user | 删除用户角色 |
add_permission_for_user | 添加角色权限 |
delete_permission_for_user | 删除角色权限 |
- 用户权限更新时(添加用户、更新用户权限、删除用户,添加角色、更新角色权限、删除角色),调用casbin策略更新接口
权限操作 | 接口调用 |
---|---|
添加用户 | add_role_for_user |
更新用户角色 | delete_role_for_user&add_role_for_user |
删除用户 | delete_role_for_user |
添加角色 | add_permission_for_user |
更新角色权限 | delete_permission_for_user&add_permission_for_user |
删除角色 | delete_permission_for_user |
来个删除用户的示例吧
from flask import current_app
from .base_model import BaseModel
class User(BaseModel):
def remove(self):
self.query.filter_by(id=self.id).update({'status': self.status_remove})
ret = db.session.commit()
if ret:
current_app.casbin_enforcer.e.delete_roles_for_user(self.id)
return ret
Python Web这一块由于项目,折腾了几个框架,接下来会陆续整理出来,看看能不能有一个系列的知识体系。