sqlalchemy_oso_cloud
SQLAlchemy extension for Local Authorization with Oso Cloud.
This library provides first-class SQLAlchemy support for Oso Cloud, allowing you to filter queries against your database based on a user's access to the data.
The main features are:
- Automatic Local Authorization configuration from your ORM models
via utilities provided in the
sqlalchemy_oso_cloud.orm
module. - Extensions to SQLAlchemy's
Select
andQuery
classes to provide an.authorized_for(actor, action)
method for filtering results.
See the README for more information.
1""" 2[SQLAlchemy](https://www.sqlalchemy.org/) extension for 3[Local Authorization with Oso Cloud](https://www.osohq.com/docs/authorization-data/local-authorization). 4 5This library provides first-class SQLAlchemy support for Oso Cloud, 6allowing you to filter queries against your database based on a user's access 7to the data. 8 9The main features are: 10- Automatic Local Authorization configuration from your ORM models 11 via utilities provided in the `.orm` module. 12- Extensions to SQLAlchemy's `Select` and `Query` classes to provide 13 an `.authorized_for(actor, action)` method for filtering results. 14 15See the [README](https://github.com/osohq/sqlalchemy-oso-cloud) for more information. 16""" 17from . import orm 18from .auth import _apply_authorization_options, authorized 19from .oso import get_oso, init 20from .query import Query 21from .select_impl import Select, select 22from .session import Session 23 24__all__ = ["orm", "Session", "Query", "init", "get_oso", "Select", "select", "authorized", "_apply_authorization_options"]
16class Session(sqlalchemy.orm.Session): 17 """ 18 A convenience wrapper around SQLAlchemy's built-in 19 [`sqlalchemy.orm.Session`](https://docs.sqlalchemy.org/orm/session_api.html#sqlalchemy%2Eorm%2ESession) 20 class. 21 22 This class extends SQLAlchemy's Session to automatically use our custom `.Query` class 23 instead of the default [`sqlalchemy.orm.Query`](https://docs.sqlalchemy.org/orm/queryguide/query.html#sqlalchemy%2Eorm%2EQuery) class. 24 This is only useful if you intend to use SQLAlchemy's [legacy Query API](https://docs.sqlalchemy.org/orm/queryguide/query.html). 25 """ 26 _query_cls: Type[Query] = Query 27 28 def __init__(self, *args, **kwargs): 29 """ 30 Initialize a SQLAlchemy session with the `.Query` class extended to support authorization. 31 Accepts all of the same arguments as [`sqlalchemy.orm.Session`](https://docs.sqlalchemy.org/orm/session_api.html#sqlalchemy%2Eorm%2ESession), 32 except for `query_cls`. 33 """ 34 if "query_cls" in kwargs: 35 raise ValueError("sqlalchemy_oso_cloud does not currently support combining with other query classes") 36 super().__init__(*args, **{ **kwargs, "query_cls": Query }) 37 38 # Single entity overload 39 @overload # type: ignore[override] 40 def query(self, entity: Type[T], /) -> Query[T]: ... 41 42 # Single column overload 43 @overload 44 def query(self, column: InstrumentedAttribute[T], /) -> Query[Row[Tuple[T]]]: ... 45 46 # Two arguments - any combination of entities/columns 47 @overload 48 def query(self, 49 arg1: Union[Type[T1], InstrumentedAttribute[T1]], 50 arg2: Union[Type[T2], InstrumentedAttribute[T2]], /) -> Query[Row[Tuple[T1, T2]]]: ... 51 52 # Three arguments - any combination of entities/columns 53 @overload 54 def query(self, 55 arg1: Union[Type[T1], InstrumentedAttribute[T1]], 56 arg2: Union[Type[T2], InstrumentedAttribute[T2]], 57 arg3: Union[Type[T3], InstrumentedAttribute[T3]], /) -> Query[Row[Tuple[T1, T2, T3]]]: ... 58 59 # Four arguments - any combination of entities/columns 60 @overload 61 def query(self, 62 arg1: Union[Type[T1], InstrumentedAttribute[T1]], 63 arg2: Union[Type[T2], InstrumentedAttribute[T2]], 64 arg3: Union[Type[T3], InstrumentedAttribute[T3]], 65 arg4: Union[Type[T4], InstrumentedAttribute[T4]], /) -> Query[Row[Tuple[T1, T2, T3, T4]]]: ... 66 67 68 # Fallback overload 69 @overload 70 def query(self, *entities: Any) -> Query[Any]: ... 71 72 def query(self, *entities, **kwargs) -> Query[Any]: 73 """ 74 Returns a SQLAlchemy query extended to support authorization. 75 76 Returns a SQLAlchemy query extended to support authorization. 77 Accepts all of the same arguments as 78 [`sqlalchemy.orm.Session.query`](https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy%2Eorm%2ESession%2Equery). 79 80 Single entity queries return Query[T]. 81 Multi-entity and column queries return Query[Row[Tuple[...]]]. 82 All other queries types return Query[Any]. 83 """ 84 return super().query(*entities, **kwargs)
A convenience wrapper around SQLAlchemy's built-in
sqlalchemy.orm.Session
class.
This class extends SQLAlchemy's Session to automatically use our custom .Query
class
instead of the default sqlalchemy.orm.Query
class.
This is only useful if you intend to use SQLAlchemy's legacy Query API.
28 def __init__(self, *args, **kwargs): 29 """ 30 Initialize a SQLAlchemy session with the `.Query` class extended to support authorization. 31 Accepts all of the same arguments as [`sqlalchemy.orm.Session`](https://docs.sqlalchemy.org/orm/session_api.html#sqlalchemy%2Eorm%2ESession), 32 except for `query_cls`. 33 """ 34 if "query_cls" in kwargs: 35 raise ValueError("sqlalchemy_oso_cloud does not currently support combining with other query classes") 36 super().__init__(*args, **{ **kwargs, "query_cls": Query })
Initialize a SQLAlchemy session with the .Query
class extended to support authorization.
Accepts all of the same arguments as sqlalchemy.orm.Session
,
except for query_cls
.
72 def query(self, *entities, **kwargs) -> Query[Any]: 73 """ 74 Returns a SQLAlchemy query extended to support authorization. 75 76 Returns a SQLAlchemy query extended to support authorization. 77 Accepts all of the same arguments as 78 [`sqlalchemy.orm.Session.query`](https://docs.sqlalchemy.org/en/20/orm/session_api.html#sqlalchemy%2Eorm%2ESession%2Equery). 79 80 Single entity queries return Query[T]. 81 Multi-entity and column queries return Query[Row[Tuple[...]]]. 82 All other queries types return Query[Any]. 83 """ 84 return super().query(*entities, **kwargs)
Returns a SQLAlchemy query extended to support authorization.
Returns a SQLAlchemy query extended to support authorization.
Accepts all of the same arguments as
sqlalchemy.orm.Session.query
.
Single entity queries return Query[T]. Multi-entity and column queries return Query[Row[Tuple[...]]]. All other queries types return Query[Any].
14class Query(sqlalchemy.orm.Query[T]): 15 """ 16 An extension of [`sqlalchemy.orm.Query`](https://docs.sqlalchemy.org/orm/queryguide/query.html#sqlalchemy%2Eorm%2EQuery) 17 that adds support for authorization. 18 """ 19 20 def __init__(self, *args, **kwargs): 21 super().__init__(*args, **kwargs) 22 self.oso = get_oso() 23 24 def authorized(self: Self, actor: Value, action: str, model: Optional[Type] = None ) -> Self: 25 """ 26 Filter the query to only include resources that the given actor is authorized to perform the given action on. 27 28 :param actor: The actor performing the action. 29 :param action: The action the actor is performing. 30 31 :return: A new query that includes only the resources that the actor is authorized to perform the action on. 32 """ 33 return _apply_authorization_options(self, actor, action, model)
An extension of sqlalchemy.orm.Query
that adds support for authorization.
20 def __init__(self, *args, **kwargs): 21 super().__init__(*args, **kwargs) 22 self.oso = get_oso()
Construct a _query.Query
directly.
E.g.::
q = Query([User, Address], session=some_session)
The above is equivalent to::
q = some_session.query(User, Address)
Parameters
entities: a sequence of entities and/or SQL expressions.
session: a
.Session
with which the_query.Query
will be associated. Optional; a_query.Query
can be associated with a.Session
generatively via the_query.Query.with_session()
method as well.
seealso.
125def init(registry: registry, **kwargs): 126 """ 127 Initialize an Oso Cloud client configured to resolve authorization data from your 128 database as specified in your ORM models. 129 See `.orm` for more information on how to map your authorization data. 130 131 :param registry: The SQLAlchemy registry containing your models. For example, `Base.registry`. 132 :param kwargs: Additional keyword arguments to pass to the Oso client constructor, such as `url` and `api_key`. 133 """ 134 global oso 135 if oso is not None: 136 raise RuntimeError("sqlalchemy_oso_cloud has already been initialized") 137 kwargs = { **kwargs } 138 if "url" not in kwargs: 139 kwargs["url"] = os.getenv("OSO_URL", "https://api.osohq.com") 140 if "api_key" not in kwargs: 141 kwargs["api_key"] = os.getenv("OSO_AUTH") 142 if "data_bindings" in kwargs: 143 # just need to conditionally close/delete the temporary file if it was created 144 raise NotImplementedError("manual data_bindings are not supported yet") 145 with NamedTemporaryFile(mode="w") as f: 146 config = generate_local_authorization_config(registry) 147 yaml.dump(config, f) 148 f.flush() 149 kwargs["data_bindings"] = f.name 150 oso = Oso(**kwargs)
Initialize an Oso Cloud client configured to resolve authorization data from your
database as specified in your ORM models.
See sqlalchemy_oso_cloud.orm
for more information on how to map your authorization data.
Parameters
- registry: The SQLAlchemy registry containing your models. For example,
Base.registry
. - kwargs: Additional keyword arguments to pass to the Oso client constructor, such as
url
andapi_key
.
11class Select(sqlalchemy.sql.Select): 12 """A Select subclass that adds authorization functionality""" 13 14 inherit_cache = True 15 """Internal SQLAlchemy caching optimization""" 16 17 18 def __init__(self, *args, **kwargs): 19 super().__init__(*args, **kwargs) 20 21 def authorized(self: Self, actor: Value, action: str) -> Self: 22 """Add authorization filtering to the select statement""" 23 return _apply_authorization_options(self, actor, action)
A Select subclass that adds authorization functionality
26def select(*args, **kwargs) -> Select: 27 """ 28 Create an sqlalchemy_oso_cloud.Select() object 29 30 This is a drop-in replacement for sqlalchemy.select() that adds 31 authorization capabilities via the .authorized() method. 32 33 Example: 34 from sqlalchemy_oso_cloud import select 35 36 stmt = select(Document).where(Document.private == True).authorized(actor, "read") 37 documents = session.execute(stmt) 38 """ 39 return Select(*args, **kwargs)
Create an sqlalchemy_oso_cloud.Select() object
This is a drop-in replacement for sqlalchemy.select() that adds authorization capabilities via the .authorized() method.
Example: from sqlalchemy_oso_cloud import select
stmt = select(Document).where(Document.private == True).authorized(actor, "read")
documents = session.execute(stmt)