Package: cerbos-sqlalchemy
Cerbos + SQLAlchemy AdapterAn adapter library that takes aCerbosQuery Plan (PlanResources API) response and converts it into aSQLAlchemySelect instance. This is designed to work alongside a project using theCerbos Python SDK.The following conditions are supported:and,or,not,eq,ne,lt,gt,le(lte),ge(gte) andin. Other operators (eg math operators) can be implemented programatically, and attached to the query object via thequery.where(...)API.RequirementsCerbos > v0.16SQLAlchemy >= 1.4 / 2.0Usagepip install cerbos-sqlalchemyfromcerbos.sdk.clientimportCerbosClientfromcerbos.sdk.modelimportPrincipal,ResourceDescfromcerbos_sqlalchemyimportget_queryfromsqlalchemyimportColumn,Integer,Stringfromsqlalchemy.ormimportdeclarative_basefromsqlalchemy.sqlimportSelectBase=declarative_base()classLeaveRequest(Base):__tablename__="leave_request"id=Column(Integer,primary_key=True)department=Column(String(225))geography=Column(String(225))team=Column(String(225))priority=Column(Integer)withCerbosClient(host="http://localhost:3592")asc:p=Principal("john",roles={"employee"},policy_version="20210210",attr={"department":"marketing","geography":"GB","team":"design"},)# Get the query plan for "view" actionrd=ResourceDesc("leave_request",policy_version="20210210")plan=c.plan_resources("view",p,rd)# the attr_map arg of get_query expects a map[string, InstrumentedAttribute | Column], with cerbos attribute strings mapped to the column/attr instancesattr_map={"request.resource.attr.department":LeaveRequest.department,# LeaveRequest.__table__.c.department is also allowed"request.resource.attr.geography":LeaveRequest.geography,"request.resource.attr.team":LeaveRequest.team,"request.resource.attr.priority":LeaveRequest.priority,}# `get_query` supports both `Table` instances and ORM entities:# ORM entity - honouring object level relationships via the sqlalchemy ORMquery:Select=get_query(plan,LeaveRequest,attr_map)# Alternatively it can generate legacy queries by passing the Table instancequery:Select=get_query(plan,LeaveRequest.__table__,attr_map)# NOTE: if columns defined within the attr_map originate from more than one table, we need to define a mapping as the optional 4th positional arg to `get_query`.# The argument is in the form:#   `list[tuple[Table | DeclarativeMeta, BinaryExpression | ColumnOperators]]`# e.g.:query:Select=get_query(plan,Table1,{"request.resource.attr.foo":Table1.foo,# or `Table1.__table__.c.foo`"request.resource.attr.bar":Table2.bar,"request.resource.attr.bosh":Table3.bosh,},[(Table2,Table1.table2_id==Table2.id),# or (Table2.__table__, Table1.__table__.c.table2_id == Table2.__table__.c.id)(Table3,Table1.table3_id==Table3.id),])# optionally extend the queryquery=query.where(LeaveRequest.priority<5)# or return a subset of the selected columns (via a new `select`)# NOTE: this is wise to do as standard, to avoid implicit joins generated by sqla `relationship()` usage, if presentquery=query.with_only_columns(LeaveRequest.department,LeaveRequest.geography,)# Print the compiled query (for debug purposes)print(query.compile(compile_kwargs={"literal_binds":True}))Overriding default predicatesBy default, the library provides a base set of operators which are widely supported across a range of SQL dialects. However, in some cases, users may wish to override a particular operator for a more idiomatic/optimised alternative for a given database. An example of this could be postgres users preferring to use= ANYoverIN:fromsqlalchemy.sql.expressionimportany_query=get_query(plan_resource_resp,some_table,attr_map={"request.resource.attr.foo":Table1.foo,},# override handler functions in the map belowoperator_override_fns={"in":lambdac,v:c==any_(v),},)The types are as follows:fromsqlalchemyimportColumnfromsqlalchemy.ormimportInstrumentedAttributefromsqlalchemy.sql.expressionimportBinaryExpression,ColumnOperatorsGenericColumn=Column|InstrumentedAttributeGenericExpression=BinaryExpression|ColumnOperators# and the actual map arg to `get_query` ⬇️OperatorFnMap=dict[str,Callable[[GenericColumn,Any],GenericExpression]]