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 and Query 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"]
class Session(sqlalchemy.orm.session.Session):
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.

Session(*args, **kwargs)
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.

def query(self, *entities, **kwargs) -> Query:
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].

class Query(sqlalchemy.sql.selectable._SelectFromElements, sqlalchemy.sql.annotation.SupportsCloneAnnotations, sqlalchemy.sql.selectable.HasPrefixes, sqlalchemy.sql.selectable.HasSuffixes, sqlalchemy.sql.selectable.HasHints, sqlalchemy.event.registry.EventTarget, sqlalchemy.log.Identified, sqlalchemy.sql.base.Generative, sqlalchemy.sql.base.Executable, typing.Generic[~_T]):
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.

Query(*args, **kwargs)
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.

oso
def authorized( self: ~Self, actor: oso_cloud.types.Value, action: str, model: Optional[Type] = None) -> ~Self:
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)

Filter the query to only include resources that the given actor is authorized to perform the given action on.

Parameters
  • actor: The actor performing the action.
  • action: The action the actor is performing.
Returns

A new query that includes only the resources that the actor is authorized to perform the action on.

def init(registry: sqlalchemy.orm.decl_api.registry, **kwargs):
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 and api_key.
def get_oso() -> oso_cloud.oso.Oso:
146def get_oso() -> Oso:
147  """
148  Get the Oso Cloud client that was created with `init`.
149
150  :return: The Oso Cloud client.
151  """
152  global oso
153  if oso is None:
154    raise RuntimeError("sqlalchemy_oso_cloud must be initialized before getting the Oso client")
155  return oso

Get the Oso Cloud client that was created with init.

Returns

The Oso Cloud client.

class Select(sqlalchemy.sql.selectable.ExecutableReturnsRows, typing.Generic[~_TP]):
 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

Select(*args, **kwargs)
16    def __init__(self, *args, **kwargs):
17        super().__init__(*args, **kwargs)

Construct a new _expression.Select.

The public constructor for _expression.Select is the _sql.select() function.

inherit_cache = True

Internal SQLAlchemy caching optimization

def authorized(self: ~Self, actor: oso_cloud.types.Value, action: str) -> ~Self:
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)

Add authorization filtering to the select statement

def select(*args, **kwargs) -> Select:
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)
def authorized( actor: oso_cloud.types.Value, action: str, model: Type) -> sqlalchemy.orm.util.LoaderCriteriaOption:
41def authorized(actor: Value, action: str, model: Type) -> LoaderCriteriaOption:
42    """
43    Create authorization options for use with .options()
44    
45    This function can be used with both Select and Query objects from the SQLAlchemy library:
46    
47    Examples:
48        # With standard SQLAlchemy select
49        from sqlalchemy import select
50        from sqlalchemy_oso_cloud import authorized
51
52        stmt = select(Document).options(*authorized(user, "read", Document))
53        docs = session.execute(stmt).scalars().all()
54
55        # With Query
56        docs = session.query(Document).options(*authorized(user, "read", Document)).all()
57
58    :param actor: The actor performing the action
59    :param action: The action the actor is performing  
60    :param models: The model classes to authorize against
61    :return: List of loader criteria options for use with .options()
62    """
63    
64    if not model:
65        raise ValueError("Must provide a model to authorize against.")
66   
67    if not issubclass(model, Resource):
68        raise ValueError(f"Model {model.__name__} must inherit from Resource to use authorization")
69
70    
71    auth_criteria = create_auth_criteria_for_model(model, actor, action)
72
73    return with_loader_criteria(
74            model,
75            auth_criteria,
76            include_aliases=True
77        )

Create authorization options for use with .options()

This function can be used with both Select and Query objects from the SQLAlchemy library:

Examples: # With standard SQLAlchemy select from sqlalchemy import select from sqlalchemy_oso_cloud import authorized

stmt = select(Document).options(*authorized(user, "read", Document))
docs = session.execute(stmt).scalars().all()

# With Query
docs = session.query(Document).options(*authorized(user, "read", Document)).all()
Parameters
  • actor: The actor performing the action
  • action: The action the actor is performing
  • models: The model classes to authorize against
Returns

List of loader criteria options for use with .options()

def _apply_authorization_options( query_obj: Union[Query, Select], actor: oso_cloud.types.Value, action: str, model: Optional[Type] = None):
103def _apply_authorization_options(query_obj: Union["Query",  "Select"], actor: Value, action: str, model: Optional[Type] = None):
104    """
105    Apply authorization to any query-like object that has column_descriptions and options()
106    
107    This works with both Select and Query objects.
108    """
109
110    if model is not None:
111        auth_option = authorized(actor, action, model)
112        return query_obj.options(auth_option)
113    else:
114        auth_options = _authorize_all_models(query_obj, actor, action)
115        return query_obj.options(*auth_options)

Apply authorization to any query-like object that has column_descriptions and options()

This works with both Select and Query objects.