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 Values and Facts- in addition to Values, 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 dicts to Values 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 dicts to Values 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 dicts to Values 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.