Go Client API
These are the docs for the v2 release. For v1, see the docs and the migration guide.
For changes to the Go Client API, see Changelog.
First, install the go-oso-cloud
package:
go get github.com/osohq/go-oso-cloud/v2
Before going through this guide, make sure you follow the Oso Cloud Quickstart to get your Oso Cloud API Key properly set in your environment.
Instantiating an Oso Cloud client
The Oso Cloud client provides an oso.NewClient
function that takes your Oso Cloud URL and API key:
import ( ... oso "github.com/osohq/go-oso-cloud/v2")osoClient := oso.NewClient("https://cloud.osohq.com", YOUR_API_KEY)// Later:e := osoClient.Insert(oso.NewFact("has_role", user, role, resource))if e != nil { // Handle error.}// Wherever authorization needs to be performed:allowed, e := osoClient.Authorize(user, action, resource)if e != nil { // Handle error.}if allowed { // Action is allowed.}
You should instantiate one client and share it across your application. Under the hood, it reuses connections to avoid paying the cost of negotiating a new connection on every request.
Specifying an Oso Fallback host
If you have deployed Oso Fallback nodes to your infrastructure, you may specify the host when instantiating the Oso Cloud client.
// Assumes Oso Fallback is hosted at http://localhost:8080osoClient := oso.NewClientWithFallbackUrl( "https://cloud.osohq.com", YOUR_API_KEY, "http://localhost:8080",)
Passing application entities into the client
Under the hood, Oso Cloud represents an entity in your application as a combination of a type and an ID, which together uniquely identify the entity.
The Go client represents these entities as oso.Value
structs with both Type
and ID
properties.
For example:
alice := oso.Value{Type: "User", ID: "alice"}
For convenience, you may prefer to use the oso.NewValue
helper function:
anvilsRepository := oso.NewValue("Repository", "anvils")// Equivalent to oso.Value{Type: "Repository", ID: "anvils"}
You will pass structs like these into nearly every function call you make to the Go client.
Wrapping primitive values
To construct facts whose arguments have primitive values,
use the oso.String
, oso.Boolean
, and oso.Integer
helpers. These helpers create
Value
s that wrap Go's built-in string
, bool
, and int64
types, and which
correspond to the built-in Polar String
, Boolean
, and Integer
primitives.
osoClient.Insert(oso.NewFact( "is_weird", oso.Integer(10), oso.String("yes"), oso.Boolean(true),))// Equivalent to:// osoClient.Insert(oso.NewFact(// "is_weird",// oso.Value{Type: "Integer", ID: "10"},// oso.Value{Type: "String", ID: "yes"},// oso.Value{Type: "Boolean", ID: "true"},// ))
Centralized Authorization Data API
Oso Cloud clients provide an API to manage authorization data stored directly in Oso Cloud.
Add fact: osoClient.Insert(fact)
Adds the given fact to Oso Cloud. Example:
osoClient.Insert(oso.NewFact(
"has_role",
oso.NewValue("User", "bob"),
oso.String("owner"),
oso.NewValue("Organization", "acme")
))
Delete fact: osoClient.Delete(pattern)
Deletes a fact, or all facts matching the given fact pattern. Does not return an error if no facts match the given fact or pattern.
Example:
// Delete a single fact:e := osoClient.Delete(oso.NewFact( "has_role", oso.NewValue("User", "bob"), oso.String("maintainer"), oso.NewValue("Repository", "anvil"),))
You can delete many facts at once by passing in a FactPattern
. The arguments in the pattern can be a Value
(matching arguments of that exact value at the given position), a ValueOfType
(matching arguments of the given type at the given position), or nil
(matching any argument at the given position).
Example:
// Delete all "has_role" facts where the first argument is the User// "alice", the second argument is anything, and the third argument is a// Repo.e := osoClient.Delete(oso.NewFactPattern( "has_role", oso.NewValue("User", "alice"), nil, oso.NewValueOfType("Repo"),))
Transactionally delete and add facts: osoClient.Batch(fn)
For Oso Cloud developer accounts, Batch
calls are limited to 20 facts. If
you attempt to send more than 20 facts, this function will return an error.
Allows deleting and inserting many facts in one atomic transaction. Deletions and insertions are run in the order they appear in the callback. Example:
e := osoClient.Batch(func(tx BatchTransaction) { tx.Insert(oso.NewFact( "has_role", oso.NewValue("User", "alice"), oso.String("owner"), oso.NewValue("Repo", "acme"), ) tx.Insert(oso.NewFact( "has_role", oso.NewValue("User", "alice"), oso.String("member"), oso.NewValue("Repo", "anvil"), ) tx.Delete(oso.NewFactPattern( "has_role", oso.NewValue("User", "bob"), nil, nil, ))})
As in calls to Delete, you may pass FactPattern
s with wildcards (nil
) and type-constrained wildcards (ValueOfType
) into calls to BatchTransaction.Delete
.
List facts: osoClient.Get(pattern)
For Oso Cloud developer accounts, Get
calls are limited to 1000 results. If
you have more than 1000 facts, the function will return an error.
Lists facts that are stored in Oso Cloud. Can be used to check the existence of a particular fact, or used to fetch all facts that have a particular argument:
// Get one fact:osoClient.Get(oso.NewFact( "has_role", oso.NewValue("User", "bob"), oso.String("admin"), oso.NewValue("Repository", "anvils"),))// => []oso.Fact{oso.Fact{// Predicate: "has_role",// Args: []oso.Value{// oso.Value{Type: "User", ID: "bob"},// oso.Value{Type: "String", ID: "admin"},// oso.Value{Type: "Repository", ID: "anvils"},// },// }}// List all role-related facts on the `anvils` repoosoClient.Get(oso.NewFactPattern( "has_role", nil, nil, oso.NewValue("Repository", "anvils"),))// => []oso.Fact{oso.Fact{// Predicate: "has_role",// Args: []oso.Value{// oso.Value{Type: "User", ID: "bob"},// oso.Value{Type: "String", ID: "admin"},// oso.Value{Type: "Repository", ID: "anvils"},// },// },// ...other has_role facts// }
Note that nil
behaves like a wildcard: the above means "find all has_role
facts where the third argument is the Repository anvils
, regardless of other
arguments".
You can also use oso.ValueOfType
to get facts with an argument of a certain type:
// List all role-related facts on any repo:osoClient.Get(oso.NewFactPattern( "has_role", nil, nil, oso.NewValueOfType("Repository"),))// => []oso.Fact{oso.Fact{// Predicate: "has_role",// Args: []oso.Value{// oso.Value{Type: "User", ID: "bob"},// oso.Value{Type: "String", ID: "reader"},// oso.Value{Type: "Repository", ID: "acme"},// },// },// ...other has_role facts// }
Get
only returns facts you've explicitly added. If you want to return a list
of authorized resources, use the Check API. For example, to
answer "on which resources can a given user perform a given action", use
List
. If you want to query for arbitrary information
that can be derived from your facts and policy, use the Query Builder
API.
Check API
For Oso Cloud developer accounts, the number of context facts per request is limited to 20; and the number of records returned is limited to 1000.
Context facts
The Check API lets you provide context facts with each request. When Oso Cloud performs a check, it considers the request's context facts in addition to any other centralized authorization data. Context facts are only used in the API call in which they're provided-- they do not persist across requests.
For more details, see Context Facts.
Check a permission: osoClient.Authorize(actor, action, resource)
Determines whether or not an action is allowed, based on a combination of authorization data and policy logic. Example:
allowed, e := osoClient.Authorize(user, "read", anvilsRepository)if e != nil { // Handle error.}if allowed { // Action is allowed.}
You may use osoClient.AuthorizeWithContext
to provide a slice of context facts.
Example:
allowed, e := osoClient.AuthorizeWithContext(user, "read", issueOnAnvilsRepository, []Fact{ // A context fact oso.NewFact("has_relation", issueOnAnvilsRepository, oso.String("parent"), anvilsRepository),}})
List authorized resources: osoClient.List(actor, action, resourceType)
Fetches a list of resource IDs on which an actor can perform a particular action. Example:
repositoryIds, e := osoClient.List(user, "read", "Repository")
You may use osoClient.ListWithContext
to provide a slice of context facts.
Example:
repositoryIds, e := osoClient.ListWithContext(user, "read", "Issue", []Fact{ // context facts oso.NewFact("has_relation", issueOnAnvilsRepository, oso.String("parent"), anvilsRepository), oso.NewFact("has_relation", issueOnAcmeRepository, oso.String("parent"), acmeRepository),})
List authorized actions: osoClient.Actions(actor, resource)
Fetches a list of actions which an actor can perform on a particular resource. Example:
actions, e := osoClient.Actions(user, anvilsRepository)
You may use osoClient.ActionsWithContext
to provide a slice of context facts.
Example:
actions, e := osoClient.ActionsWithContext(user, issueOnAnvilsRepository, []Fact{ // a context fact oso.NewFact("has_relation", issueOnAnvilsRepository, oso.String("parent"), anvilsRepository),})
Query for any rule: osoClient.BuildQuery(queryFact)
Query Oso Cloud for any predicate and any combination of concrete and wildcard
arguments. Unlike osoClient.Get
, which only lists facts you've added, you can use
osoClient.BuildQuery
to list derived information about any rule in your policy.
Example:
actor := oso.NewValue("User", "bob")repository := oso.TypedVar("Repository")// Query for all the repos `User:bob` can `read`repos, err := osoClient. BuildQuery(oso.NewQueryFact("allow", actor, oso.String("read"), repository)). EvaluateValues(repository)// => [ "acme", "anvils" ]
Query Builder API
The osoClient.BuildQuery()
API is a builder-style API where you chain methods to
construct a query and then execute it.
osoClient.BuildQuery(queryFact)
The osoClient.BuildQuery()
function takes the name of the rule you want to query and
a list of arguments. The arguments can be concrete values (e.g.,
oso.String("read")
or oso.NewValue("User", "bob")
) or type-constrained variables
constructed via the TypedVar
function:
actor := oso.NewValue("User", "bob")repository := oso.TypedVar("Repository")// Query for all the repositories bob can readosoClient.BuildQuery(oso.NewQueryFact("allow", actor, oso.String("read"), repository))// => QueryBuilder { ... }
Note: once you've finished building up your query, you must call an Evaluate*
method to
run it and get the results.
QueryBuilder.And(queryFact)
This function adds another condition that must be true of the query results.
For example:
actor := oso.NewValue("User", "bob")repository := oso.TypedVar("Repository")folder := oso.NewValue("Folder", "folder-1")// Query for all the repositories this user can read...osoClient. BuildQuery(oso.NewQueryFact("allow", actor, oso.String("read"), repository)). //... and require the repositories to belong to the given folder. And(oso.NewQueryFact("has_relation", repository, oso.String("folder"), folder))// => QueryBuilder { ... }
Note: once you've finished building up your query, you must call an Evaluate*
method to
run it and get the results.
QueryBuilder.In(variable, values)
This function requires a given TypedVar
query variable to be included in a
given set of values. You can only call In
once per variable per query. Calling
In
a second time with the same variable on the same query builder will throw
an error.
For example:
actor := oso.NewValue("User", "bob")repositories := []string{"acme", "anvil"}action := oso.TypedVar("String")repository := oso.TypedVar("Repository")// Query for all the actions this user can perform on any repository...osoClient. BuildQuery(NewQueryFact("allow", actor, action, repository)). // ...given that the repository's ID is in the given list of IDs. In(repository, repositories)// => QueryBuilder { ... }
Note: once you've finished building up your query, you must call an Evaluate*
method to
run it and get the results.
QueryBuilder.WithContextFacts(contextFacts)
This function adds the given context facts to the query. For example:
actor := oso.NewValue("User", "bob")repository := oso.TypedVar("Repository")// Query for all the repositories bob can read...osoClient. BuildQuery(oso.NewQueryFact("allow", actor, "read", repository)). // ...while including the fact that bob owns acme WithContextFacts([]Fact{ NewFact("has_role", actor, oso.String("owner"), oso.NewValue("Repository", "acme")), })// => QueryBuilder { ... }
For more information on context facts, see this section.
Note: once you've finished building up your query, you must call an Evaluate*
method to
run it and get the results.
QueryBuilder.EvaluateExists()
This function evaluates the built query, fetching the results from Oso.
Returns a boolean representing if the given conditions in the query were satisfied.
// true if the given actor can perform the given action on the given resourceallowed, err := osoClient. BuildQuery(oso.NewQueryFact("allow", actor, action, resource)). EvaluateExists()
QueryBuilder.EvaluateValues(variable)
This function evaluates the built query, fetching the results from Oso.
Returns a slice of values for the given variable. For example:
action := oso.TypedVar("String")// all the actions the actor can perform on the given resource- eg. ["read", "write"]actions, err := osoClient. BuildQuery(oso.NewQueryFact("allow", actor, action, resource)). EvaluateValues(action)
QueryBuilder.EvaluateCombinations(variables)
This function evaluates the built query, fetching the results from Oso.
Returns a slice of tuples of values for the given variables. For example:
action := oso.TypedVar("String")repo := oso.TypedVar("Repo")// a slice of pairs of allowed actions and repo IDs-// eg. [["read", "acme"], ["read", "anvil"], ["write", "anvil"]]pairs, err := osoClient. BuildQuery(oso.NewQueryFact("allow", actor, action, repo)). EvaluateCombinations([]Variable {action, repo})
QueryBuilder.Evaluate(&result, shape)
This function evaluates the built query, fetching the results from Oso.
The returned value is written into the passed &result
argument.
The type of this result varies based on the type of the shape
argument you pass in.
-
If you pass
nil
, this function writes a boolean. For example:var allowed boolerr := osoClient.BuildQuery(oso.NewQueryFact("allow", actor, action, resource)).Evaluate(&allowed, nil)// => true if the given actor can perform the given action on the given resource -
If you pass a single
TypedVar
query variable, this function writes a slice of values for that variable. For example:action := oso.TypedVar("String")var actions []stringactions := osoClient.BuildQuery(oso.NewQueryFact("allow", actor, action, resource)).Evaluate(&actions, action)// => all the actions the actor can perform on the given resource- eg. ["read", "write"] -
If you pass a slice of
TypedVar
query variables, this function writes a slice of tuples of values for those variables. For example:action := oso.TypedVar("String")repository := oso.TypedVar("Repository")var pairs [][]stringerr := osoClient.BuildQuery(oso.NewQueryFact("allow", actor, action, repository))Evaluate(&pairs, []oso.Variable {action, repository})// => a slice of pairs of allowed actions and repo IDs-// eg. [["read", "acme"], ["read", "anvil"], ["write", "anvil"]] -
If you pass a dict mapping one
TypedVar
query variable (call it K) to another (call it V), this function writes a map grouping unique values of K to unique values of V for each value of K. For example:action := oso.TypedVar("String")repository := oso.TypedVar("Repository")var repoActions map[string][]stringerr := osoClient.BuildQuery(oso.NewQueryFact("allow", actor, action, repository))Evaluate(&repoActions, map[Variable]Variable{repository: action})// => a mapping of repo IDs to allowed actions-// eg. { "acme": ["read"], "anvil": ["read", "write"]}
Some queries have unconstrained results. For instance, maybe users with the
admin
role can read all Repository
entities in your application. In this
case, rather than returning an array containing the ID of every repository,
Evaluate
will write an array containing the string "*"
. For example:
repos := oso.TypedVar("Repository")var results []stringerr := osoClient. BuildQuery(oso.NewQueryFact("allow", oso.NewValue("User", "admin"), oso.String("read"), repos)). Evaluate(&results, repos) // Return just the IDs of the repos admin can read// => ["*"] // admin can read anything
Query Builder examples
Field-level access control
actor := oso.NewValue("User", "alice")resource := oso.NewValue("Repository", "anvil")field := oso.TypedVar("Field")results, err := osoClient. BuildQuery(oso.NewQueryFact("allow_field", actor, oso.String("read"), resource, field)). EvaluateValues(field)// => Returns a list of the fields alice can read on the given repo- eg.// ["name", "stars"]
Checking a global permission
actor := oso.NewValue("User", "alice")canCreate, err := osoClient. BuildQuery(oso.NewQueryFact("has_permission", actor, oso.String("create_repository"))). EvaluateExists()// => true if alice has the global "create_repository" permission
Fetching authorized actions for a collection of resources
repos := []string{"anvil", "acme"}actor := oso.NewValue("User", "alice")action := oso.TypedVar("String")repo := oso.TypedVar("Repository")var results map[string]stringerr := osoClient. BuildQuery(oso.NewQueryFact("allow", actor, action, repo)). In(repo, repos). Evaluate(&results, map[Variable]Variable{repo: action})// => Returns a map from the given repos to the actions alice can perform on those repos- eg.// { "anvil": ["read"], "acme": ["read", "write"] }
Filtering out unauthorized resources from a collection
repos := []string{"anvil", "acme"}actor := oso.NewValue("User", "bob")repo := oso.TypedVar("Repository")results, err := osoClient. BuildQuery(oso.NewQueryFact("allow", actor, oso.String("read"), repo)). In(repo, repos). EvaluateValues(repo)// => Returns the subset of `repos` that bob can read- eg.// ["anvil"]
Filtering an authorize()
query based on a relation
actor := oso.NewValue("User", "bob")repo := oso.TypedVar("Repository");org := oso.NewValue("Org", "coolguys")results, err := osoClient. BuildQuery(oso.NewQueryFact("allow", actor, oso.String("read"), repo)). And(oso.NewQueryFact("has_relation", repo, oso.String("parent"), org)). EvaluateValues(repo)// => Returns the IDs of the repos in the coolguys org that bob can read- eg.// ["acme", "anvil"]
Learn more about how to query Oso Cloud.
Local Check API
The local check API lets you perform authorization using data that's distributed across Oso Cloud and your own database.
After creating your Local Authorization configuration, provide the path to the YAML file that specifies how to resolve facts in your database.
osoClient := oso.NewClientWithDataBindings(..., "path/to/local_authorization_config.yaml")
For more information, see Local Authorization.
List authorized resources with local data: osoClient.ListLocal(actor, action, resourceType, column)
Fetches a filter that can be applied to a database query to return just the resources on which an actor can perform an action. Example with GORM (opens in a new tab):
var issues []Issuedb.Find(&issues, osoClient.ListLocal(user, "read", "Issue", "id"))
You may use the GORM query interface (opens in a new tab) to combine this authorization filter with other things such as ordering and pagination.
You may use osoClient.ListLocalWithContext
to provide a slice of context facts.
Example:
var issues []Issuedb.Find(&issues, osoClient.ListLocalWithContext(user, "read", "Issue", "id", []Fact{ // context facts oso.NewFact("has_relation", issueOnAnvilsRepository, oso.String("parent"), anvilsRepository), oso.NewFact("has_relation", issueOnAcmeRepository, oso.String("parent"), acmeRepository),}))
Check a permission with local data: osoClient.AuthorizeLocal(actor, action, resource)
Fetches a query that can be run against your database to determine whether an actor can perform an action on a resource. Example with GORM (opens in a new tab):
var authorizeResult AuthorizeResultdb.Raw(osoClient.AuthorizeLocal(alice, "read", swageIssue)).Scan(&authorizeResult)if (!authorizeResult.Allowed) { return fmt.Errorf("Action is not allowed")}
You may use osoClient.AuthorizeLocalWithContext
to provide a slice of context facts.
Example:
var authorizeResult AuthorizeResultdb.Raw(osoClient.AuthorizeLocalWithContext(alice, "read", swageIssue, []Fact{ // A context fact oso.NewFact("has_relation", swageIssue, oso.String("parent"), anvilsRepository),})).Scan(&authorizeResult)if !authorizeResult.Allowed { return fmt.Errorf("Action is not allowed")}
List authorized actions with local data: osoClient.ActionsLocal(actor, resource)
Fetches a query that can be run against your database to fetch the actions an actor can perform on a resource. Example with GORM (opens in a new tab):
query, err := osoClient.ActionsLocal(alice, swageIssue)if err != nil { ...}var actions []stringdb.Raw(query).Pluck("actions", &actions)
You may use osoClient.ActionsLocalWithContext
to provide a slice of context facts.
Example:
query, err := osoClient.ActionsLocalWithContext(alice, swageIssue, []Fact{ // a context fact oso.NewFact("has_relation", swageIssue, oso.String("parent"), anvilsRepository),})if err != nil { ...}var actions []stringdb.Raw(query).Pluck("actions", &actions)
Policy API
Update the active policy: osoClient.Policy(policy)
Updates the policy in Oso Cloud. The string passed into this method should be written in Polar. Example:
e := osoClient.Policy("actor User {}")
This command will run any tests defined in your policy. If one or more of these tests fail, your policy will not be updated.
Get policy metadata: osoClient.GetPolicyMetadata()
Returns metadata about the currently active policy. Example:
metadata, err := osoClient.getPolicyMetadata()
returns:
PolicyMetadata{ Resources: map[string]ResourceMetadata{ "Organization": { Permissions: []string{ "add_member", "read", "repository.create", "repository.delete", "repository.read", }, Roles: []string{"admin", "member"}, Relations: map[string]string{ "parent": "Organization", }, }, "User": { Permissions: []string{}, Roles: []string{}, Relations: map[string]string{}, }, "global": { Permissions: []string{}, Roles: []string{}, Relations: map[string]string{}, }, },}
See the Policy Metadata guide for more information on use cases.
Talk to an Oso engineer
If you'd like to learn more about using Oso Cloud in your app or have any questions about this guide, schedule a 1x1 with an Oso engineer. We're happy to help.