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 Instance
s with Value
s, 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 Instance
s with Value
s, 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 Instance
s with Value
s 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 Fact
s to FactPattern
s- use the
oso.NewFactPattern
helper for this. As with Insert and
Delete, be sure to replace Instance
s with Value
s 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 Instance
s with Value
s 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 Fact
s
to FactPattern
s- use the oso.NewFactPattern
helper for this. As with
Delete, be sure to replace Instance
s with Value
s 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][]stringerr := 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 readresults, 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.