Modeling Authorization for Slack with OpenFGA
This tutorial explains how to model permissions for a communication platform like Slack using OpenFGA.
- How to indicate relationships between a group of users and an object.
Used here to indicate that all members of a slack workspace can write in a certain channel.
See Modeling User Groups for more. - How to Model concentric relationship to have a certain relation on an object imply another relation on the same object.
Used here to indicate that legacy admins have all the permissions of the more granular channels admin.
See Modeling Concentric Relationships for more. - How to use the union operator condition to indicate that a user might have a certain relation with an object if they match any of the criteria indicated.
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.
OpenFGA concepts
It would be helpful to have an understanding of some concepts of OpenFGA before you start.
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 →
Modeling concentric relationships
You need to know how to update the authorization model to allow having nested relations such as all writers are readers. Learn more →
Concepts & configuration language
What you will be modeling
Slack is a messaging app for businesses that connects people to the information they need. By bringing people together to work as one unified team, Slack transforms the way organizations communicate. (Source: What is Slack?)
In this tutorial, you will build a subset of the Slack permission model (detailed below) in OpenFGA, using some scenarios to validate the model.
As reference, you can refer to Slack's publicly available docs:
Note: For brevity, this tutorial will not model all of Slack's permissions. Instead, it will focus on modeling the scenarios outlined below.
Requirements
This tutorial will focus on the following sections (this is a partial list of Slack's roles):
Workspace Roles:
- Guest: This type of user is limited in their ability to use Slack, and is only permitted to see one or multiple delegated channels.
- Member: This is the base type of user that does not have any particular administrative abilities, but has basic access to the organization's Slack workspaces. When an administrative change needs to be made, these users need the support of admins and owners to make the changes.
- Legacy Admin: This type of user is the basic administrator of any organization, and can make a wide variety of administrative changes across Slack, such as renaming channels, archiving channels, setting up preferences and policies, inviting new users, and installing applications. Users with this role perform the majority of administrative tasks across a team.
System Roles:
- Channels Admin: This type of user has the permission to archive channels, rename channels, create private channels, and convert public channels into private channels.
Channel Settings:
- Visibility:
- Public: Visible to all members and open to join
- Private: Visible to admins and invited members
- Posting Permissions:
- Open: Anyone can post
- Limited: Only allowed members can post
Defined scenarios
Use the following scenarios to be able to validate whether the model of the requirements is correct.
There will be the following users:
- Amy
- Bob
- Catherine
- David
- Emily
These users will interact in the following scenarios:
- You will assume there is a Slack workspace called Sandcastle
- Amy is a legacy admin of the Sandcastle workspace
- Bob is a member of the Sandcastle workspace with a channels admin role (Read more about system roles at Slack here)
- Catherine and Emily are normal members of the Sandcastle workspace, they can view all public channels, as well as channels they have been invited to
- David is a guest user with only view and write access to #proj-marketing-campaign, one of the public channels in the Sandcastle workspace
- Bob and Emily are in a private channel #marketing-internal in the Sandcastle workspace which only they can view and post to
- All members of the Sandcastle workspace can view the general channel, but only Amy and Emily can post to it
In production, it is highly recommended to use unique, immutable identifiers. Names are used in this article to make it easier to read and follow.
Modeling workspaces & channels
The goal by the end of this post is to ask OpenFGA: Does person X have permission to perform action Y on channel Z? In response, you want to either get a confirmation that person X can indeed do that, or a rejection that they cannot. E.g. does David have access to view #general?
The OpenFGA is based on Zanzibar, a Relation Based Access Control system. This means it relies on objects and user relations to perform authorization checks.
Setting aside the permissions, you will start with the roles and learn how to express the requirements in terms of relations you can feed into OpenFGA.
The requirements stated:
- Amy is a legacy admin of the Sandcastle workspace
- Bob is a channels admin of the Sandcastle workspace
- Catherine and Emily are a normal members of the Sandcastle workspace
- David is a guest user
Here is how you would express than in OpenFGA's authorization model: You have a type called "workspace", and users can be related to it as a legacy_admin, channels_admin, member and guest
- DSL
- JSON
model
schema 1.1
type user
type workspace
relations
define legacy_admin: [user]
define channels_admin: [user]
define member: [user]
define guest: [user]
{
"schema_version": "1.1",
"type_definitions": [
{
"type": "user"
},
{
"type": "workspace",
"relations": {
"legacy_admin": {
"this": {}
},
"channels_admin": {
"this": {}
},
"member": {
"this": {}
},
"guest": {
"this": {}
}
},
"metadata": {
"relations": {
"legacy_admin": {
"directly_related_user_types": [
{
"type": "user"
}
]
},
"channels_admin": {
"directly_related_user_types": [
{
"type": "user"
}
]
},
"member": {
"directly_related_user_types": [
{
"type": "user"
}
]
},
"guest": {
"directly_related_user_types": [
{
"type": "user"
}
]
}
}
}
}
]
}
Objects of type workspace
have users related to them as:
- Legacy Admin (
legacy_admin
) - Channels Admin (
channels_admin
) - Member (
member
) - Guest (
guest
)
Direct relationship type restrictions indicate that a user can have a direct relationship with an object of the type the relation specifies.
01. Individual permissions
To keep things simple and focus on OpenFGA rather than Slack complexity, we will model only four roles (legacy_admin, channels_admin, member, guest).
At the end of this section we want to have the following permissions represented
User | Relation | Object |
---|---|---|
amy | legacy_admin | workspace:sandcastle |
bob | channels_admin | workspace:sandcastle |
catherine | member | workspace:sandcastle |
david | guest | workspace:sandcastle |
emily | member | workspace:sandcastle |
To represent permissions in OpenFGA we use relations. For workspace permissions we need to create the following authorization model:
- DSL
- JSON
model
schema 1.1
type user
type workspace
relations
define legacy_admin: [user]
define channels_admin: [user]
define member: [user]
define guest: [user]
{
"schema_version": "1.1",
"type_definitions": [
{
"type": "user"
},
{
"type": "workspace",
"relations": {
"legacy_admin": {
"this": {}
},
"channels_admin": {
"this": {}
},
"member": {
"this": {}
},
"guest": {
"this": {}
}
},
"metadata": {
"relations": {
"legacy_admin": {
"directly_related_user_types": [
{
"type": "user"
}
]
},
"channels_admin": {
"directly_related_user_types": [
{
"type": "user"
}
]
},
"member": {
"directly_related_user_types": [
{
"type": "user"
}
]
},
"guest": {
"directly_related_user_types": [
{
"type": "user"
}
]
}
}
}
}
]
}
The OpenFGA service determines if a user has access to an object by checking if the user has a relation to that object. Let us examine one of those relations in detail:
- DSL
- JSON
type workspace
relations
define member: [user]
{
"type": "workspace",
"relations": {
"member": {
"this": {}
}
},
"metadata": {
"relations": {
"member": {
"directly_related_user_types": [
{
"type": "user"
}
]
}
}
}
}
The snippet above indicates that objects of type workspace have users related to them as "member" if those users belong to the userset of all users related to the workspace as "member".
This means that a user can be directly related as a member to an object of type "workspace"
If we want to say amy
is a legacy_admin
of workspace:sandcastle
we create this relationship tuple
- Node.js
- Go
- .NET
- Python
- Java
- 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({
apiUrl: process.env.FGA_API_URL, // required, e.g. 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: [
// Amy is a Legacy Admin in the Sandcastle workspace
{"_description":"Amy is a Legacy Admin in the Sandcastle workspace","user":"user:amy","relation":"legacy_admin","object":"workspace:sandcastle"}
],
}, {
authorization_model_id: "01HVMMBCMGZNT3SED4Z17ECXCA"
});
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import (
"os"
. "github.com/openfga/go-sdk"
. "github.com/openfga/go-sdk/client"
)
func main() {
// Initialize the SDK with no auth - see "How to setup SDK client" for more options
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiUrl: os.Getenv("FGA_API_URL"), // required, e.g. 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: os.Getenv("FGA_MODEL_ID"), // Optional, can be overridden per request
})
if err != nil {
// .. Handle error
}
}
options := ClientWriteOptions{
AuthorizationModelId: PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
}
body := ClientWriteRequest{
Writes: []ClientTupleKey{
{
// Amy is a Legacy Admin in the Sandcastle workspace
User: "user:amy",
Relation: "legacy_admin",
Object: "workspace:sandcastle",
},
},
}
data, err := fgaClient.Write(context.Background()).
Body(body).
Options(options).
Execute()
if err != nil {
// .. Handle error
}
_ = data // use the response
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() {
ApiUrl = Environment.GetEnvironmentVariable("FGA_API_URL"), ?? "http://localhost:8080", // required, e.g. 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 ClientWriteOptions {
AuthorizationModelId = "01HVMMBCMGZNT3SED4Z17ECXCA",
};
var body = new ClientWriteRequest() {
Writes = new List<ClientTupleKey>() {
// Amy is a Legacy Admin in the Sandcastle workspace
new() {
User = "user:amy",
Relation = "legacy_admin",
Object = "workspace:sandcastle"
}
},
};
var response = await fgaClient.Write(body, options);
Initialize the SDK
# ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import asyncio
import os
import json
from openfga_sdk.client import ClientConfiguration, OpenFgaClient
async def main():
configuration = ClientConfiguration(
api_url = os.environ.get('FGA_API_URL'), # required, e.g. 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()
asyncio.run(main())
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}
body = ClientWriteRequest(
writes=[
ClientTuple(
# Amy is a Legacy Admin in the Sandcastle workspace
user="user:amy",
relation="legacy_admin",
object="workspace:sandcastle",
),
],
)
response = await fga_client.write(body, options)
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.configuration.ClientConfiguration;
public class Example {
public static void main(String[] args) throws Exception {
var config = new ClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "https://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_AUTHORIZATION_MODEL_ID")); // Optional, can be overridden per request
var fgaClient = new OpenFgaClient(config);
}
}
var options = new ClientWriteOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var body = new ClientWriteRequest()
.writes(List.of(
// Amy is a Legacy Admin in the Sandcastle workspace
new ClientTupleKey()
.user("user:amy")
.relation("legacy_admin")
._object("workspace:sandcastle")
));
var response = fgaClient.write(body, options).get();
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
curl -X POST $FGA_API_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" : [{"_description":"Amy is a Legacy Admin in the Sandcastle workspace","user":"user:amy","relation":"legacy_admin","object":"workspace:sandcastle"}] }, "authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"}'
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
fga tuple write --store-id=${FGA_STORE_ID} --model-id=01HVMMBCMGZNT3SED4Z17ECXCA user:amy legacy_admin workspace:sandcastle
write([
// Amy is a Legacy Admin in the Sandcastle workspace
{
"user":"user:amy",
"relation":"legacy_admin",
"object":"workspace:sandcastle"
}
], authorization_model_id="01HVMMBCMGZNT3SED4Z17ECXCA")
We can now ask OpenFGA "is amy
a legacy_admin of workspace:sandcastle?"
- Node.js
- Go
- .NET
- Python
- Java
- 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({
apiUrl: process.env.FGA_API_URL, // required, e.g. 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:amy',
relation: 'legacy_admin',
object: 'workspace:sandcastle',
}, {
authorization_model_id: '01HVMMBCMGZNT3SED4Z17ECXCA',
});
// allowed = true
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import (
"os"
. "github.com/openfga/go-sdk"
. "github.com/openfga/go-sdk/client"
)
func main() {
// Initialize the SDK with no auth - see "How to setup SDK client" for more options
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiUrl: os.Getenv("FGA_API_URL"), // required, e.g. 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: os.Getenv("FGA_MODEL_ID"), // Optional, can be overridden per request
})
if err != nil {
// .. Handle error
}
}
options := ClientCheckOptions{
AuthorizationModelId: PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
}
body := ClientCheckRequest{
User: "user:amy",
Relation: "legacy_admin",
Object: "workspace:sandcastle",
}
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() {
ApiUrl = Environment.GetEnvironmentVariable("FGA_API_URL"), ?? "http://localhost:8080", // required, e.g. 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 = "01HVMMBCMGZNT3SED4Z17ECXCA",
};
var body = new ClientCheckRequest {
User = "user:amy",
Relation = "legacy_admin",
Object = "workspace:sandcastle",
};
var response = await fgaClient.Check(body, options);
// response.Allowed = true
Initialize the SDK
# ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import asyncio
import os
import json
from openfga_sdk.client import ClientConfiguration, OpenFgaClient
async def main():
configuration = ClientConfiguration(
api_url = os.environ.get('FGA_API_URL'), # required, e.g. 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()
asyncio.run(main())
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}
body = ClientCheckRequest(
user="user:amy",
relation="legacy_admin",
object="workspace:sandcastle",
)
response = await fga_client.check(body, options)
# response.allowed = true
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.configuration.ClientConfiguration;
public class Example {
public static void main(String[] args) throws Exception {
var config = new ClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "https://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_AUTHORIZATION_MODEL_ID")); // Optional, can be overridden per request
var fgaClient = new OpenFgaClient(config);
}
}
var options = new ClientCheckOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var body = new ClientCheckRequest()
.user("user:amy")
.relation("legacy_admin")
._object("workspace:sandcastle");
var response = fgaClient.check(body, options).get();
// response.getAllowed() = true
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
fga query check --store-id=$FGA_STORE_ID --model-id=01HVMMBCMGZNT3SED4Z17ECXCA user:amy legacy_admin workspace:sandcastle
# Response: {"allowed":true}
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
curl -X POST $FGA_API_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": "01HVMMBCMGZNT3SED4Z17ECXCA", "tuple_key":{"user":"user:amy","relation":"legacy_admin","object":"workspace:sandcastle"}}'
# Response: {"allowed":true}
check(
user = "user:amy", // check if the user `user:amy`
relation = "legacy_admin", // has an `legacy_admin` relation
object = "workspace:sandcastle", // with the object `workspace:sandcastle`
authorization_id = "01HVMMBCMGZNT3SED4Z17ECXCA"
);
Reply: true
We can also say that catherine
is a member
of workspace:sandcastle
:
- Node.js
- Go
- .NET
- Python
- Java
- 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({
apiUrl: process.env.FGA_API_URL, // required, e.g. 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: [
// Catherine is a Member in the Sandcastle workspace
{"_description":"Catherine is a Member in the Sandcastle workspace","user":"user:catherine","relation":"member","object":"workspace:sandcastle"}
],
}, {
authorization_model_id: "01HVMMBCMGZNT3SED4Z17ECXCA"
});
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import (
"os"
. "github.com/openfga/go-sdk"
. "github.com/openfga/go-sdk/client"
)
func main() {
// Initialize the SDK with no auth - see "How to setup SDK client" for more options
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiUrl: os.Getenv("FGA_API_URL"), // required, e.g. 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: os.Getenv("FGA_MODEL_ID"), // Optional, can be overridden per request
})
if err != nil {
// .. Handle error
}
}
options := ClientWriteOptions{
AuthorizationModelId: PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
}
body := ClientWriteRequest{
Writes: []ClientTupleKey{
{
// Catherine is a Member in the Sandcastle workspace
User: "user:catherine",
Relation: "member",
Object: "workspace:sandcastle",
},
},
}
data, err := fgaClient.Write(context.Background()).
Body(body).
Options(options).
Execute()
if err != nil {
// .. Handle error
}
_ = data // use the response
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() {
ApiUrl = Environment.GetEnvironmentVariable("FGA_API_URL"), ?? "http://localhost:8080", // required, e.g. 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 ClientWriteOptions {
AuthorizationModelId = "01HVMMBCMGZNT3SED4Z17ECXCA",
};
var body = new ClientWriteRequest() {
Writes = new List<ClientTupleKey>() {
// Catherine is a Member in the Sandcastle workspace
new() {
User = "user:catherine",
Relation = "member",
Object = "workspace:sandcastle"
}
},
};
var response = await fgaClient.Write(body, options);
Initialize the SDK
# ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import asyncio
import os
import json
from openfga_sdk.client import ClientConfiguration, OpenFgaClient
async def main():
configuration = ClientConfiguration(
api_url = os.environ.get('FGA_API_URL'), # required, e.g. 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()
asyncio.run(main())
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}
body = ClientWriteRequest(
writes=[
ClientTuple(
# Catherine is a Member in the Sandcastle workspace
user="user:catherine",
relation="member",
object="workspace:sandcastle",
),
],
)
response = await fga_client.write(body, options)
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.configuration.ClientConfiguration;
public class Example {
public static void main(String[] args) throws Exception {
var config = new ClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "https://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_AUTHORIZATION_MODEL_ID")); // Optional, can be overridden per request
var fgaClient = new OpenFgaClient(config);
}
}
var options = new ClientWriteOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var body = new ClientWriteRequest()
.writes(List.of(
// Catherine is a Member in the Sandcastle workspace
new ClientTupleKey()
.user("user:catherine")
.relation("member")
._object("workspace:sandcastle")
));
var response = fgaClient.write(body, options).get();
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
curl -X POST $FGA_API_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" : [{"_description":"Catherine is a Member in the Sandcastle workspace","user":"user:catherine","relation":"member","object":"workspace:sandcastle"}] }, "authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"}'
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
fga tuple write --store-id=${FGA_STORE_ID} --model-id=01HVMMBCMGZNT3SED4Z17ECXCA user:catherine member workspace:sandcastle
write([
// Catherine is a Member in the Sandcastle workspace
{
"user":"user:catherine",
"relation":"member",
"object":"workspace:sandcastle"
}
], authorization_model_id="01HVMMBCMGZNT3SED4Z17ECXCA")
And verify by asking OpenFGA
- Node.js
- Go
- .NET
- Python
- Java
- 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({
apiUrl: process.env.FGA_API_URL, // required, e.g. 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:catherine',
relation: 'member',
object: 'workspace:sandcastle',
}, {
authorization_model_id: '01HVMMBCMGZNT3SED4Z17ECXCA',
});
// allowed = true
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import (
"os"
. "github.com/openfga/go-sdk"
. "github.com/openfga/go-sdk/client"
)
func main() {
// Initialize the SDK with no auth - see "How to setup SDK client" for more options
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiUrl: os.Getenv("FGA_API_URL"), // required, e.g. 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: os.Getenv("FGA_MODEL_ID"), // Optional, can be overridden per request
})
if err != nil {
// .. Handle error
}
}
options := ClientCheckOptions{
AuthorizationModelId: PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
}
body := ClientCheckRequest{
User: "user:catherine",
Relation: "member",
Object: "workspace:sandcastle",
}
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() {
ApiUrl = Environment.GetEnvironmentVariable("FGA_API_URL"), ?? "http://localhost:8080", // required, e.g. 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 = "01HVMMBCMGZNT3SED4Z17ECXCA",
};
var body = new ClientCheckRequest {
User = "user:catherine",
Relation = "member",
Object = "workspace:sandcastle",
};
var response = await fgaClient.Check(body, options);
// response.Allowed = true
Initialize the SDK
# ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import asyncio
import os
import json
from openfga_sdk.client import ClientConfiguration, OpenFgaClient
async def main():
configuration = ClientConfiguration(
api_url = os.environ.get('FGA_API_URL'), # required, e.g. 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()
asyncio.run(main())
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}
body = ClientCheckRequest(
user="user:catherine",
relation="member",
object="workspace:sandcastle",
)
response = await fga_client.check(body, options)
# response.allowed = true
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.configuration.ClientConfiguration;
public class Example {
public static void main(String[] args) throws Exception {
var config = new ClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "https://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_AUTHORIZATION_MODEL_ID")); // Optional, can be overridden per request
var fgaClient = new OpenFgaClient(config);
}
}
var options = new ClientCheckOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var body = new ClientCheckRequest()
.user("user:catherine")
.relation("member")
._object("workspace:sandcastle");
var response = fgaClient.check(body, options).get();
// response.getAllowed() = true
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
fga query check --store-id=$FGA_STORE_ID --model-id=01HVMMBCMGZNT3SED4Z17ECXCA user:catherine member workspace:sandcastle
# Response: {"allowed":true}
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
curl -X POST $FGA_API_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": "01HVMMBCMGZNT3SED4Z17ECXCA", "tuple_key":{"user":"user:catherine","relation":"member","object":"workspace:sandcastle"}}'
# Response: {"allowed":true}
check(
user = "user:catherine", // check if the user `user:catherine`
relation = "member", // has an `member` relation
object = "workspace:sandcastle", // with the object `workspace:sandcastle`
authorization_id = "01HVMMBCMGZNT3SED4Z17ECXCA"
);
Reply: true
Catherine, on the other hand, is not a legacy_admin of workspace:sandcastle.
- Node.js
- Go
- .NET
- Python
- Java
- 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({
apiUrl: process.env.FGA_API_URL, // required, e.g. 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:catherine',
relation: 'legacy_admin',
object: 'workspace:sandcastle',
}, {
authorization_model_id: '01HVMMBCMGZNT3SED4Z17ECXCA',
});
// allowed = false
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import (
"os"
. "github.com/openfga/go-sdk"
. "github.com/openfga/go-sdk/client"
)
func main() {
// Initialize the SDK with no auth - see "How to setup SDK client" for more options
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiUrl: os.Getenv("FGA_API_URL"), // required, e.g. 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: os.Getenv("FGA_MODEL_ID"), // Optional, can be overridden per request
})
if err != nil {
// .. Handle error
}
}
options := ClientCheckOptions{
AuthorizationModelId: PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
}
body := ClientCheckRequest{
User: "user:catherine",
Relation: "legacy_admin",
Object: "workspace:sandcastle",
}
data, err := fgaClient.Check(context.Background()).
Body(body).
Options(options).
Execute()
// data = { allowed: false }
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() {
ApiUrl = Environment.GetEnvironmentVariable("FGA_API_URL"), ?? "http://localhost:8080", // required, e.g. 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 = "01HVMMBCMGZNT3SED4Z17ECXCA",
};
var body = new ClientCheckRequest {
User = "user:catherine",
Relation = "legacy_admin",
Object = "workspace:sandcastle",
};
var response = await fgaClient.Check(body, options);
// response.Allowed = false
Initialize the SDK
# ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import asyncio
import os
import json
from openfga_sdk.client import ClientConfiguration, OpenFgaClient
async def main():
configuration = ClientConfiguration(
api_url = os.environ.get('FGA_API_URL'), # required, e.g. 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()
asyncio.run(main())
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}
body = ClientCheckRequest(
user="user:catherine",
relation="legacy_admin",
object="workspace:sandcastle",
)
response = await fga_client.check(body, options)
# response.allowed = false
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.configuration.ClientConfiguration;
public class Example {
public static void main(String[] args) throws Exception {
var config = new ClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "https://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_AUTHORIZATION_MODEL_ID")); // Optional, can be overridden per request
var fgaClient = new OpenFgaClient(config);
}
}
var options = new ClientCheckOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var body = new ClientCheckRequest()
.user("user:catherine")
.relation("legacy_admin")
._object("workspace:sandcastle");
var response = fgaClient.check(body, options).get();
// response.getAllowed() = true
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
fga query check --store-id=$FGA_STORE_ID --model-id=01HVMMBCMGZNT3SED4Z17ECXCA user:catherine legacy_admin workspace:sandcastle
# Response: {"allowed":false}
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
curl -X POST $FGA_API_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": "01HVMMBCMGZNT3SED4Z17ECXCA", "tuple_key":{"user":"user:catherine","relation":"legacy_admin","object":"workspace:sandcastle"}}'
# Response: {"allowed":false}
check(
user = "user:catherine", // check if the user `user:catherine`
relation = "legacy_admin", // has an `legacy_admin` relation
object = "workspace:sandcastle", // with the object `workspace:sandcastle`
authorization_id = "01HVMMBCMGZNT3SED4Z17ECXCA"
);
Reply: false
Repeat this process for the other relationships
[
{
// Bob is a Channels Admin in the Sandcastle workspace
user: 'user:bob',
relation: 'channels_admin',
object: 'workspace:sandcastle',
},
{
// David is a guest in the Sandcastle workspace
user: 'user:david',
relation: 'guest',
object: 'workspace:sandcastle',
},
{
// Emily is a Member in the Sandcastle workspace
user: 'user:emily',
relation: 'member',
object: 'workspace:sandcastle',
},
]
- Node.js
- Go
- .NET
- Python
- Java
- 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({
apiUrl: process.env.FGA_API_URL, // required, e.g. 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: [
// Bob is a Channels Admin in the Sandcastle workspace
{"_description":"Bob is a Channels Admin in the Sandcastle workspace","user":"user:bob","relation":"channels_admin","object":"workspace:sandcastle"},
// David is a guest in the Sandcastle workspace
{"_description":"David is a guest in the Sandcastle workspace","user":"user:david","relation":"guest","object":"workspace:sandcastle"},
// Emily is a Member in the Sandcastle workspace
{"_description":"Emily is a Member in the Sandcastle workspace","user":"user:emily","relation":"member","object":"workspace:sandcastle"}
],
}, {
authorization_model_id: "01HVMMBCMGZNT3SED4Z17ECXCA"
});
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import (
"os"
. "github.com/openfga/go-sdk"
. "github.com/openfga/go-sdk/client"
)
func main() {
// Initialize the SDK with no auth - see "How to setup SDK client" for more options
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiUrl: os.Getenv("FGA_API_URL"), // required, e.g. 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: os.Getenv("FGA_MODEL_ID"), // Optional, can be overridden per request
})
if err != nil {
// .. Handle error
}
}
options := ClientWriteOptions{
AuthorizationModelId: PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
}
body := ClientWriteRequest{
Writes: []ClientTupleKey{
{
// Bob is a Channels Admin in the Sandcastle workspace
User: "user:bob",
Relation: "channels_admin",
Object: "workspace:sandcastle",
}, {
// David is a guest in the Sandcastle workspace
User: "user:david",
Relation: "guest",
Object: "workspace:sandcastle",
}, {
// Emily is a Member in the Sandcastle workspace
User: "user:emily",
Relation: "member",
Object: "workspace:sandcastle",
},
},
}
data, err := fgaClient.Write(context.Background()).
Body(body).
Options(options).
Execute()
if err != nil {
// .. Handle error
}
_ = data // use the response
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() {
ApiUrl = Environment.GetEnvironmentVariable("FGA_API_URL"), ?? "http://localhost:8080", // required, e.g. 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 ClientWriteOptions {
AuthorizationModelId = "01HVMMBCMGZNT3SED4Z17ECXCA",
};
var body = new ClientWriteRequest() {
Writes = new List<ClientTupleKey>() {
// Bob is a Channels Admin in the Sandcastle workspace
new() {
User = "user:bob",
Relation = "channels_admin",
Object = "workspace:sandcastle"
},
// David is a guest in the Sandcastle workspace
new() {
User = "user:david",
Relation = "guest",
Object = "workspace:sandcastle"
},
// Emily is a Member in the Sandcastle workspace
new() {
User = "user:emily",
Relation = "member",
Object = "workspace:sandcastle"
}
},
};
var response = await fgaClient.Write(body, options);
Initialize the SDK
# ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import asyncio
import os
import json
from openfga_sdk.client import ClientConfiguration, OpenFgaClient
async def main():
configuration = ClientConfiguration(
api_url = os.environ.get('FGA_API_URL'), # required, e.g. 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()
asyncio.run(main())
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}
body = ClientWriteRequest(
writes=[
ClientTuple(
# Bob is a Channels Admin in the Sandcastle workspace
user="user:bob",
relation="channels_admin",
object="workspace:sandcastle",
),
ClientTuple(
# David is a guest in the Sandcastle workspace
user="user:david",
relation="guest",
object="workspace:sandcastle",
),
ClientTuple(
# Emily is a Member in the Sandcastle workspace
user="user:emily",
relation="member",
object="workspace:sandcastle",
),
],
)
response = await fga_client.write(body, options)
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.configuration.ClientConfiguration;
public class Example {
public static void main(String[] args) throws Exception {
var config = new ClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "https://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_AUTHORIZATION_MODEL_ID")); // Optional, can be overridden per request
var fgaClient = new OpenFgaClient(config);
}
}
var options = new ClientWriteOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var body = new ClientWriteRequest()
.writes(List.of(
// Bob is a Channels Admin in the Sandcastle workspace
new ClientTupleKey()
.user("user:bob")
.relation("channels_admin")
._object("workspace:sandcastle"),
// David is a guest in the Sandcastle workspace
new ClientTupleKey()
.user("user:david")
.relation("guest")
._object("workspace:sandcastle"),
// Emily is a Member in the Sandcastle workspace
new ClientTupleKey()
.user("user:emily")
.relation("member")
._object("workspace:sandcastle")
));
var response = fgaClient.write(body, options).get();
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
curl -X POST $FGA_API_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" : [{"_description":"Bob is a Channels Admin in the Sandcastle workspace","user":"user:bob","relation":"channels_admin","object":"workspace:sandcastle"},{"_description":"David is a guest in the Sandcastle workspace","user":"user:david","relation":"guest","object":"workspace:sandcastle"},{"_description":"Emily is a Member in the Sandcastle workspace","user":"user:emily","relation":"member","object":"workspace:sandcastle"}] }, "authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"}'
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
fga tuple write --store-id=${FGA_STORE_ID} --model-id=01HVMMBCMGZNT3SED4Z17ECXCA user:bob channels_admin workspace:sandcastle
fga tuple write --store-id=${FGA_STORE_ID} --model-id=01HVMMBCMGZNT3SED4Z17ECXCA user:david guest workspace:sandcastle
fga tuple write --store-id=${FGA_STORE_ID} --model-id=01HVMMBCMGZNT3SED4Z17ECXCA user:emily member workspace:sandcastle
write([
// Bob is a Channels Admin in the Sandcastle workspace
{
"user":"user:bob",
"relation":"channels_admin",
"object":"workspace:sandcastle"
},
// David is a guest in the Sandcastle workspace
{
"user":"user:david",
"relation":"guest",
"object":"workspace:sandcastle"
},
// Emily is a Member in the Sandcastle workspace
{
"user":"user:emily",
"relation":"member",
"object":"workspace:sandcastle"
}
], authorization_model_id="01HVMMBCMGZNT3SED4Z17ECXCA")
Verification
To verify, we can issue check request to verify it is working as expected.
- Node.js
- Go
- .NET
- Python
- Java
- 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({
apiUrl: process.env.FGA_API_URL, // required, e.g. 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:amy',
relation: 'legacy_admin',
object: 'workspace:sandcastle',
}, {
authorization_model_id: '01HVMMBCMGZNT3SED4Z17ECXCA',
});
// allowed = true
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import (
"os"
. "github.com/openfga/go-sdk"
. "github.com/openfga/go-sdk/client"
)
func main() {
// Initialize the SDK with no auth - see "How to setup SDK client" for more options
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiUrl: os.Getenv("FGA_API_URL"), // required, e.g. 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: os.Getenv("FGA_MODEL_ID"), // Optional, can be overridden per request
})
if err != nil {
// .. Handle error
}
}
options := ClientCheckOptions{
AuthorizationModelId: PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
}
body := ClientCheckRequest{
User: "user:amy",
Relation: "legacy_admin",
Object: "workspace:sandcastle",
}
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() {
ApiUrl = Environment.GetEnvironmentVariable("FGA_API_URL"), ?? "http://localhost:8080", // required, e.g. 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 = "01HVMMBCMGZNT3SED4Z17ECXCA",
};
var body = new ClientCheckRequest {
User = "user:amy",
Relation = "legacy_admin",
Object = "workspace:sandcastle",
};
var response = await fgaClient.Check(body, options);
// response.Allowed = true
Initialize the SDK
# ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import asyncio
import os
import json
from openfga_sdk.client import ClientConfiguration, OpenFgaClient
async def main():
configuration = ClientConfiguration(
api_url = os.environ.get('FGA_API_URL'), # required, e.g. 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()
asyncio.run(main())
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}
body = ClientCheckRequest(
user="user:amy",
relation="legacy_admin",
object="workspace:sandcastle",
)
response = await fga_client.check(body, options)
# response.allowed = true
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.configuration.ClientConfiguration;
public class Example {
public static void main(String[] args) throws Exception {
var config = new ClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "https://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_AUTHORIZATION_MODEL_ID")); // Optional, can be overridden per request
var fgaClient = new OpenFgaClient(config);
}
}
var options = new ClientCheckOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var body = new ClientCheckRequest()
.user("user:amy")
.relation("legacy_admin")
._object("workspace:sandcastle");
var response = fgaClient.check(body, options).get();
// response.getAllowed() = true
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
fga query check --store-id=$FGA_STORE_ID --model-id=01HVMMBCMGZNT3SED4Z17ECXCA user:amy legacy_admin workspace:sandcastle
# Response: {"allowed":true}
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
curl -X POST $FGA_API_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": "01HVMMBCMGZNT3SED4Z17ECXCA", "tuple_key":{"user":"user:amy","relation":"legacy_admin","object":"workspace:sandcastle"}}'
# Response: {"allowed":true}
check(
user = "user:amy", // check if the user `user:amy`
relation = "legacy_admin", // has an `legacy_admin` relation
object = "workspace:sandcastle", // with the object `workspace:sandcastle`
authorization_id = "01HVMMBCMGZNT3SED4Z17ECXCA"
);
Reply: true
Let's try to verify the followings:
User | Object | Relation | Query | Relation? |
---|---|---|---|---|
amy | workspace:sandcastle | legacy_admin | is amy related to workspace:sandcastle as legacy_admin? | Yes |
david | workspace:sandcastle | legacy_admin | is david related to workspace:sandcastle as legacy_admin? | No |
amy | workspace:sandcastle | guest | is amy related to workspace:sandcastle as guest? | No |
david | workspace:sandcastle | guest | is david related to workspace:sandcastle as guest? | Yes |
amy | workspace:sandcastle | member | is amy related to workspace:sandcastle as member? | No |
david | workspace:sandcastle | member | is david related to workspace:sandcastle as member? | No |
02. Updating The workspace
Authorization Model With Implied Relations
Some of the queries that you ran earlier, while returning the correct response, do not match reality. One of which is:
- Node.js
- Go
- .NET
- Python
- Java
- 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({
apiUrl: process.env.FGA_API_URL, // required, e.g. 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:amy',
relation: 'member',
object: 'workspace:sandcastle',
}, {
authorization_model_id: '01HVMMBCMGZNT3SED4Z17ECXCA',
});
// allowed = false
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import (
"os"
. "github.com/openfga/go-sdk"
. "github.com/openfga/go-sdk/client"
)
func main() {
// Initialize the SDK with no auth - see "How to setup SDK client" for more options
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiUrl: os.Getenv("FGA_API_URL"), // required, e.g. 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: os.Getenv("FGA_MODEL_ID"), // Optional, can be overridden per request
})
if err != nil {
// .. Handle error
}
}
options := ClientCheckOptions{
AuthorizationModelId: PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
}
body := ClientCheckRequest{
User: "user:amy",
Relation: "member",
Object: "workspace:sandcastle",
}
data, err := fgaClient.Check(context.Background()).
Body(body).
Options(options).
Execute()
// data = { allowed: false }
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() {
ApiUrl = Environment.GetEnvironmentVariable("FGA_API_URL"), ?? "http://localhost:8080", // required, e.g. 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 = "01HVMMBCMGZNT3SED4Z17ECXCA",
};
var body = new ClientCheckRequest {
User = "user:amy",
Relation = "member",
Object = "workspace:sandcastle",
};
var response = await fgaClient.Check(body, options);
// response.Allowed = false
Initialize the SDK
# ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import asyncio
import os
import json
from openfga_sdk.client import ClientConfiguration, OpenFgaClient
async def main():
configuration = ClientConfiguration(
api_url = os.environ.get('FGA_API_URL'), # required, e.g. 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()
asyncio.run(main())
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}
body = ClientCheckRequest(
user="user:amy",
relation="member",
object="workspace:sandcastle",
)
response = await fga_client.check(body, options)
# response.allowed = false
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.configuration.ClientConfiguration;
public class Example {
public static void main(String[] args) throws Exception {
var config = new ClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "https://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_AUTHORIZATION_MODEL_ID")); // Optional, can be overridden per request
var fgaClient = new OpenFgaClient(config);
}
}
var options = new ClientCheckOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var body = new ClientCheckRequest()
.user("user:amy")
.relation("member")
._object("workspace:sandcastle");
var response = fgaClient.check(body, options).get();
// response.getAllowed() = true
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
fga query check --store-id=$FGA_STORE_ID --model-id=01HVMMBCMGZNT3SED4Z17ECXCA user:amy member workspace:sandcastle
# Response: {"allowed":false}
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
curl -X POST $FGA_API_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": "01HVMMBCMGZNT3SED4Z17ECXCA", "tuple_key":{"user":"user:amy","relation":"member","object":"workspace:sandcastle"}}'
# Response: {"allowed":false}
check(
user = "user:amy", // check if the user `user:amy`
relation = "member", // has an `member` relation
object = "workspace:sandcastle", // with the object `workspace:sandcastle`
authorization_id = "01HVMMBCMGZNT3SED4Z17ECXCA"
);
Reply: false
As you saw before, running this query will return amy is not a member of workspace:sandcastle
, which is correct based on the data you have given OpenFGA so far. But in reality, Amy, who is a legacy_admin
already has an implied channels_admin
and member
relations. In fact anyone (other than a guest) is a member
of the workspace.
To change this behavior, we will update our system with a concentric relationship model.
With the following updated authorization model, you are informing OpenFGA that any user who is related to a workspace as legacy_admin
, is also related as a channels_admin
and a member
.
- DSL
- JSON
model
schema 1.1
type user
type workspace
relations
define legacy_admin: [user]
define channels_admin: [user] or legacy_admin
define member: [user] or channels_admin or legacy_admin
define guest: [user]
{
"schema_version": "1.1",
"type_definitions": [
{
"type": "user"
},
{
"type": "workspace",
"relations": {
"legacy_admin": {
"this": {}
},
"channels_admin": {
"union": {
"child": [
{
"this": {}
},
{
"computedUserset": {
"relation": "legacy_admin"
}
}
]
}
},
"member": {
"union": {
"child": [
{
"this": {}
},
{
"computedUserset": {
"relation": "channels_admin"
}
},
{
"computedUserset": {
"relation": "legacy_admin"
}
}
]
}
},
"guest": {
"this": {}
}
},
"metadata": {
"relations": {
"legacy_admin": {
"directly_related_user_types": [
{
"type": "user"
}
]
},
"channels_admin": {
"directly_related_user_types": [
{
"type": "user"
}
]
},
"member": {
"directly_related_user_types": [
{
"type": "user"
}
]
},
"guest": {
"directly_related_user_types": [
{
"type": "user"
}
]
}
}
}
}
]
}
We can then verify amy
is a member
of workspace:sandcastle
.
- Node.js
- Go
- .NET
- Python
- Java
- 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({
apiUrl: process.env.FGA_API_URL, // required, e.g. 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:amy',
relation: 'member',
object: 'workspace:sandcastle',
}, {
authorization_model_id: '01HVMMBCMGZNT3SED4Z17ECXCA',
});
// allowed = true
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import (
"os"
. "github.com/openfga/go-sdk"
. "github.com/openfga/go-sdk/client"
)
func main() {
// Initialize the SDK with no auth - see "How to setup SDK client" for more options
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiUrl: os.Getenv("FGA_API_URL"), // required, e.g. 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: os.Getenv("FGA_MODEL_ID"), // Optional, can be overridden per request
})
if err != nil {
// .. Handle error
}
}
options := ClientCheckOptions{
AuthorizationModelId: PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
}
body := ClientCheckRequest{
User: "user:amy",
Relation: "member",
Object: "workspace:sandcastle",
}
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() {
ApiUrl = Environment.GetEnvironmentVariable("FGA_API_URL"), ?? "http://localhost:8080", // required, e.g. 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 = "01HVMMBCMGZNT3SED4Z17ECXCA",
};
var body = new ClientCheckRequest {
User = "user:amy",
Relation = "member",
Object = "workspace:sandcastle",
};
var response = await fgaClient.Check(body, options);
// response.Allowed = true
Initialize the SDK
# ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import asyncio
import os
import json
from openfga_sdk.client import ClientConfiguration, OpenFgaClient
async def main():
configuration = ClientConfiguration(
api_url = os.environ.get('FGA_API_URL'), # required, e.g. 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()
asyncio.run(main())
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}
body = ClientCheckRequest(
user="user:amy",
relation="member",
object="workspace:sandcastle",
)
response = await fga_client.check(body, options)
# response.allowed = true
Initialize the SDK
// ApiTokenIssuer, ApiAudience, ClientId and ClientSecret are optional.
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.configuration.ClientConfiguration;
public class Example {
public static void main(String[] args) throws Exception {
var config = new ClientConfiguration()
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "https://localhost:8080"
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
.authorizationModelId(System.getenv("FGA_AUTHORIZATION_MODEL_ID")); // Optional, can be overridden per request
var fgaClient = new OpenFgaClient(config);
}
}
var options = new ClientCheckOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var body = new ClientCheckRequest()
.user("user:amy")
.relation("member")
._object("workspace:sandcastle");
var response = fgaClient.check(body, options).get();
// response.getAllowed() = true
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
fga query check --store-id=$FGA_STORE_ID --model-id=01HVMMBCMGZNT3SED4Z17ECXCA user:amy member workspace:sandcastle
# Response: {"allowed":true}
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
Set FGA_API_URL according to the service you are using (e.g. https://api.fga.example)
curl -X POST $FGA_API_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": "01HVMMBCMGZNT3SED4Z17ECXCA", "tuple_key":{"user":"user:amy","relation":"member","object":"workspace:sandcastle"}}'
# Response: {"allowed":true}
check(
user = "user:amy", // check if the user `user:amy`
relation = "member", // has an `member` relation
object = "workspace:sandcastle", // with the object `workspace:sandcastle`
authorization_id = "01HVMMBCMGZNT3SED4Z17ECXCA"
);
Reply: true
We can check for other users and relationships.
User | Object | Relation | Query | Relation? |
---|---|---|---|---|
amy | workspace:sandcastle | legacy_admin | is amy related to workspace:sandcastle as legacy_admin? | Yes |
david | workspace:sandcastle | legacy_admin | is david related to workspace:sandcastle as legacy_admin? | No |
amy | workspace:sandcastle | guest | is amy related to workspace:sandcastle as guest? | No |
david | workspace:sandcastle | guest | is david related to workspace:sandcastle as guest? | Yes |
amy | workspace:sandcastle | member | is amy related to workspace:sandcastle as member? | Yes |
david | workspace:sandcastle | member | is david related to workspace:sandcastle as member? | No |
03. Updating the authorization model to include channels
So far, you have modeled the users' relations to the workspace itself. In this task you will expand the model to include the relations concerning the channels.
By the end of it, you will run some queries to check whether a user can view or write to a certain channel. Queries such as:
is david related to channel:general as viewer?
(expected answer: No relation, as David is a guest user with only a relation to #proj-marketing-campaign)is david related to channel:proj_marketing_campaign as viewer?
(expected answer: There is a relation, as there is a relation between David and #proj-marketing-campaign as a writer)is bob related to channel:general as viewer?
(expected answer: There is a relation, as Bob is a member of the Sandcastle workspace, and all members of the workspace have a viewer relation to #general)
The requirements are:
- Amy, Bob, Catherine and Emily, are normal members of the Sandcastle workspace, they can view all public channels, in this case: #general and #proj-marketing-campaign
- David, a guest user, has only view and write access to the #proj-marketing-campaign channel
- Bob and Emily are the only ones with either view or write access to the #marketing-internal channel
- Amy and Emily are the only ones with write access to the #general channel
The possible relations to channels are:
- Workspace includes the channel, consider the relation that of a parent workspace
- A user can be a viewer and/or writer on a channel
The authorization model already has a section describing the workspace, what remains is describing the channel. That can be done by adding the following section to the configuration above:
- DSL
- JSON
type channel
relations
define parent_workspace: [workspace]
define writer: [user, workspace#legacy_admin, workspace#channels_admin, workspace#member, workspace#guest]
define viewer: [user, workspace#legacy_admin, workspace#channels_admin, workspace#member, workspace#guest]
{
"type": "channel",
"relations": {
"parent_workspace": {
"this": {}
},
"writer": {
"this": {}
},
"viewer": {
"this": {}
}
},
"metadata": {
"relations": {
"parent_workspace": {
"directly_related_user_types": [
{
"type": "workspace"
}
]
},
"writer": {
"directly_related_user_types": [
{
"type": "user"
},
{
"type": "workspace",
"relation": "legacy_admin"
},
{
"type": "workspace",
"relation": "channels_admin"
},
{
"type": "workspace",
"relation": "member"
},
{
"type": "workspace",
"relation": "guest"
}
]
},
"viewer": {
"directly_related_user_types": [
{
"type": "user"
},
{
"type": "workspace",
"relation": "legacy_admin"
},
{
"type": "workspace",
"relation": "channels_admin"
},
{
"type": "workspace",
"relation": "member"
},
{
"type": "workspace",
"relation": "guest"
}
]
}
}
}
}
The configuration snippet above describes a channel that can have the following relations:
- workspaces related to it as
parent_workspace
- users related to it as
writer
- users related to it as
viewer
Implied relation
There is an implied relation that anyone who can write to a channel can also read from it, so the authorization model can be modified to be:
- DSL
- JSON
type channel
relations
define parent_workspace: [workspace]
define writer: [user, workspace#legacy_admin, workspace#channels_admin, workspace#member, workspace#guest]
define viewer: [user, workspace#legacy_admin, workspace#channels_admin, workspace#member, workspace#guest] or writer
{
"type": "channel",
"relations": {
"parent_workspace": {
"this": {}
},
"writer": {
"this": {}
},
"viewer": {
"union": {
"child": [
{
"this": {}
},
{
"computedUserset": {
"relation": "writer"
}
}
]
}
}
},
"metadata": {
"relations": {
"parent_workspace": {
"directly_related_user_types": [
{
"type": "workspace"
}
]
},
"writer": {
"directly_related_user_types": [
{
"type": "user"
},
{
"type": "workspace",
"relation": "legacy_admin"
},
{
"type": "workspace",
"relation": "channels_admin"
},
{
"type": "workspace",
"relation": "member"
},
{
"type": "workspace",
"relation": "guest"
}
]
},
"viewer": {
"directly_related_user_types": [
{
"type": "user"
},
{
"type": "workspace",
"relation": "legacy_admin"
},
{
"type": "workspace",
"relation": "channels_admin"
},
{
"type": "workspace",
"relation": "member"
},
{
"type": "workspace",
"relation": "guest"
}
]
}
}
}
}
Note that the channel type definition has been updated to indicate that viewer is the union of:
- the set of users with a direct viewer relation to this object
- the set of users with writer relations to this object
As a result, the authorization model is:
- DSL
- JSON
model
schema 1.1
type user
type workspace
relations
define legacy_admin: [user]
define channels_admin: [user] or legacy_admin
define member: [user] or channels_admin or legacy_admin
define guest: [user]
type channel
relations
define parent_workspace: [workspace]
define writer: [user, workspace#legacy_admin, workspace#channels_admin, workspace#member, workspace#guest]
define viewer: [user, workspace#legacy_admin, workspace#channels_admin, workspace#member, workspace#guest] or writer
{
"schema_version": "1.1",
"type_definitions": [
{
"type": "user"
},
{
"type": "workspace",
"relations": {
"legacy_admin": {
"this": {}
},
"channels_admin": {
"union": {
"child": [
{
"this": {}
},
{
"computedUserset": {
"relation": "legacy_admin"
}
}
]
}
},
"member": {
"union": {
"child": [
{
"this": {}
},
{
"computedUserset": {
"relation": "channels_admin"
}
},
{
"computedUserset": {
"relation": "legacy_admin"