User Groups
In this guide you will learn how to add users to groups and grant groups access to an object using OpenFGA.
Adding a relationship tuple specifying that a group has a relation to an object is helpful in cases where you want to encompass a set of users with the same relation to an object. For example:
- Grant a group of
engineers
viewer
access toroadmap.doc
- Create a
block_list
ofmembers
who can't access adocument
- Sharing a
document
with ateam
- Granting
viewer
access to aphoto
tofollowers
only - Making a
file
viewable for allusers
within anorganization
- Restricting access from or to
users
in a certainlocale
Before you start
In order to understand this guide correctly you must be familiar with some OpenFGA Concepts and know how to develop the things that we will list below.
Assume that you have the following authorization model.
You have an object called document
that users can be related to as an editor
.
You have an object called
document
that users can be related to as an editor
.- DSL
- JSON
model
schema 1.1
type user
type document
relations
define editor: [user]
{
"schema_version": "1.1",
"type_definitions": [
{
"type": "user"
},
{
"type": "document",
"relations": {
"editor": {
"this": {}
}
},
"metadata": {
"relations": {
"editor": {
"directly_related_user_types": [
{
"type": "user"
}
]
}
}
}
}
]
}
In addition, you will need to know the following:
Direct Access
You need to know how to create an authorization model and create a relationship tuple to grant a user access to an object. Learn more →
OpenFGA Concepts
- A Type: a class of objects that have similar characteristics
- A User: an entity in the system that can be related to an object
- A Relation: is a string defined in the type definition of an authorization model that defines the possibility of a relationship between an object of the same type as the type definition and a user in the system
- An Object: represents an entity in the system. Users' relationships to it can be define through relationship tuples and the authorization model
- A Relationship Tuple: a grouping consisting of a user, a relation and an object stored in OpenFGA
Step By Step
As we develop our application, we might encounter use cases where a group of users have a certain role or permission on an object. For example, members
of a certain team
might have an editor
relation to a certain document
.
In order to represent this in OpenFGA, we need:
- Introduce the concept of a
team
to the authorization model - Add users as
members
to theteam
- Assign the
team
members a relation to an object - Checking an individual member's access to the object
01. Introduce The Concept Of A Team To The Authorization Model
We need to define the object team
in our authorization model. In our use case, a team
can have member
s, so we make the following changes to our authorization model:
- DSL
- JSON
model
schema 1.1
type user
type document
relations
define editor: [team#member]
type team
relations
define member: [user]
{
"schema_version": "1.1",
"type_definitions": [
{
"type": "user"
},
{
"type": "document",
"relations": {
"editor": {
"this": {}
}
},
"metadata": {
"relations": {
"editor": {
"directly_related_user_types": [
{
"type": "team",
"relation": "member"
}
]
}
}
}
},
{
"type": "team",
"relations": {
"member": {
"this": {}
}
},
"metadata": {
"relations": {
"member": {
"directly_related_user_types": [
{
"type": "user"
}
]
}
}
}
}
]
}
02. Add Users As Members To The Team
We can now assign users as member
s of team
s. Let's create a new relationship tuple that states alice is a member of team:writers.
- Node.js
- Go
- .NET
- Python
- curl
- CLI
- Pseudocode
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
// import the SDK
const { OpenFgaClient } = require('@openfga/sdk');
// Initialize the SDK with no auth - see "How to setup SDK client" for more options
const fgaClient = new OpenFgaClient({
apiScheme: process.env.FGA_API_SCHEME, // Either "http" or "https", defaults to "https"
apiHost: process.env.FGA_API_HOST, // required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
storeId: process.env.FGA_STORE_ID,
authorizationModelId: process.env.FGA_MODEL_ID, // Optional, can be overridden per request
});
await fgaClient.write({
writes: [
{ user: 'user:alice', relation: 'member', object: 'team:writers'}]
},
}, {
authorization_model_id: "1uHxCSuTP0VKPYSnkq1pbb1jeZw"
});
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import (
. "github.com/openfga/go-sdk/client"
"os"
)
func main() {
// Initialize the SDK with no auth - see "How to setup SDK client" for more options
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiScheme: os.Getenv("FGA_SCHEME"), // Either "http" or "https", defaults to "https"
ApiHost: os.Getenv("FGA_API_HOST"), // required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
StoreId: os.Getenv("FGA_STORE_ID"), // optional, not needed for `CreateStore` and `ListStores`, required before calling for all other methods
AuthorizationModelId: openfga.PtrString(os.Getenv("FGA_MODEL_ID")), // Optional, can be overridden per request
})
if err != nil {
// .. Handle error
}
}
options := ClientWriteOptions{
AuthorizationModelId: openfga.PtrString("1uHxCSuTP0VKPYSnkq1pbb1jeZw"),
}
body := fgaClient.ClientWriteRequest{
Writes: &[]ClientTupleKey{
{
User: openfga.PtrString("user:alice"),
Relation: openfga.PtrString("member"),
Object: openfga.PtrString("team:writers"),
}, } }
data, err := fgaClient.Write(context.Background()).Body(requestBody).Options(options).Execute()
if err != nil {
// .. Handle error
}
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
// import the SDK
using OpenFga.Sdk.Client;
using OpenFga.Sdk.Client.Model;
using OpenFga.Sdk.Model;
using Environment = System.Environment;
namespace Example;
class Example {
public static async Task Main() {
// Initialize the SDK with no auth - see "How to setup SDK client" for more options
var configuration = new ClientConfiguration() {
ApiScheme = Environment.GetEnvironmentVariable("FGA_API_SCHEME"), // Either "http" or "https", defaults to "https"
ApiHost = Environment.GetEnvironmentVariable("FGA_API_HOST"), // required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
StoreId = Environment.GetEnvironmentVariable("FGA_STORE_ID"), // optional, not needed for `CreateStore` and `ListStores`, required before calling for all other methods
AuthorizationModelId = Environment.GetEnvironmentVariable("FGA_MODEL_ID"), // Optional, can be overridden per request
};
var fgaClient = new OpenFgaClient(configuration);
}
}
var options = new ClientListObjectsOptions {
AuthorizationModelId = "1uHxCSuTP0VKPYSnkq1pbb1jeZw",
};
var body = new ClientWriteRequest() {
Writes = new List<ClientTupleKey>() {
new() { User = "user:alice", Relation = "member", Object = "team:writers" }
},
};
var response = await fgaClient.Write(body, options);
Initialize the SDK
# ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import os
import json
import openfga_sdk
from openfga_sdk.client import OpenFgaClient
configuration = openfga_sdk.ClientConfiguration(
api_scheme = os.environ.get('FGA_API_SCHEME'), # Either "http" or "https", defaults to "https"
api_host = os.environ.get('FGA_API_HOST'), # required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
store_id = os.environ.get('FGA_STORE_ID') # optional, not needed for `CreateStore` and `ListStores`, required before calling for all other methods
authorization_model_id = os.environ.get('FGA_MODEL_ID'), # Optional, can be overridden per request
)
# Enter a context with an instance of the OpenFgaClient
async with OpenFgaClient(configuration) as fga_client:
api_response = await fga_client.read_authorization_models()
await fga_client.close()
options = {
"authorization_model_id": "1uHxCSuTP0VKPYSnkq1pbb1jeZw"
}
body = body = ClientWriteRequest(
writes=[
ClientTuple(
user="user:alice",
relation="member",
object="team:writers",
),,
),
",
)
response = await fga_client.write(body, options)
Set FGA_SERVER_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_SERVER_URL according to the service you are using (e.g. https://api.fga.example)
curl -X POST $FGA_SERVER_URL/stores/$FGA_STORE_ID/write \
-H "Authorization: Bearer $FGA_API_TOKEN" \ # Not needed if service does not require authorization
-H "content-type: application/json" \
-d '{"writes": { "tuple_keys" : [{"user":"user:alice","relation":"member","object":"team:writers"}] }, "authorization_model_id": "1uHxCSuTP0VKPYSnkq1pbb1jeZw"}'
Set FGA_SERVER_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_SERVER_URL according to the service you are using (e.g. https://api.fga.example)
fga tuple write --store-id=${FGA_STORE_ID} --model-id=1uHxCSuTP0VKPYSnkq1pbb1jeZw user:alice member team:writers
write([
{
"user":"user:alice",
"relation":"member",
"object":"team:writers"
}
], authorization_model_id="1uHxCSuTP0VKPYSnkq1pbb1jeZw")
03. Assign The Team Members A Relation To An Object
To represent groups we use the type:object_id#relation
format, which represents the set of users related to the type:object_id
as a certain relation. For example, team:writers#members
is used to represent the set of users related to the team:writers object as member
s.
In order to assign member
s of a team
a relation to a document
, we can create the following relationship tuple that states that members of team:writers are editors of document:meeting_notes.doc.
- Node.js
- Go
- .NET
- Python
- curl
- CLI
- Pseudocode
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
// import the SDK
const { OpenFgaClient } = require('@openfga/sdk');
// Initialize the SDK with no auth - see "How to setup SDK client" for more options
const fgaClient = new OpenFgaClient({
apiScheme: process.env.FGA_API_SCHEME, // Either "http" or "https", defaults to "https"
apiHost: process.env.FGA_API_HOST, // required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
storeId: process.env.FGA_STORE_ID,
authorizationModelId: process.env.FGA_MODEL_ID, // Optional, can be overridden per request
});
await fgaClient.write({
writes: [
// Set of users related to 'team:writers' as 'member'
{ user: 'team:writers#member', relation: 'editor', object: 'document:meeting_notes.doc'}]
},
}, {
authorization_model_id: "1uHxCSuTP0VKPYSnkq1pbb1jeZw"
});
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import (
. "github.com/openfga/go-sdk/client"
"os"
)
func main() {
// Initialize the SDK with no auth - see "How to setup SDK client" for more options
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiScheme: os.Getenv("FGA_SCHEME"), // Either "http" or "https", defaults to "https"
ApiHost: os.Getenv("FGA_API_HOST"), // required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
StoreId: os.Getenv("FGA_STORE_ID"), // optional, not needed for `CreateStore` and `ListStores`, required before calling for all other methods
AuthorizationModelId: openfga.PtrString(os.Getenv("FGA_MODEL_ID")), // Optional, can be overridden per request
})
if err != nil {
// .. Handle error
}
}
options := ClientWriteOptions{
AuthorizationModelId: openfga.PtrString("1uHxCSuTP0VKPYSnkq1pbb1jeZw"),
}
body := fgaClient.ClientWriteRequest{
Writes: &[]ClientTupleKey{
{
// Set of users related to 'team:writers' as 'member'
User: openfga.PtrString("team:writers#member"),
Relation: openfga.PtrString("editor"),
Object: openfga.PtrString("document:meeting_notes.doc"),
}, } }
data, err := fgaClient.Write(context.Background()).Body(requestBody).Options(options).Execute()
if err != nil {
// .. Handle error
}
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
// import the SDK
using OpenFga.Sdk.Client;
using OpenFga.Sdk.Client.Model;
using OpenFga.Sdk.Model;
using Environment = System.Environment;
namespace Example;
class Example {
public static async Task Main() {
// Initialize the SDK with no auth - see "How to setup SDK client" for more options
var configuration = new ClientConfiguration() {
ApiScheme = Environment.GetEnvironmentVariable("FGA_API_SCHEME"), // Either "http" or "https", defaults to "https"
ApiHost = Environment.GetEnvironmentVariable("FGA_API_HOST"), // required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
StoreId = Environment.GetEnvironmentVariable("FGA_STORE_ID"), // optional, not needed for `CreateStore` and `ListStores`, required before calling for all other methods
AuthorizationModelId = Environment.GetEnvironmentVariable("FGA_MODEL_ID"), // Optional, can be overridden per request
};
var fgaClient = new OpenFgaClient(configuration);
}
}
var options = new ClientListObjectsOptions {
AuthorizationModelId = "1uHxCSuTP0VKPYSnkq1pbb1jeZw",
};
var body = new ClientWriteRequest() {
Writes = new List<ClientTupleKey>() {
// Set of users related to 'team:writers' as 'member'
new() { User = "team:writers#member", Relation = "editor", Object = "document:meeting_notes.doc" }
},
};
var response = await fgaClient.Write(body, options);
Initialize the SDK
# ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import os
import json
import openfga_sdk
from openfga_sdk.client import OpenFgaClient
configuration = openfga_sdk.ClientConfiguration(
api_scheme = os.environ.get('FGA_API_SCHEME'), # Either "http" or "https", defaults to "https"
api_host = os.environ.get('FGA_API_HOST'), # required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
store_id = os.environ.get('FGA_STORE_ID') # optional, not needed for `CreateStore` and `ListStores`, required before calling for all other methods
authorization_model_id = os.environ.get('FGA_MODEL_ID'), # Optional, can be overridden per request
)
# Enter a context with an instance of the OpenFgaClient
async with OpenFgaClient(configuration) as fga_client:
api_response = await fga_client.read_authorization_models()
await fga_client.close()
options = {
"authorization_model_id": "1uHxCSuTP0VKPYSnkq1pbb1jeZw"
}
body = body = ClientWriteRequest(
writes=[
ClientTuple(
# Set of users related to 'team:writers' as 'member'
user="team:writers#member",
relation="editor",
object="document:meeting_notes.doc",
),,
),
",
)
response = await fga_client.write(body, options)
Set FGA_SERVER_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_SERVER_URL according to the service you are using (e.g. https://api.fga.example)
curl -X POST $FGA_SERVER_URL/stores/$FGA_STORE_ID/write \
-H "Authorization: Bearer $FGA_API_TOKEN" \ # Not needed if service does not require authorization
-H "content-type: application/json" \
-d '{"writes": { "tuple_keys" : [{"user":"team:writers#member","relation":"editor","object":"document:meeting_notes.doc"}] }, "authorization_model_id": "1uHxCSuTP0VKPYSnkq1pbb1jeZw"}'
Set FGA_SERVER_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_SERVER_URL according to the service you are using (e.g. https://api.fga.example)
fga tuple write --store-id=${FGA_STORE_ID} --model-id=1uHxCSuTP0VKPYSnkq1pbb1jeZw team:writers#member editor document:meeting_notes.doc
write([
// Set of users related to 'team:writers' as 'member'
{
"user":"team:writers#member",
"relation":"editor",
"object":"document:meeting_notes.doc"
}
], authorization_model_id="1uHxCSuTP0VKPYSnkq1pbb1jeZw")
04. Checking An Individual Member's Access To An Object
Now that we have:
- a relationship tuple indicating that alice is an
member
of team:writers - a relationship tuple indicating that members of team:writers are editors of document:meeting_notes.doc
This means that if we *check*is alice an editor of document:meeting_notes.doc? We would get the following:
- Node.js
- Go
- .NET
- Python
- CLI
- curl
- Pseudocode
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
// import the SDK
const { OpenFgaClient } = require('@openfga/sdk');
// Initialize the SDK with no auth - see "How to setup SDK client" for more options
const fgaClient = new OpenFgaClient({
apiScheme: process.env.FGA_API_SCHEME, // Either "http" or "https", defaults to "https"
apiHost: process.env.FGA_API_HOST, // required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
storeId: process.env.FGA_STORE_ID,
authorizationModelId: process.env.FGA_MODEL_ID, // Optional, can be overridden per request
});
// Run a check
const { allowed } = await fgaClient.check({
user: 'user:alice',
relation: 'editor',
object: 'document:meeting_notes.doc',
}, {
authorization_model_id: '1uHxCSuTP0VKPYSnkq1pbb1jeZw',
});
// allowed = true
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import (
. "github.com/openfga/go-sdk/client"
"os"
)
func main() {
// Initialize the SDK with no auth - see "How to setup SDK client" for more options
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiScheme: os.Getenv("FGA_SCHEME"), // Either "http" or "https", defaults to "https"
ApiHost: os.Getenv("FGA_API_HOST"), // required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
StoreId: os.Getenv("FGA_STORE_ID"), // optional, not needed for `CreateStore` and `ListStores`, required before calling for all other methods
AuthorizationModelId: openfga.PtrString(os.Getenv("FGA_MODEL_ID")), // Optional, can be overridden per request
})
if err != nil {
// .. Handle error
}
}
options := ClientCheckOptions{
AuthorizationModelId: openfga.PtrString("1uHxCSuTP0VKPYSnkq1pbb1jeZw"),
}
body := ClientCheckRequest{
User: "user:alice",
Relation: "editor",
Object: "document:meeting_notes.doc",
}
data, err := fgaClient.Check(context.Background()).Body(body).Options(options).Execute()
// data = { allowed: true }
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
// import the SDK
using OpenFga.Sdk.Client;
using OpenFga.Sdk.Client.Model;
using OpenFga.Sdk.Model;
using Environment = System.Environment;
namespace Example;
class Example {
public static async Task Main() {
// Initialize the SDK with no auth - see "How to setup SDK client" for more options
var configuration = new ClientConfiguration() {
ApiScheme = Environment.GetEnvironmentVariable("FGA_API_SCHEME"), // Either "http" or "https", defaults to "https"
ApiHost = Environment.GetEnvironmentVariable("FGA_API_HOST"), // required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
StoreId = Environment.GetEnvironmentVariable("FGA_STORE_ID"), // optional, not needed for `CreateStore` and `ListStores`, required before calling for all other methods
AuthorizationModelId = Environment.GetEnvironmentVariable("FGA_MODEL_ID"), // Optional, can be overridden per request
};
var fgaClient = new OpenFgaClient(configuration);
}
}
var options = new ClientCheckOptions {
AuthorizationModelId = "1uHxCSuTP0VKPYSnkq1pbb1jeZw",
};
var body = new ClientCheckRequest {
User = "user:alice",
Relation = "editor",
Object = "document:meeting_notes.doc",
};
var response = await fgaClient.Check(body, options);
// response.Allowed = true
Initialize the SDK
# ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import os
import json
import openfga_sdk
from openfga_sdk.client import OpenFgaClient
configuration = openfga_sdk.ClientConfiguration(
api_scheme = os.environ.get('FGA_API_SCHEME'), # Either "http" or "https", defaults to "https"
api_host = os.environ.get('FGA_API_HOST'), # required, define without the scheme (e.g. api.fga.example instead of https://api.fga.example)
store_id = os.environ.get('FGA_STORE_ID') # optional, not needed for `CreateStore` and `ListStores`, required before calling for all other methods
authorization_model_id = os.environ.get('FGA_MODEL_ID'), # Optional, can be overridden per request
)
# Enter a context with an instance of the OpenFgaClient
async with OpenFgaClient(configuration) as fga_client:
api_response = await fga_client.read_authorization_models()
await fga_client.close()
options = {
"authorization_model_id": "1uHxCSuTP0VKPYSnkq1pbb1jeZw"
}
body = ClientCheckRequest(
user="user:alice",
relation="editor",
object="document:meeting_notes.doc",
)
response = await fga_client.check(body, options)
# response.allowed = true
Set FGA_SERVER_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_SERVER_URL according to the service you are using (e.g. https://api.fga.example)
fga query check --store-id=$FGA_STORE_ID --model-id=1uHxCSuTP0VKPYSnkq1pbb1jeZw user:alice editor document:meeting_notes.doc
# Response: {"allowed":true}
Set FGA_SERVER_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_SERVER_URL according to the service you are using (e.g. https://api.fga.example)
curl -X POST $FGA_SERVER_URL/stores/$FGA_STORE_ID/check \
-H "Authorization: Bearer $FGA_API_TOKEN" \ # Not needed if service does not require authorization
-H "content-type: application/json" \
-d '{"authorization_model_id": "1uHxCSuTP0VKPYSnkq1pbb1jeZw", "tuple_key":{"user":"user:alice","relation":"editor","object":"document:meeting_notes.doc"}}'
# Response: {"allowed":true}
check(
user = "user:alice", // check if the user `user:alice`
relation = "editor", // has an `editor` relation
object = "document:meeting_notes.doc", // with the object `document:meeting_notes.doc`
authorization_id = "1uHxCSuTP0VKPYSnkq1pbb1jeZw"
);
Reply: true
The chain of resolution becomes:
- alice is
member
of team:writers member
s of team:writers areeditor
s of document:meeting_notes- therefore, alice is
editor
of document:meeting_notes
Note: When creating relationship tuples for OpenFGA make sure to use unique ids for each object and user within your application domain. We're using first names and simple ids to just illustrate an easy-to-follow example.