Migrating to v1 of the Java SDK

The v1 release of the Java SDK represents a significant rethinking of the developer experience. The biggest new feature is the flexible Query Builder API, and there are a few other breaking changes to be aware of.

Packages

The public API surface has been consolidated. Most top-level methods are now directly available under the com.osohq.oso_cloud package via the Oso class. Query-related functionality is accessible through the QueryBuilder returned by oso.buildQuery() and the com.osohq.oso_cloud.querybuilder package.

Key types like Fact and Value, previously found in com.osohq.oso_cloud.api, are now directly available under com.osohq.oso_cloud. When migrating, you will need to update your import statements. For example, if you were using Value and Fact (which were often implicitly used or needed for methods in v0), your imports would change as follows:


-import com.osohq.oso_cloud.api.Value;
-import com.osohq.oso_cloud.api.Fact;
+import com.osohq.oso_cloud.Value;
+import com.osohq.oso_cloud.Fact;

Additionally, it's important to note that in v1, Fact and Value constructors no longer accept null for type or id fields. In v0, passing null to a Value constructor was a way to achieve wildcard behavior in oso.get(). In v1, this wildcard behavior is now explicitly handled by ValuePattern.ANY and ValuePattern.ValueOfType when using FactPattern with methods like oso.get() and oso.delete().

Centralized Authorization Data API

The Centralized Authorization Data API has been condensed and made more consistent.

tell to insert

To migrate from tell to insert, you first need to ensure your imports are updated, and then wrap the arguments in a Fact object:


import com.osohq.oso_cloud.Oso;
-import com.osohq.oso_cloud.api.Value;
+import com.osohq.oso_cloud.Value;
+import com.osohq.oso_cloud.Fact;
...
Oso oso = new Oso(YOUR_API_KEY);
Value user = new Value("User", "1");
Value repo = new Value("Repo", "2");
-oso.tell("has_role", user, new Value("member"), repo);
+oso.insert(new Fact("has_role", user, new Value("member"), repo));

This new Fact-based syntax aligns with how facts are represented across the rest of the API.

delete

To migrate from the previous delete API (for specific facts), update your imports and wrap the arguments in a Fact object:


import com.osohq.oso_cloud.Oso;
-import com.osohq.oso_cloud.api.Value;
+import com.osohq.oso_cloud.Value;
+import com.osohq.oso_cloud.Fact;
...
Oso oso = new Oso(YOUR_API_KEY);
Value user = new Value("User", "1");
Value repo = new Value("Repo", "2");
-oso.delete("has_role", user, new Value("member"), repo);
+oso.delete(new Fact("has_role", user, new Value("member"), repo));

Additionally, there is a new delete method that supports deleting all facts matching a pattern using FactPattern and wildcards (ValuePattern.ANY or ValuePattern.ValueOfType):


import com.osohq.oso_cloud.Oso;
import com.osohq.oso_cloud.Value;
import com.osohq.oso_cloud.FactPattern;
import com.osohq.oso_cloud.ValuePattern;
...
Oso oso = new Oso(YOUR_API_KEY);
Value user = new Value("User", "1");
// Remove all of User:1's roles across the entire system.
oso.delete(new FactPattern("has_role", user, ValuePattern.ANY, ValuePattern.ANY));
// Or, to delete roles only on Repositories:
oso.delete(new FactPattern("has_role", user, ValuePattern.ANY, new ValuePattern.ValueOfType("Repository")));

get

The get method now accepts a FactPattern to specify which facts to retrieve, allowing for wildcard matching using ValuePattern.ANY and ValuePattern.ValueOfType.

Previously, in v0, wildcard matches in get were achieved by passing null as an argument to a Value constructor (e.g., new Value("User", null)) or by passing null directly as an argument for one of the fact's positions. This is no longer supported.

Here's how to migrate a get call that used null for wildcards:


import com.osohq.oso_cloud.Oso;
-import com.osohq.oso_cloud.api.Value;
-import com.osohq.oso_cloud.api.Fact;
+import com.osohq.oso_cloud.Value;
+import com.osohq.oso_cloud.Fact;
+import com.osohq.oso_cloud.FactPattern;
+import com.osohq.oso_cloud.ValuePattern;
...
Oso oso = new Oso(YOUR_API_KEY);
Value userBob = new Value("User", "bob");
-Fact[] results_v0 = oso.get(
- "has_role",
- userBob,
- null, // any role
- new Value("Folder", null) // any folder ID, type must be Folder
-);
+Fact[] results_v1 = oso.get(new FactPattern(
+ "has_role",
+ userBob,
+ ValuePattern.ANY, // any role
+ new ValuePattern.ValueOfType("Folder") // any folder
+));

