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 factFact[] specificRole = oso.get(new FactPattern("has_role", user, new Value("admin"), repo));// List all roles for User:1 on any resourceFact[] userRoles = oso.get(new FactPattern("has_role", user, ValuePattern.ANY, ValuePattern.ANY));// List all roles on the 'anvils' repo for any userFact[] 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.