Python Client API
These are the docs for the v2 release. For v1, see the docs and the migration guide.
For changes to the Python Client API, see Changelog.
First, install the oso-cloud
package on PyPI:
pip install oso-cloud
Before going through this guide, make sure you follow the Oso Cloud Quickstart to get your Oso Cloud API Key properly set in your environment.
Instantiating an Oso Cloud client
The Oso Cloud client provides an Oso
class that takes your Oso Cloud URL and API key:
from oso_cloud import Osooso = Oso(url="https://cloud.osohq.com", api_key=YOUR_API_KEY)# Later:oso.insert(("has_role", user, role, resource))# Wherever authorization needs to be performed:if oso.authorize(user, action, resource): # Action is allowed
You should instantiate one client and share it across your application. Under the hood, it reuses connections to avoid paying the cost of negotiating a new connection on every request.
Specifying an Oso Fallback host
If you have deployed Oso Fallback nodes to your infrastructure, you may specify the host when instantiating the Oso Cloud client.
# Assumes Oso Fallback is hosted at http://localhost:8080oso = Oso(url="https://cloud.osohq.com", api_key=YOUR_API_KEY, fallback_url="http://localhost:8080")
Passing application entities into the client
Under the hood, Oso Cloud represents an entity in your application as a combination of a type and an ID,
which together uniquely identify the entity.
The Python client represents these entities with the Value
class, which has both type
and id
fields.
For example:
from oso_cloud import Valuealice = Value(type="User", id="alice")anvils_repository = Value("Repository", "anvils") # shorthand
You will pass objects like these into nearly every function call you make to the Python client.
Primitive value conversion
Additionally, for fields with primitive values you
may pass an instance of the corresponding Python type directly instead of a full Value
object:
oso.insert(("is_weird", 10, "yes", True))# equivalent to:# (# "is_weird",# Value("Integer", "10"),# Value("String", "yes"),# Value("Boolean", "true")# )
Centralized Authorization Data API
Oso Cloud clients provide an API to manage authorization data stored directly in Oso Cloud.
Add fact: oso.insert((name, *args))
Adds a fact named name
with the provided arguments. Example:
oso.insert((
"has_role",
Value("User", "bob"),
"owner",
Value("Organization", "acme")
))
Delete fact: oso.delete((name, *args))
Deletes a fact. Does not throw an error if the fact is not found. Example:
oso.delete(("has_role", Value("User", "bob"), "maintainer", Value("Repository", "anvils")))
When deleting facts, you can use None
as a wildcard to delete many facts at once.
# remove all of bob's roles across all resourcesoso.delete(("has_role", Value("User", "bob"), None, None))
You can also use oso_cloud.ValueOfType
to delete facts with an argument of a particular type.
# remove all of bob's roles across all Repositoriesoso.delete(("has_role", Value("User", "bob"), None, ValueOfType("Repository")))
Transactionally delete and add facts: oso.batch()
For Oso Cloud developer accounts, batch
calls are limited to 20 facts. If
you attempt to send more than 20 facts, these functions will throw an error.
Allows deleting and inserting many facts in one atomic transaction. Deletions and insertions are run in the order they appear in the context. Example:
with oso.batch() as tx: tx.insert(("has_role", Value("User", "bob"), "owner", Value("Organization", "acme"))) tx.delete(("has_role", Value("User", "bob"), "maintainer", Value("Repository", "anvil")))
As in calls to delete
, you may use wildcards (None
) and
type-constrained wildcards (ValueOfType
) in tx.delete
calls.
List facts: oso.get((name, *args))
For Oso Cloud developer accounts, Get
calls are limited to 1000 results. If
you have more than 1000 facts, the function will throw an error.
Lists facts that are stored in Oso Cloud. Can be used to check the existence of a particular fact, or used to fetch all facts that have a particular argument:
bob = Value("User", "bob")anvils = Value("Repository", "anvils")# Get one fact:oso.get(("has_role", bob, "admin", anvils))# => [(# "has_role",# Value("User", "bob"),# Value("String", "admin"),# Value("Repository", "anvils")# )]# List all role-related facts on the `anvils` repooso.get(("has_role", None, None, anvils))# => [(# "has_role",# Value("User", "bob"),# Value("String", "admin"),# Value("Repository", "anvils")# ),# ... other has_role facts# ]
Note that None
behaves like a wildcard: passing the fact arguments None, None, anvils
means "find all facts where anvils
is the third argument,
regardless of other arguments".
You can also use oso_cloud.ValueOfType
to get all facts with an argument of a
certain type:
# List all role-related facts on any repo:oso.get(("has_role", None, None, ValueOfType("Repository")))# => [(# "has_role",# Value("User", "bob"),# Value("String", "reader"),# Value("Repository", "acme")# ),# ... other has_role facts# ]
oso.get()
only returns facts you've explicitly added. If you want to return
a list of authorized resources, use the Check API. For example,
to answer "on which resources can a given user perform a given action", use
oso.list()
. If you want to query for arbitrary
information that can be derived from your facts and policy, use the Query
Builder API.
Check API
For Oso Cloud developer accounts, the number of context facts per request is limited to 20; and the number of records returned is limited to 1000.
Context facts
The Check API lets you provide context facts with each request. When Oso Cloud performs a check, it considers the request's context facts in addition to any other centralized authorization data. Context facts are only used in the API call in which they're provided-- they do not persist across requests.
For more details, see Context Facts.
Check a permission: oso.authorize(actor, action, resource)
Determines whether or not an action is allowed, based on a combination of authorization data and policy logic. Example:
alice = Value("User", "alice")anvils_repository = Value("Repository", "anvils")if not oso.authorize(alice, "read", anvils_repository): raise Exception("Action is not allowed")
You may provide a list of context facts as an optional fourth argument to this method. Example:
issue_on_anvils_repository = Value("Issue", "anvils-1")oso.authorize(alice, "read", anvils_repository, [ # a context fact ("has_relation", issue_on_anvils_repository, "parent", anvils_repository)])
List authorized resources: oso.list(actor, action, resource_type)
Fetches a list of resources on which an actor can perform a particular action. Example:
alice = Value("User", "alice")oso.list(alice, "read", "Repository")# => ["acme"]
You may provide a list of context facts as an optional fourth argument to this method. Example:
anvils_repository = Value("Repository", "anvils")acme_repository = Value("Repository", "acme")issue_on_acme_repository = Value("Issue", "acme-1")issue_on_anvils_repository = Value("Issue", "anvils-2")oso.list( alice, "read", "Issue", [ # context facts ("has_relation", issue_on_anvils_repository, "parent", anvils_repository), ("has_relation", issue_on_acme_repository, "parent", acme_repository), ])# => ["acme-1"]
List authorized actions: oso.actions(actor, resource)
Fetches a list of actions which an actor can perform on a particular resource. Example:
alice = Value("User", "alice")acme_repository = Value("Repository", "acme")oso.actions(alice, acme_repository)# => ["read"]
You may provide a list of context facts as an optional third argument to this method. Example:
issue_on_acme_repository = Value("Issue", "acme-1")actions = oso.actions( alice, issue_on_acme_repository, # a context fact [ ("has_relation", issue_on_acme_repository, "parent", acme_repository) ])# => ["read"]
Query for any rule: oso.build_query((predicate, *args))
Query Oso Cloud for any predicate and any combination of concrete and wildcard
arguments. Unlike oso.get
, which only lists facts you've added, you can use
oso.build_query
to list derived information about any rule in your policy.
Example:
actor = Value("User", "bob")repository = typed_var("Repository")# Query for all the repos `User:bob` can `read`oso.build_query(("allow", actor, "read", repository)).evaluate(repository)# => [ "acme", "anvils" ]
Query Builder API
The oso.build_query()
API is a builder-style API where you chain methods to
construct a query and then execute it.
oso.build_query((predicate, *args))
The oso.build_query()
function takes the name of the rule you want to query and
a list of arguments. The arguments can be concrete values (e.g.,
"read"
or Value("User", "bob")
) or type-constrained variables
constructed via the typed_var
function:
actor = Value("User", "bob")repository = typed_var("Repository")# Query for all the repositories bob can readoso.build_query(("allow", actor, "read", repository))# => QueryBuilder { ... }
Note: once you've finished building up your query, you must call evaluate
to
run it and get the results.
QueryBuilder.and_((predicate, *args))
This function adds another condition that must be true of the query results.
For example:
actor = Value("User", "bob")repository = typed_var("Repository")folder = Value("Folder", "folder-1")# Query for all the repositories this user can read...( oso .build_query(("allow", actor, "read", repository)) #... and require the repositories to belong to the given folder. .and_(("has_relation", repository, "folder", folder)))# => QueryBuilder { ... }
Note: once you've finished building up your query, you must call evaluate
to
run it and get the results.
QueryBuilder.in_(variable, values)
This function requires a given typed_var
query variable to be included in a
given set of values. You can only call in_
once per variable per query. Calling
in_
a second time with the same variable on the same query builder will throw
an error.
For example:
actor = Value("User", "bob")repositories = ["acme", "anvil"]action = typed_var("String")repository = typed_var("Repository")# Query for all the actions this user can perform on any repository...( oso .build_query(("allow", actor, action, repository)) # ...given that the repository's ID is in the given list of IDs. .in_(repository, repositories))# => QueryBuilder { ... }
Note: once you've finished building up your query, you must call evaluate
to
run it and get the results.
QueryBuilder.with_context_facts(context_facts)
This function adds the given context facts to the query. For example:
actor = Value("User", "bob")repository = typed_var("Repository")# Query for all the repositories bob can read...( oso .build_query(("allow", actor, "read", repository)) # ...while including the fact that bob owns acme .with_context_facts([ ("has_role", actor, "owner", Value("Repository", "acme")), ]))# => QueryBuilder { ... }
For more information on context facts, see this section.
Note: once you've finished building up your query, you must call evaluate
to
run it and get the results.
QueryBuilder.evaluate()
This function evaluates the built query, fetching the results from Oso.
The return type of this function varies based on the arguments you pass in.
-
If you pass no arguments, this function returns a boolean. For example:
allowed = (oso.build_query(("allow", actor, action, resource)).evaluate())# => true if the given actor can perform the given action on the given resource -
If you pass a single
typed_var
query variable, this function returns a list of values for that variable. For example:action = typed_var("String")actions = (oso.build_query(("allow", actor, action, resource)).evaluate(action))# => all the actions the actor can perform on the given resource- eg. ["read", "write"] -
If you pass a tuple of
typed_var
query variables, this function returns a list of tuples of values for those variables. For example:action = typed_var("String")repository = typed_var("Repository")pairs = (oso.build_query("allow", actor, action, repository).evaluate((action, repository)))# => an array of pairs of allowed actions and repo IDs-# eg. [("read", "acme"), ("read", "anvil"), ("write", "anvil")] -
If you pass a dict mapping one
typed_var
query variable (call it K) to another (call it V), returns a dict grouping unique values of K to unique values of V for each value of K. For example:action = typed_var("String")repository = typed_var("Repository")repo_actions = (oso.build_query(("allow", actor, action, repository)).evaluate({repository: action}))# => a dict of repo IDs to allowed actions-# eg. { "acme": ["read"], "anvil": ["read", "write"]}
Some queries have unconstrained results. For instance, maybe users with the
admin
role can read all Repository
entities in your application. In this
case, rather than returning an array containing the ID of every repository,
evaluate
will return an array containing the string "*"
. For example:
repos = typed_var("Repository")( oso .build_query(("allow", Value("User", "admin"), "read", repos)) .evaluate(repos) # Return just the IDs of the repos admin can read)# => ["*"] # admin can read anything
Query Builder examples
Field-level access control
actor = Value("User", "alice")resource = Value("Repository", "anvil")field = typed_var("Field")results = ( oso .build_query(("allow_field", actor, "read", resource, field)) .evaluate(field))# => Returns a list of the fields alice can read on the given repo- eg.# ["name", "stars"]
Checking a global permission
actor = Value("User", "alice")result = ( oso .build_query(("has_permission", actor, "create_repository")) .evaluate())# => true if alice has the global "create_repository" permission
Fetching authorized actions for a collection of resources
repos = ["anvil", "acme"]actor = Value("User", "alice")action = typed_var("String")repo = typed_var("Repository")results = ( oso .build_query(("allow", actor, action, repo)) .in_(repo, repos) .evaluate({repo: action}))# => Returns a dict mapping the given repos to the actions alice can perform on those repos- eg.# { "anvil": ["read"], "acme": ["read", "write"] }
Filtering out unauthorized resources from a collection
repos = ["anvil", "acme"]actor = Value("User", "bob")repo = typed_var("Repository")results = ( oso .build_query(("allow", actor, "read", repo)) .in_(repo, repos) .evaluate(repo))# => Returns the subset of `repos` that bob can read- eg.# ["anvil"]
Filtering an authorize()
query based on a relation
actor = Value("User", "bob")repo = typed_var("Repository")org = Value("Org", "coolguys")results = ( oso .build_query(("allow", actor, "read", repo)) .and_(("has_relation", repo, "parent", org)) .evaluate(repo))# => Returns the IDs of the repos in the coolguys org that bob can read- eg.# ["acme", "anvil"]
Learn more about how to query Oso Cloud.
Local Check API
The local check API lets you perform authorization using data that's distributed across Oso Cloud and your own database.
After creating your Local Authorization configuration, provide the path to the YAML file that specifies how to resolve facts in your database.
oso = Oso( ... data_bindings="path/to/local_authorization_config.yaml",)
For more information, see Local Authorization.
List authorized resources with local data: oso.list_local(actor, action, resource_type, column)
Fetches a filter that can be applied to a database query to return just the resources on which an actor can perform an action. Example with SQLAlchemy:
from sqlalchemy import select, textalice = Value("User", "alice")authorized_issues = session.scalars( select(Issues) .filter(text(oso.list_local(alice, "read", "Issue", "id")))).all()
You may use the SQLAlchemy query builder (opens in a new tab) to combine this authorization filter with other things such as ordering and pagination.
You may provide a list of context facts as an optional final argument to this method.
Check a permission with local data: oso.authorize_local(actor, action, resource)
Fetches a query that can be run against your database to determine whether an actor can perform an action on a resource. Example with SQLAlchemy:
from sqlalchemy import textalice = Value("User", "alice")swage_issue = Value("Issue", "swage")query = oso.authorize_local(alice, "read", swage_issue)authorized = session.execute(text(query)).scalar()if not authorized: raise Exception("Action is not allowed")
The query will return a single boolean value. The user is authorized to perform the action if that boolean value is true.
You may provide a list of context facts as an optional final argument to this method.
List authorized actions with local data: oso.actions_local(actor, resource)
Fetches a query that can be run against your database to fetch the actions an actor can perform on a resource. Example with SQLAlchemy:
from sqlalchemy import select, textalice = Value("User", "alice")swage_issue = Value("Issue", "swage")query = oso.actions_local(alice, swage_issue)actions = session.execute(text(query)).scalars()
You may provide a list of context facts as an optional final argument to this method.
Query for any rule with local data: QueryBuilder
You can use the Query Builder to construct SQL queries that you can run against your database to answer arbitrary questions about authorization.
See the Query Builder API documentation for information on how to construct queries. Once you've
built your query, instead of calling evaluate
(which would evaluate your query exclusively against data
stored in Oso Cloud), call either evaluate_local_select
or
evaluate_local_filter
to construct a SQL query which you can run against
your database.
Local Query API
QueryBuilder.evaluate_local_select(column_names_to_query_vars)
Fetches a complete SQL query representing the given Query Builder query. (See the Query Builder API documentation for information on how to construct queries.)
The argument to this function is a dict that maps the column names that will appear in the SELECT
clause of the SQL query to the Query Builder variables whose values should be selected. For example,
query.evaluate_local_select({"user_id": user_var})
will select all the authorized values of user_var
(that is, all the values of user_var
that satisfy the Query Builder query) into a column called "user_id"
.
If you pass in an empty dict or omit this argument entirely, the SQL query will return a single
row selecting a boolean column called result
. This column will be True
when there's some combination
of data in your database that can satisfy the given Query Builder query and False
otherwise.
Note that each query variable can appear at most once in the column_names_to_query_vars
mapping. For example,
query.evaluate_local_select({"user_id": user_var, "another_user_id": user_var})
will raise a ValueError
, because
user_var
appears twice in the mapping parameter.
Note that column names will be double-quoted and are thus case-sensitive.
Limitations
evaluate_local_select
has the following limitations:
-
Queries that would return a wildcard for one of the selected query variables are currently unsupported.
Example:
# alice is a global admin, and can read *any* repooso.insert(("is_global_admin", Value("User", "alice")))# UNSUPPORTED: Attempt to query for each authorized user / repo pairuser_var = typed_var("User")repo_var = typed_var("Repo")sql = oso.build_query(("allow", user_var, "read", repo_var)).evaluate_local_select({"user_id": user_var, "repo_id": repo_var})# => raises `Exception`, because alice can read any repo, so this query would return# a wildcard for the repos alice can read
QueryBuilder.evaluate_local_filter(column_name, query_var)
Fetches a SQL fragment representing the given Query Builder query that you can embed
in the WHERE
clause of some other SQL query. Use this to filter out unauthorized results from
your SQL query.
(See the Query Builder API documentation for information on how to construct queries.)
column_name
is the name of the column you want to filter in your SQL query, and query_var
is the
query variable to filter against.
For example, query.evaluate_local_filter("user_id", user_var)
will return a SQL fragment of the form
"user_id" IN (...)
, constraining the column "user_id"
to the authorized values of user_var
.
Note that the column name will be double-quoted and is thus case-sensitive.
Local Query examples
These examples all use SQLAlchemy (opens in a new tab) to execute the returned SQL queries, but you can use any database client you prefer.
Field-level access control
actor = Value("User", "alice")resource = Value("Repository", "anvil")field = typed_var("Field")sql_query = oso.build_query( ("allow_field", actor, "read", resource, field)).evaluate_local_select({"field_name": field})# => 'SELECT "field_name" FROM (... /* only the fields alice can read on anvil */)'fields = session.execute(sqlalchemy.sql.text(sql_query)).scalars().all()# => ["name", "stars"]
Checking a global permission
actor = Value("User", "alice")sql_query = oso.build_query( ("has_permission", actor, "create_repository")).evaluate_local_select()# => 'SELECT EXISTS (... /* alice has the create_repository permission */) AS result'has_permission = session.execute(sqlalchemy.sql.text(sql_query)).scalar()# => True
Fetching authorized actions for a paginated collection of resources
from sqlalchemy.sql import column, textactor = Value("User", "alice")action_var = typed_var("String")repo_var = typed_var("Repository")subquery = oso.build_query( ("allow", actor, action_var, repo_var)).evaluate_local_select({"action": action_var, "repo_id": repo_var})# => 'SELECT "action", "repo_id" FROM (... /* pairs of alice's authorized actions / repos */)'# Embed the Oso query into a common table expression:authorized_actions = text(subquery).columns( column("action"), column("repo_id")).cte("authorized_actions")# Assuming Repository is a SQLAlchemy ORM-mapped class:query = ( sqlalchemy.select(Repository.id, authorized_actions.c.action). join_from(Repository, authorized_actions, Repository.id == authorized_actions.c.repo_id). order_by(Repository.id).limit(10))session.execute(query).fetchall()# => [(1, "read"), (1, "write"), ...]
Filtering an authorize()
query based on a relation
actor = Value("User", "bob")repo_var = typed_var("Repository")org = Value("Org", "coolguys")sql_query = (oso .build_query(("allow", actor, "read", repo_var)) .and_(("has_relation", repo_var, "parent", org)) .evaluate_local_select({"repo_id": repo_var}))# => 'SELECT "repo_id" FROM (... /* only repos bob can read which belong to coolguys */)'session.execute(sqlalchemy.sql.text(sql_query)).scalars().all()# => ["acme", "anvil"]
Filtering on users who can read a certain resource
actor_var = typed_var("User")resource = Value("Repository", "anvil")authorized_user_fragment = (oso .build_query(("allow", actor_var, "read", resource)) .evaluate_local_filter("id", actor_var))# => '"id" IN (... /* only users who can read anvil */)'# Assuming User is a SQLAlchemy ORM-mapped class:session.query(User).filter(sqlalchemy.sql.text(authorized_user_fragment)).all()# => [User["alice"], User["bob"], ...]
Policy API
Update the active policy: oso.policy(policy)
Updates the policy in Oso Cloud. The string passed into this method should be written in Polar. Example:
oso.policy("actor User {}")
This command will run any tests defined in your policy. If one or more of these tests fail, your policy will not be updated.
Get policy metadata: oso.get_policy_metadata()
Returns metadata about the currently active policy. Example:
metadata = oso.get_policy_metadata()print(metadata.resources.keys())# returns ["Organization", "User", "global"]print(metadata.resources["Organization"].roles)# returns ["admin", "member"]
See the Policy Metadata guide for more information on use cases.
Talk to an Oso engineer
If you'd like to learn more about using Oso Cloud in your app or have any questions about this guide, schedule a 1x1 with an Oso engineer. We're happy to help.