The following examples further illustrate using the v1 get(FactPattern) method:


import com.osohq.oso_cloud.Oso;
import com.osohq.oso_cloud.Value;
import com.osohq.oso_cloud.Fact;
import com.osohq.oso_cloud.FactPattern;
import com.osohq.oso_cloud.ValuePattern;
...
Oso oso = new Oso(YOUR_API_KEY);
Value user = new Value("User", "1");
Value repo = new Value("Repository", "anvils");
// Get specific fact
Fact[] specificRole = oso.get(new FactPattern("has_role", user, new Value("admin"), repo));
// List all roles for User:1 on any resource
Fact[] userRoles = oso.get(new FactPattern("has_role", user, ValuePattern.ANY, ValuePattern.ANY));
// List all roles on the 'anvils' repo for any user
Fact[] repoRoles = oso.get(new FactPattern(
"has_role",
new ValuePattern.ValueOfType("User"), // Or ValuePattern.ANY for any type
ValuePattern.ANY,
repo
));

bulk to batch or delete

To migrate from bulk to batch, use the oso.batch() method which accepts a Consumer<BatchTransaction>. Call tx.delete() and tx.insert() within the consumer lambda:


import com.osohq.oso_cloud.Oso;
import com.osohq.oso_cloud.Value;
import com.osohq.oso_cloud.Fact;
import com.osohq.oso_cloud.FactPattern;
import com.osohq.oso_cloud.ValuePattern;
...
Oso oso = new Oso(YOUR_API_KEY);
Value user = new Value("User", "1");
Value repo = new Value("Repo", "3");
FactPattern patternToDelete = new FactPattern("has_role", user, new Value("old_role"), ValuePattern.ANY);
Fact toInsert = new Fact("has_role", user, new Value("member"), repo);
oso.batch(tx -> {
tx.delete(patternToDelete);
tx.insert(toInsert);
});

Additionally, the delete API now handles deleting many facts at once via FactPattern with wildcards. If you were previously using the bulk API just to delete many facts at once, you can now use the delete API directly:


import com.osohq.oso_cloud.Oso;
import com.osohq.oso_cloud.Value;
import com.osohq.oso_cloud.FactPattern;
import com.osohq.oso_cloud.ValuePattern;
...
Oso oso = new Oso(YOUR_API_KEY);
Value user = new Value("User", "1");
FactPattern deletePattern = new FactPattern("has_role", user, ValuePattern.ANY, ValuePattern.ANY);
oso.delete(deletePattern);

bulkTell to batch

To migrate from bulkTell to batch, use the oso.batch() method with a Consumer<BatchTransaction> and call tx.insert() for each fact within the lambda:


import com.osohq.oso_cloud.Oso;
import com.osohq.oso_cloud.Value;
import com.osohq.oso_cloud.Fact;
...
Oso oso = new Oso(YOUR_API_KEY);
List<Fact> facts = Arrays.asList(
new Fact("has_role", new Value("User", "1"), new Value("member"), new Value("Repo", "2")),
new Fact("has_role", new Value("User", "1"), new Value("member"), new Value("Repo", "3"))
);
oso.batch(tx -> {
facts.forEach(tx::insert);
});

bulkDelete to batch

To migrate from bulkDelete to batch, use the oso.batch() method with a Consumer<BatchTransaction> and call tx.delete() for each fact or pattern within the lambda:


import com.osohq.oso_cloud.Oso;
import com.osohq.oso_cloud.Value;
import com.osohq.oso_cloud.Fact;
...
Oso oso = new Oso(YOUR_API_KEY);
List<Fact> facts = Arrays.asList(
new Fact("has_role", new Value("User", "1"), new Value("member"), new Value("Repo", "2")),
new Fact("has_role", new Value("User", "1"), new Value("member"), new Value("Repo", "3"))
);
oso.batch(tx -> {
facts.forEach(tx::delete);
});

