Cerbos + SQLAlchemy Adapter
An adapter library that takes a Cerbos Query Plan (PlanResources API) response and converts it into a SQLAlchemy Select instance. This is designed to work alongside a project using the Cerbos Python SDK.
The following conditions are supported: and
, or
, not
, eq
, ne
, lt
, gt
, le
(lte
), ge
(gte
) and in
. Other operators (eg math operators) can be implemented programatically, and attached to the query object via the query.where(...)
API.
Requirements
- Cerbos > v0.16
- SQLAlchemy >= 1.4 / 2.0
Usage
pip install cerbos-sqlalchemy
from cerbos.sdk.client import CerbosClient
from cerbos.sdk.model import Principal, ResourceDesc
from cerbos_sqlalchemy import get_query
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import declarative_base
from sqlalchemy.sql import Select
Base = declarative_base()
class LeaveRequest(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)
with CerbosClient(host="http://localhost:3592") as c:
p = Principal(
"john",
roles={"employee"},
policy_version="20210210",
attr={"department": "marketing", "geography": "GB", "team": "design"},
)
rd = ResourceDesc("leave_request", policy_version="20210210")
plan = c.plan_resources("view", p, rd)
attr_map = {
"request.resource.attr.department": LeaveRequest.department,
"request.resource.attr.geography": LeaveRequest.geography,
"request.resource.attr.team": LeaveRequest.team,
"request.resource.attr.priority": LeaveRequest.priority,
}
query: Select = get_query(plan, LeaveRequest, attr_map)
query: Select = get_query(plan, LeaveRequest.__table__, attr_map)
query: Select = get_query(
plan,
Table1,
{
"request.resource.attr.foo": Table1.foo,
"request.resource.attr.bar": Table2.bar,
"request.resource.attr.bosh": Table3.bosh,
},
[
(Table2, Table1.table2_id == Table2.id),
(Table3, Table1.table3_id == Table3.id),
]
)
query = query.where(LeaveRequest.priority < 5)
query = query.with_only_columns(
LeaveRequest.department,
LeaveRequest.geography,
)
print(query.compile(compile_kwargs={"literal_binds": True}))
Overriding default predicates
By 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 = ANY
over IN
:
from sqlalchemy.sql.expression import any_
query = get_query(
plan_resource_resp,
some_table,
attr_map={
"request.resource.attr.foo": Table1.foo,
},
operator_override_fns={
"in": lambda c, v: c == any_(v),
},
)
The types are as follows:
from sqlalchemy import Column
from sqlalchemy.orm import InstrumentedAttribute
from sqlalchemy.sql.expression import BinaryExpression, ColumnOperators
GenericColumn = Column | InstrumentedAttribute
GenericExpression = BinaryExpression | ColumnOperators
OperatorFnMap = dict[str, Callable[[GenericColumn, Any], GenericExpression]]