Migrating to v2 of the Python SDK
The v2 release of the Python SDK represents a significant rethinking of the developer experience. As such there are a few changes to be aware of, both breaking and non-breaking.
Fact Representation
Methods that accept a fact or fact pattern now accept the fact as a tuple of the predicate and arguments instead of a dict:
-context_fact = {"name": "has_role", args: [user, "member", repo]}+context_fact = ("has_role", user, "member", repo)oso.authorize(user, "read", repo, [context_fact])
The representation of fact arguments has changed as well. Previously you would represent a fact argument as a dict with "type"
and "id"
keys.
Now, fact arguments should be represented as oso_cloud.Value
instead:
-alice = { "type": "User", "id": "alice" }+from oso_cloud import Value+alice = Value("User", "alice")
In addition, the get
function now returns facts in the new tuple format, rather than in the old dict format.
Input Types
In some cases, Python type checkers (such as Mypy) need explicit type declarations on variables to perform
type inference correctly. If you were explicitly declaring variables with the types oso_cloud.Value
and oso_cloud.Fact
for
input parameters to Oso Cloud functions, change these types to oso_cloud.IntoValue
and oso_cloud.IntoFact
instead.
(These types represent values that can be passed into Oso Cloud and converted to Value
s and Fact
s-
in addition to Value
s, they accommodate native Python booleans, integers, and strings.)
Management API
The Management API has been condensed from 6 methods to 4.
tell
to insert
To migrate from tell
to insert
, convert the arguments from dict
s to Value
s and wrap them in a tuple:
-user = { "type": "User", "id": "1" }-repo = { "type": "Repo", "id": "2" }-oso.tell("has_role", user, "member", repo)+user = Value("User", "1")+repo = Value("Repo", "2")+oso.insert(("has_role", user, "member", repo))
This new tuple-wrapped syntax matches the representation of facts across the rest of the API.
delete
To migrate from the previous delete
API, convert the arguments from dict
s to Value
s and wrap them in a tuple:
-user = { "type": "User", "id": "1" }-repo = { "type": "Repo", "id": "2" }-oso.delete("has_role", user, "member", repo)+user = Value("User", "1")+repo = Value("Repo", "2")+oso.delete(("has_role", user, "member", repo))
This new tuple-wrapped syntax matches the representation of facts across the rest of the API.
Additionally, the new delete
method supports deleting all facts matching a
pattern:
user = Value("User", "1")# Remove all of User 1's roles across the entire system.oso.delete(("has_role", user, None, None))
get
To migrate from the previous get
API, convert the arguments from dict
s to Value
s and wrap them in a tuple:
-user = { "type": "User", "id": "1" }-roles = oso.get("has_role", user, None, None)+user = Value("User", "1")+roles = oso.get(("has_role", user, None, None))
If you used a dict to fetch facts with arguments of a particular type (but no specific ID), use ValueOfType
instead:
-user = { "type": "User", "id": "1" }-roles = oso.get("has_role", user, None, { "type": "Repo" })+from oso_cloud import ValueOfType+user = Value("User", "1")+roles = oso.get(("has_role", user, None, ValueOfType("Repo")))
If you used an empty dict as a wildcard, replace it with None
:
-user = { "type": "User", "id": "1" }-roles = oso.get("has_role", user, {}, {})+user = Value("User", "1")+roles = oso.get(("has_role", user, None, None))
Please note that the structure of the facts returned by get
has changed, too:
-user = { "type": "User", "id": "1" }-roles = oso.get("has_role", user, None, None)-# => [{ "name": "has_role", "args":-# [{ "type": "User", "id": "1" },-# { "type": "String", "id": "reader" },-# { "type": "Repo", "id": "acme" }]-# },-# ...]+user = Value("User", "1")+roles = oso.get(("has_role", user, None, None))+# => [("has_role", Value("User", "1"), Value("String", "reader"), Value("Repo", "acme")), ...]
bulk
to batch
or delete
To migrate from bulk
to batch
, turn all patterns to delete into calls to
tx.delete()
and all facts to insert into calls to tx.insert()
:
-user = {"type": "User", "id": "1" }-repo = {"type": "Repo", "id": "3" }-oso.bulk(- [{"name": "has_role", "args": [user, None, None]}],- [{"name": "has_role", "args": [user, "member", repo]}],-)+user = Value("User", "1")+repo = Value("Repo", "3")+with oso.batch() as tx:+ tx.delete(("has_role", user, None, None))+ tx.insert(("has_role", user, "member", repo))
Additionally, the delete
API now handles deleting many facts at once via
wildcards. If you were previously using the bulk
API just to delete many
facts at once, you can now use the delete
API directly without wrapping it in
a call to batch
:
-user = { "type": "User", "id": "1" }-oso.bulk(- [{"name": "has_role", "args": [user, None, None]}],-)+user = Value("User", "1")+oso.delete(("has_role", user, None, None))
If you used a dict to delete facts with an argument of a particular type (but no particular ID),
replace the dict with oso_cloud.ValueOfType
:
-user = { "type": "User", "id": "1" }-oso.bulk(- [{"name": "has_role", "args": [user, None, None]}],-)+ from oso_cloud import ValueOfType+user = Value("User", "1")+oso.delete(("has_role", user, None, ValueOfType("Repo"))
If you used an empty dict as a wildcard, replace it with None
:
-user = { "type": "User", "id": "1" }-roles = oso.get("has_role", user, {}, {})+user = Value("User", "1")+roles = oso.get(("has_role", user, None, None))
bulk_tell
to batch
To migrate from bulk_tell
to batch
, turn all facts to insert into calls to
tx.insert()
:
-oso.bulk_tell([- {"name": "has_role", "args": [{ "type": "User", "id": "1" }, "member", { "type": "Repo", "id": "2" }]},- {"name": "has_role", "args": [{ "type": "User", "id": "1" }, "member", { "type": "Repo", "id": "3" }]},-])+with oso.batch() as tx:+ tx.insert(("has_role", Value("User", "1"), "member", Value("Repo", "2")))+ tx.insert(("has_role", Value("User", "1"), "member", Value("Repo", "3")))
bulk_delete
to batch
To migrate from bulk_delete
to batch
, turn all facts to delete into calls to
tx.delete()
:
-oso.bulk_delete([- {"name": "has_role", "args": [{ "type": "User", "id": "1" }, "member", { "type": "Repo", "id": "2" }]},- {"name": "has_role", "args": [{ "type": "User", "id": "1" }, "member", { "type": "Repo", "id": "3" }]},-])+with oso.batch() as tx:+ tx.delete(("has_role", Value("User", "1"), "member", Value("Repo", "2")))+ tx.delete(("has_role", Value("User", "1"), "member", Value("Repo", "3")))
Additionally, the new batch
method supports deleting all facts matching a
pattern:
user1 = Value("User", "1")user2 = Value("User", "2")# Remove all roles for User 1 and User 2 across the entire system.with oso.batch() as tx: tx.delete(("has_role", user1, None, None)) tx.delete(("has_role", user2, None, None))
Query API
We've replaced the Query API with a more powerful and flexible QueryBuilder API with a fluent interface. We've also dropped the AuthorizeResources APIs in favor of the QueryBuilder.
query
to build_query
We recommend taking a look at the reference docs for the new QueryBuilder API. It's more flexible and expressive than the old Query API, and it may let you simplify your application code.
But if you just want to migrate your existing queries as-is, here's how.
Queries with no wildcards
Typically, a query with no wildcards is used to check for the existence of a derived rule or fact.
With the old Query API:
results = oso.query({ "name": "has_role", "args": [ { "type": "User", "id": "bob" }, "reader", { "type": "Repository", "id": "acme" } ]})# => [["has_role", {"type": "User", "id": "bob"}, "reader", {"type": "Repository", "id": "acme"}]]ok = bool(results)# True
With the new QueryBuilder API:
ok = ( oso .build_query(( "has_role", Value("User", "bob"), "reader", Value("Repository", "acme"), )) .evaluate() # Return a boolean)# => True
Queries with type-constrained wildcards
The old Query API let you query for all the results of a particular type.
To migrate these, replace type-constrained wildcards with a typed_var(my_type)
variable.
With the old Query API:
# Query for all the repos User `bob` can `read`oso.query({ "name": "allow", "args": [ { "type": "User", "id": "bob" }, "read", {"type": "Repository"} ]})# => [# {# "name": "allow",# "args": [# {"type": "User", "id": "bob"},# "read",# {"type": "Repository", "id": "acme"}# ]# },# {# "name": "allow",# "args": [# {"type": "User", "id": "bob"},# "read",# {"type": "Repository", "id": "anvils"}# ]# }# ]
With the new QueryBuilder API:
repos = typed_var("Repository")( oso .build_query(("allow", Value("User", "bob"), "read", repos)) .evaluate(repos) # Return just the IDs of the repos bob can read)# => ["acme", "anvils"]
If you have several type-constrained wildcards in a single query, you may prefer to get results as a dict:
users = typed_var("User")repos = typed_var("Repository")( oso .build_query(( # Query for which users can read which repos "allow", users, "read", repos, ]) # Return the results as a map from user IDs to arrays of repo IDs .evaluate({users: repos}))# => { "bob": ["acme", "anvil"], "alice": ["anvil"], ... }
Queries with unconstrained wildcards
The old Query API let you use None
to query for many types of results at once:
# Query for all the objects User `bob` can `read`oso.query({"name": "allow", "args": [{ "type": "User", "id": "bob" }, "read", None]})# => [# [{"name": "allow", "args": [{"type": "User", "id": "bob"}, "read", {"type": "Repository", "id": "acme"}]},# [{"name": "allow", "args": [{"type": "User", "id": "bob"}, "read", {"type": "Repository", "id": "anvil"}]},# [{"name": "allow", "args": [{"type": "User", "id": "bob"}, "read", {"type": "Issue", "id": "123"}]},# ...# ]
In the new QueryBuilder API, this is no longer possible.
Instead, make one request for each concrete type:
def query_for_bob_readable(resource_type: str): resource = typed_var(resource_type) return oso.build_query( ("allow", Value("User", "bob"), "read", resource) ).evaluate(resource)readable_repos = query_for_bob_readable("Repository")# ["acme", "anvil"]readable_issues = query_for_bob_readable("Issue")# ["123"]readable_orgs = query_for_bob_readable("Organization")# ["org1", "org2"]
Handling wildcards in results
The old Query API sometimes returned results containing the value None
, indicating
that any value could apply at a particular position. For example:
# Query for all the objects User `admin` can `read`oso.query({"name": "allow", "args": [{ "type": "User", "id": "admin" }, "read", None])# => [# # User `admin` can `read` anything# [{"name": "allow", "args": [{"type": "User", "id": "admin"}, "read", None]}],# ]
The new QueryBuilder API will instead return the string "*"
to mean the same thing,
just like the Check APIs-
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
Context facts
If you are using context facts with query
, check out the with_context_facts
QueryBuilder method.
authorize_resources
to build_query
The authorize_resources
API was used to authorize many resources for a single actor and action.
user = { "type": "User", "id": "1" }repo_ids = ["acme", "anvil"]repos = [{"type": "Repo", "id": id} for id in repo_ids]oso.authorize_resources(user, "read", repos)# => [{ "type": "Repo", "id": "acme"}]
With the new QueryBuilder API:
user = { "type": "User", "id": "1" }repo_ids = ["acme", "anvil"]repo_var = typed_var("Repo")( oso .build_query(("allow", user, "read", repo_var)) .in_(repo_var, repo_ids) .evaluate(repo_var))# => ["acme"]
If you are using context facts with authorized_resources
, check out the with_context_facts
QueryBuilder method.