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