Additionally, the batch method supports deleting facts matching patterns via FactPattern within the transaction:


import com.osohq.oso_cloud.Oso;
import com.osohq.oso_cloud.Value;
import com.osohq.oso_cloud.FactPattern;
import com.osohq.oso_cloud.ValuePattern;
...
Oso oso = new Oso(YOUR_API_KEY);
Value user1 = new Value("User", "1");
oso.batch(tx -> {
tx.delete(new FactPattern("has_role", user1, ValuePattern.ANY, ValuePattern.ANY));
});

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 API in favor of the QueryBuilder.

query to buildQuery

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:


import com.osohq.oso_cloud.Oso;
import com.osohq.oso_cloud.api.Value;
import com.osohq.oso_cloud.api.Fact;
...
Oso oso = new Oso(YOUR_API_KEY);
Value user = new Value("User", "bob");
Value role = new Value("reader");
Value repo = new Value("Repository", "acme");
Fact[] results = oso.query(
"has_role",
user,
role,
repo
);
// => [Fact("has_role", User{"bob"}, "reader", Repository{"acme"})]
boolean ok = results.length > 0;
// true

With the new QueryBuilder API:


import com.osohq.oso_cloud.Oso;
import com.osohq.oso_cloud.Value;
import com.osohq.oso_cloud.QueryBuilder.EvaluateArgs;
...
Oso oso = new Oso(YOUR_API_KEY);
Value user = new Value("User", "bob");
Value role = new Value("reader");
Value repo = new Value("Repository", "acme");
boolean ok = oso
.buildQuery("has_role", user, role, repo)
.evaluate(EvaluateArgs.exists());
// => true

Queries with type-constrained wildcards

The old Query API let you query for all the results of a particular type using null or potentially type markers. To migrate these, replace type-constrained wildcards with a TypedVar variable.

With the old Query API:


import com.osohq.oso_cloud.Oso;
import com.osohq.oso_cloud.api.Value;
import com.osohq.oso_cloud.api.Fact;
...
Oso oso = new Oso(YOUR_API_KEY);
Value user = new Value("User", "bob");
Value action = new Value("read");
// Query for all the repos `User:bob` can `read`
Fact[] results = oso.query("allow", user, action, new Value("Repository", null));
// => [
// Fact("allow", User{"bob"}, "read", Repository{"acme"}),
// Fact("allow", User{"bob"}, "read", Repository{"anvils"}),
// ]

With the new QueryBuilder API:


import com.osohq.oso_cloud.Oso;
import com.osohq.oso_cloud.Value;
import com.osohq.oso_cloud.TypedVar;
import com.osohq.oso_cloud.QueryBuilder.EvaluateArgs;
import java.util.List;
...
Oso oso = new Oso(YOUR_API_KEY);
Value user = new Value("User", "bob");
Value action = new Value("read");
TypedVar repos = new TypedVar("Repository");
List<String> repoIds = oso
.buildQuery("allow", user, action, repos)
.evaluate(EvaluateArgs.values(repos));
// => ["acme", "anvils"]

If you have several type-constrained wildcards in a single query, you may prefer to get results as a map:


import com.osohq.oso_cloud.Oso;
import com.osohq.oso_cloud.Value;
import com.osohq.oso_cloud.TypedVar;
import com.osohq.oso_cloud.QueryBuilder.EvaluateArgs;
import java.util.List;
import java.util.Map;
...
Oso oso = new Oso(YOUR_API_KEY);
Value action = new Value("read");
TypedVar users = new TypedVar("User");
TypedVar repos = new TypedVar("Repository");
Map<String, List<String>> userToRepos = oso
.buildQuery("allow", users, action, repos)
.evaluate(EvaluateArgs.map(users, EvaluateArgs.values(repos)));
// => {"bob"=["acme", "anvil"], "alice"=["anvil"], ...}

Queries with unconstrained wildcards

The old Query API let you use null to query for many types of results at once:


import com.osohq.oso_cloud.Oso;
import com.osohq.oso_cloud.api.Value;
import com.osohq.oso_cloud.api.Fact;
...
Oso oso = new Oso(YOUR_API_KEY);
Value user = new Value("User", "bob");
Value action = new Value("read");
// Query for all the objects `User:bob` can `read`
Fact[] results = oso.query("allow", user, action, null);
// => [
// Fact("allow", User{"bob"}, "read", Repository{"acme"}),
// Fact("allow", User{"bob"}, "read", Repository{"anvil"}),
// Fact("allow", User{"bob"}, "read", Issue{"123"}),
// ...
// ]

