Migrating to v2

Migrating to v2 of the Go SDK

The v2 release of the Go 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.

Updating your imports

Run go get github.com/osohq/go-oso-cloud/v2 to install v2 of the SDK.

In each place you import go-oso-cloud, add /v2 to the module import path:


- import (oso "github.com/osohq/go-oso-cloud")
+ import (oso "github.com/osohq/go-oso-cloud/v2")

Structs

The Name field on the Fact struct has been renamed Predicate.

The Instance struct has been replaced with Value. Unlike Instance, it is invalid for a Value to have an zero-valued ID or Type field.

To distinguish between functions that accept concrete facts and those that accept patterns (such as Get or Delete), the type FactPattern has been introduced. In v1, these were both represented by Fact.

There are also helper functions for constructing Fact, FactPattern, and Value instances in a more ergonomic way:


alice := oso.NewValue("User", "alice") // Value{Type: "User", ID: "alice"}
oso.NewFact(
"has_role",
alice,
oso.String("reader"),
oso.NewValue("Repository", "123"),
) // Fact{Predicate: "has_role", Args: []Value{ alice, ... }}

The migration instructions for each API function contain examples of how to use these helper functions.

Centralized Authorization Data API

The Centralized Authorization Data API has been condensed from 6 methods to 4: Insert, Delete, Get, and Batch.

Tell to Insert

To migrate from Tell to Insert, replace Instances with Values, and wrap the arguments in a call to oso.NewFact:


- osoClient.Tell(
- "has_role",
- oso.Instance{Type: "User", ID: "bob"},
- oso.String("owner"),
- oso.Instance{Type: "Organization", ID: "acme"},
- )
+ osoClient.Insert(oso.NewFact(
+ "has_role",
+ oso.NewValue("User", "bob"),
+ oso.String("owner"),
+ oso.NewValue("Organization", "acme"),
+ ))

Delete

To migrate from the previous Delete API, replace Instances with Values, and wrap the arguments in a call to oso.NewFactPattern:


- osoClient.Delete(
- "has_role",
- oso.Instance{Type: "User", ID: "bob"},
- oso.String("maintainer"),
- oso.Instance{Type: "Repository", ID: "anvil"},
- )
+ osoClient.Delete(oso.NewFactPattern(
+ "has_role",
+ oso.NewValue("User", "bob"),
+ oso.String("maintainer"),
+ oso.NewValue("Repository", "anvil"),
+ ))

Additionally, the new Delete function supports deleting all facts matching a pattern:


osoClient.Delete(oso.NewFactPattern(
"has_role",
oso.NewValue("User", "1"),
nil,
nil,
)) // Remove all of User 1's roles across all resources

Get

To migrate from the previous Get API, replace Instances with Values and wrap them in a call to oso.NewFactPattern:


- oso.Get(
- "has_role",
- oso.Instance{Type: "User", ID: "bob"},
- oso.String("admin"),
- oso.Instance{Type: "Repository", ID: "anvils"},
- )
+ oso.Get(oso.NewFactPattern(
+ "has_role",
+ oso.NewValue("User", "bob"),
+ oso.String("admin"),
+ oso.NewValue("Repository", "anvils"),
+ ))

If you used an oso.Instance with a Type but no ID to fetch facts with arguments of a particular type (but no particular ID), use oso.ValueOfType instead:


// Fetch all has_role facts for Users with an admin role on Repository anvils
- oso.Get(
- "has_role",
- oso.Instance{Type: "User"},
- oso.String("admin"),
- oso.Instance{Type: "Repository", ID: "anvils"},
- )
+ oso.Get(oso.NewFactPattern(
+ "has_role",
+ oso.NewValueOfType("User"),
+ oso.String("admin"),
+ oso.NewValue("Repository", "anvils"),
+ ))

If you used the zero-valued oso.Instance as a wildcard, replace it with nil:


// Fetch all has_role facts for Users with an admin role on any resource
- oso.Get(
- "has_role",
- oso.Instance{Type: "User"},
- oso.String("admin"),
- oso.Instance{},
- )
+ oso.Get(oso.NewFactPattern(
+ "has_role",
+ oso.NewValueOfType("User"),
+ oso.String("admin"),
+ nil,
+ ))

Additionally, it is no longer possible to use a wildcard in the fact predicate in calls to Get.

Bulk to Batch or Delete

To migrate from Bulk to Batch, call OsoClient.Batch, call tx.Delete once for each deletion pattern in the payload, and call tx.Insert once for each fact to insert. For deletes, you will need to convert your Facts to FactPatterns- use the oso.NewFactPattern helper for this. As with Insert and Delete, be sure to replace Instances with Values in your facts, too.


- osoClient.Bulk([]oso.Fact{ // Deletes
- oso.Fact{
- Name: "has_role",
- Args: []oso.Instance{
- oso.Instance{Type: "User", ID: "bob"},
- oso.String("viewer"),
- oso.Instance{Type: "Repository", ID: "anvil"},
- },
- },
- }, []oso.Fact{ // Inserts
- oso.Fact{
- Name: "has_role",
- Args: []oso.Instance{
- oso.Instance{Type: "User", ID: "bob"},
- oso.String("maintainer"),
- oso.Instance{Type: "Repository", ID: "anvil"},
- },
- },
- })
+ osoClient.Batch(func(tx oso.BatchTransaction) {
+ tx.Delete(oso.NewFactPattern(
+ "has_role",
+ oso.NewValue("User", "bob"),
+ oso.String("viewer"),
+ oso.NewValue("Repository", "anvil"),
+ ))
+ tx.Insert(oso.NewFact(
+ "has_role",
+ oso.NewValue("User", "bob"),
+ oso.String("maintainer"),
+ oso.NewValue("Repository", "anvil"),
+ ))
+ })

Additionally, the Delete API now handles deleting many facts matching a pattern at once. If you were previously using the Bulk API just to delete many facts at once, you can now use the Delete API directly:


- osoClient.Bulk(
- []oso.Fact{ // Deletes
- oso.Fact{
- Name: "has_role",
- Args: []oso.Instance{
- oso.Instance{Type: "User", ID: "bob"},
- oso.Instance{},
- oso.Instance{}
- },
- },
- },
- []oso.Fact{}, // Inserts
- })
+ osoClient.Delete(oso.NewFactPattern(
+ "has_role",
+ oso.NewValue("User", "bob"),
+ nil,
+ nil,
+ ))

If you used an oso.Instance with a Type but no ID to fetch facts with arguments of a particular type (but no particular ID), use oso.ValueOfType instead:


// Delete all has_role facts for Users with an admin role on Repository anvils
- osoClient.Bulk(
- []oso.Fact{ // Deletes
- oso.Fact{
- Name: "has_role",
- Args: []oso.Instance{
- oso.Instance{Type: "User"},
- oso.String("admin"),
- oso.Instance{Type: "Repository", ID: "anvils"},
- },
- },
- },
- []oso.Fact{}, // Inserts
- })
+ oso.Delete(oso.NewFactPattern(
+ "has_role",
+ oso.NewValueOfType("User"),
+ oso.String("admin"),
+ oso.NewValue("Repository", "anvils"),
+ ))

If you used the zero-valued oso.Instance as a wildcard, replace it with nil:


// Delete all has_role facts for Users with an admin role on any resource
- osoClient.Bulk(
- []oso.Fact{ // Deletes
- oso.Fact{
- Name: "has_role",
- Args: []oso.Instance{
- oso.Instance{Type: "User"},
- oso.String("admin"),
- oso.Instance{},
- },
- },
- },
- []oso.Fact{}, // Inserts
- })
+ oso.Delete(oso.NewFactPattern(
+ "has_role",
+ oso.NewValueOfType("User"),
+ oso.String("admin"),
+ nil,
+ ))

The migration notes about wildcards apply regardless of whether you use OsoClient.Batch or OsoClient.Delete.

BulkTell to Batch

To migrate from BulkTell to Batch, call OsoClient.Batch and call tx.Insert for each fact in the payload. As with Insert, be sure to replace Instances with Values in your facts, too.