In the new QueryBuilder API, this is no longer possible directly.

Instead, make one request for each concrete type:


import com.osohq.oso_cloud.Oso;
import com.osohq.oso_cloud.Value;
import com.osohq.oso_cloud.TypedVar;
import com.osohq.oso_cloud.QueryBuilder.EvaluateArgs;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
...
Oso oso = new Oso(YOUR_API_KEY);
Value user = new Value("User", "admin");
Value action = new Value("read");
List<String> readableTypes = Arrays.asList("Repository", "Issue", "Organization");
Map<String, List<String>> resultsByType = readableTypes.stream().collect(Collectors.toMap(
typeName -> typeName,
typeName -> {
TypedVar resource = new TypedVar(typeName);
try {
return oso
.buildQuery("allow", user, action, resource)
.evaluate(EvaluateArgs.values(resource));
} catch (Exception e) {
return List.of();
}
}
));
// => {"Repository"=["acme", "anvil"], "Issue"=["123"], ...}

Handling wildcards in results

The old Query API sometimes returned results containing null, indicating that any value could apply at a particular position. For example:


import com.osohq.oso_cloud.Oso;
import com.osohq.oso_cloud.api.Value;
import com.osohq.oso_cloud.api.Fact;
...
Oso oso = new Oso(YOUR_API_KEY);
Value user = new Value("User", "admin");
Value action = new Value("read");
// Query for all the objects `User:admin` can `read`
Fact[] results = oso.query("allow", user, action, null);
// => [
// // `User:admin` can `read` anything
// Fact("allow", User{"admin"}, "read", null),
// ]

The new QueryBuilder API will instead return a list containing the string "*" to mean the same thing, just like the Check APIs:


import com.osohq.oso_cloud.Oso;
import com.osohq.oso_cloud.Value;
import com.osohq.oso_cloud.TypedVar;
import com.osohq.oso_cloud.QueryBuilder.EvaluateArgs;
import java.util.List;
...
Oso oso = new Oso(YOUR_API_KEY);
Value user = new Value("User", "admin");
Value action = new Value("read");
TypedVar repos = new TypedVar("Repository");
List<String> repoIds = oso
.buildQuery("allow", user, action, repos)
.evaluate(EvaluateArgs.values(repos));
// => ["*"] // admin can read anything

Context facts

If you are using context facts with query, check out the withContextFacts QueryBuilder method.

authorizeResources to buildQuery

The authorizeResources API was used to authorize many resources for a single actor and action.

With the old Query API:


import com.osohq.oso_cloud.Oso;
import com.osohq.oso_cloud.api.Value;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
...
Oso oso = new Oso(YOUR_API_KEY);
Value user = new Value("User", "1");
Value action = new Value("read");
List<String> repoIds = Arrays.asList("acme", "anvil");
List<Value> repos = repoIds.stream()
.map(id -> new Value("Repo", id))
.collect(Collectors.toList());
Value[] authorizedRepos = oso.authorizeResources(user, action, repos);
// => [Value("Repo", "acme")]

With the new QueryBuilder API:


import com.osohq.oso_cloud.Oso;
import com.osohq.oso_cloud.Value;
import com.osohq.oso_cloud.TypedVar;
import com.osohq.oso_cloud.QueryBuilder.EvaluateArgs;
import java.util.Arrays;
import java.util.List;
...
Oso oso = new Oso(YOUR_API_KEY);
Value user = new Value("User", "1");
Value action = new Value("read");
List<String> repoIds = Arrays.asList("acme", "anvil");
TypedVar repoVar = new TypedVar("Repo");
List<String> authorizedRepoIds = oso
.buildQuery("allow", user, action, repoVar)
.in(repoVar, repoIds)
.evaluate(EvaluateArgs.values(repoVar));
// => ["acme"]

If you are using context facts with authorizeResources, check out the withContextFacts QueryBuilder method.

Exported Types

You can now rely primarily on types exported directly from com.osohq.oso_cloud like Oso, Value, Fact, FactPattern, TypedVar and the QueryBuilder related classes/interfaces rather than implementation details previously exposed.