- osoClient.BulkTell([]oso.Fact{
- oso.Fact{
- Name: "has_role",
- Args: []oso.Instance{
- oso.Instance{Type: "User", ID: "bob"},
- oso.String("owner"),
- oso.Instance{Type: "Organization", ID: "acme"},
- },
- },
- oso.Fact{
- Name: "has_role",
- Args: []oso.Instance{
- oso.Instance{Type: "User", ID: "bob"},
- oso.String("maintainer"),
- oso.Instance{Type: "Repository", ID: "anvil"},
- },
- },
- })
+ osoClient.Batch(func(tx oso.BatchTransaction) {
+ tx.Insert(oso.NewFact(
+ "has_role",
+ oso.NewValue("User", "bob"),
+ oso.String("owner"),
+ oso.NewValue("Organization", "acme"),
+ ))
+ tx.Insert(oso.NewFact(
+ "has_role",
+ oso.NewValue("User", "bob"),
+ oso.String("maintainer"),
+ oso.NewValue("Repository", "anvil"),
+ ))
+ })

As above, you may find it easier to use the NewFact and NewValue helper functions than to construct Fact and Value structs by hand.

BulkDelete to Batch

To migrate from BulkDelete to Batch, call OsoClient.Batch and call tx.Delete for each fact in the payload. You will need to convert your Facts to FactPatterns- use the oso.NewFactPattern helper for this. As with Delete, be sure to replace Instances with Values in your facts, too.


- osoClient.BulkDelete([]oso.Fact{
- oso.Fact{
- Name: "has_role",
- Args: []oso.Instance{
- oso.Instance{Type: "User", ID: "bob"},
- oso.String("owner"),
- oso.Instance{Type: "Organization", ID: "acme"},
- },
- },
- oso.Fact{
- Name: "has_role",
- Args: []oso.Instance{
- oso.Instance{Type: "User", ID: "bob"},
- oso.String("maintainer"),
- oso.Instance{Type: "Repository", ID: "anvil"},
- },
- },
- })
+ osoClient.Batch(func(tx BatchTransaction) {
+ tx.Delete(oso.NewFactPattern(
+ "has_role",
+ oso.NewValue("User", "bob"),
+ oso.String("owner"),
+ oso.NewValue("Organization", "acme"),
+ ))
+ tx.Delete(oso.NewFactPattern(
+ "has_role",
+ oso.NewValue("User", "bob"),
+ oso.String("maintainer"),
+ oso.NewValue("Repository", "anvil"),
+ ))
+ })

Additionally, the new Batch function supports deleting all facts matching a pattern:


osoClient.Batch(func (tx BatchTransaction) {
tx.Delete(oso.NewFactPattern(
"has_role",
oso.NewValue("User", "1"),
nil,
oso.NewValueOfType("Repository"),
)) // Remove all of User 1's roles across all Repositories
})

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 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:


results, err := osoClient.Query(
"has_role",
&oso.Instance{Type: "User", ID: "bob"},
&oso.String("reader"),
&oso.Instance{Type: "Repository", ID: "acme"},
)
// => []oso.Fact{{
// Name: "has_role",
// Args: []oso.Instance{
// {Type: "User", ID: "bob"},
// {Type: "String", ID: "reader"},
// {Type: "Repository", ID: "acme"},
// },
// }}
ok := err != nil && len(results) > 0
// => true

With the new QueryBuilder API:


ok, err := osoClient.
BuildQuery(oso.NewQueryFact(
"has_role",
oso.NewValue("User", "bob"),
oso.String("reader"),
oso.NewValue("Repository", "acme"),
)).
EvaluateExists() // Returns a boolean
// => true

Queries with type-constrained wildcards

The old Query API let you query for all the results of a particular type by using an oso.Instance with a Type but no ID. To migrate these, replace type-constrained wildcards with a oso.TypedVar(myType) variable.

With the old Query API:


// Query for all the repos User `bob` can `read`
results, err := osoClient.Query(
"allow",
&oso.Instance{Type: "User", ID: "bob"},
&oso.String("read"),
&oso.Instance{Type: "Repository"},
)
// => []oso.Fact{
// {
// Name: "allow",
// Args: []oso.Instance{
// {Type: "User", ID: "bob"},
// {Type: "String", ID: "read"},
// {Type: "Repository", ID: "acme"},
// },
// }, {
// Name: "allow",
// Args: []oso.Instance{
// {Type: "User", ID: "bob"},
// {Type: "String", ID: "read"},
// {Type: "Repository", ID: "anvils"},
// },
// }

With the new QueryBuilder API:


repos := oso.TypedVar("Repository")
repoIds, err := osoClient.
BuildQuery(oso.NewQueryFact(
"allow",
oso.NewValue("User", "bob"),
oso.String("read"),
repos,
)).
EvaluateValues(repos) // Return just the IDs of the repos bob can read
// => []string{"acme", "anvils"}

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


users := oso.TypedVar("User")
repos := oso.TypedVar("Repository")
var result map[string][]string
err := osoClient.
BuildQuery(oso.NewQueryFact( // Query for which users can read which repos
"allow",
users,
oso.String("read"),
repos,
)).
Evaluate(&result, map[oso.Variable]oso.Variable{users: repos})
// Write the results, a map of user IDs to lists of repo IDs, to `&result`
// => map[string][]string{"bob": {"acme", "anvil"}, "alice": {"anvil"}}

Queries with unconstrained wildcards

The old Query API let you use a zero-valued Instance or nil to query for many types of results at once:


// Query for all the objects User `bob` can `read`
results, err := osoClient.Query(
"allow",
&oso.Instance{Type: "User", ID: "bob"},
&oso.String("read"),
nil,
)
// => []oso.Fact{
// {
// Name: "allow",
// Args: []oso.Instance{
// {Type: "User", ID: "bob"},
// {Type: "String", ID: "read"},
// {Type: "Repository", ID: "acme"},
// },
// }, {
// Name: "allow",
// Args: []oso.Instance{
// {Type: "User", ID: "bob"},
// {Type: "String", ID: "read"},
// {Type: "Issue", ID: "123"},
// },
// ...
// }

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

Instead, make one request for each concrete type:


bob := oso.NewValue("User", "bob")
repos := oso.TypedVar("Repository")
repoIds, err := osoClient.
BuildQuery(oso.NewQueryFact("allow", bob, oso.String("read"), repos)).
EvaluateValues(repos)
// => []string{"acme", "anvil"}
issues := oso.TypedVar("Issue")
issueIds, err := osoClient
.BuildQuery(oso.NewQueryFact("allow", bob, oso.String("read"), issues))
.EvaluateValues(issues)
// => []string{"123"}
orgs := oso.TypedVar("Organization")
orgIds, err := osoClient.
BuildQuery(oso.NewQueryFact("allow", bob, oso.String("read"), orgs)).
EvaluateValues(orgs)
// => []string{"org1", "org2"}

Handling wildcards in results

The old Query API sometimes returned results containing a zero-valued oso.Instance, indicating that any value could apply at a particular position. For example:


// Query for all the objects User `bob` can `read`
results, err := osoClient.Query(
"allow",
&oso.Instance{Type: "User", ID: "bob"},
&oso.String("read"),
nil,
)
// => []oso.Fact{{
// Name: "allow",
// Args: []oso.Instance{
// {Type: "User", ID: "bob"},
// {Type: "String", ID: "read"},
// {Type: "", ID: ""}, // Bob can read anything
// },
// }}

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


orgVar = oso.TypedVar("Organization")
result, err := osoClient.BuildQuery(oso.NewQueryFact(
"allow",
oso.NewValue("User", "bob"),
oso.String("read"),
orgVar
)).
EvaluateValues(orgVar)
// => []string{"*"} // Bob can read any Organization

Context facts

You can pass context facts into the QueryBuilder by calling the WithContextFacts method before you call Evaluate.

AuthorizeResources to BuildQuery

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


// Return only the subset of the given resources that bob can read
results, e := osoClient.AuthorizeResources(
oso.Instance{Type: "User", ID: "bob"},
"read",
[]oso.Instance{
{Type: "Repository", ID: "anvils"},
{Type: "Repository", ID: "acme"},
},
)
// => []oso.Instance{{Type: "Repository, ID: "acme"}}

With the new QueryBuilder API:


repoIds := []string{"acme", "anvil"}
repoVar := oso.TypedVar("Repository")
allowedRepoIds, e := osoClient.
BuildQuery(oso.NewQueryFact(
"allow",
oso.NewValue("User", "bob"),
oso.String("read"),
repoVar
)).
In(repos, repoIds).
EvaluateValues(repoVar)
// => []string{"acme"}

If you are using AuthorizeResourcesWithContext to include context facts, check out the WithContextFacts QueryBuilder function.