If you haven't followed up with my last post, i.e., Part 1, you can definitely grab the necessary concepts for this following post.
In this blog, I will focus on various aspects, from what you can experiment with to a practical workaround example.
We will understand the workflow of authorization model using a use case and its implementation on solving the issue of permissions access.
Modeling SaaS Project Permissions with OpenFGA
To understand our Fine-Grained Authorization (FGA) model, we will take a SaaS project use case into account. We will model a project organization permission model using OpenFGA. Our goal is to build a service that enables users to develop and collaborate on features efficiently.
We will implement a subset of the feature permission model using the OpenFGA Go SDK and validate the model through a few access control scenarios.
Requirements Recap
- Users can be admins or members of services.
- Each role inherits the permissions of the lower level (i.e., admins inherit member access).
- Teams and organizations can have members.
- Organizations can own services.
- Organization admins have admin access to all services under that organization.
We will configure OpenFGA locally where we will use docker to run the tool and step further on my toes
Setting Up OpenFGA
There are multiple ways to set up OpenFGA, but we will leverage the Docker setup, as it is quite easy and traceable.
Running OpenFGA Locally
If you want to run OpenFGA locally as a Docker container, follow these steps:
Install Docker (if not already installed).
Pull the latest OpenFGA Docker image:
docker pull openfga/openfga
Run OpenFGA as a container:
docker run -p 8080:8080 -p 8081:8081 -p 3000:3000 openfga/openfga run
This will start:
An HTTP server on port 8080.
A gRPC server on port 8081.
The Playground on port 3000.
Running OpenFGA with Postgres
To run OpenFGA and Postgres in containers, follow these steps:
Create a Docker network to simplify communication between containers:
docker network create openfga
Start a Postgres container in the created network:
docker run -d --name postgres --network=openfga -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=password postgres:14
This will start Postgres in the openfga network.
Run the database migration to set up necessary tables (Very important, don't miss this step):
docker run --rm --network=openfga openfga/openfga migrate \
--datastore-engine postgres \
--datastore-uri "postgres://postgres:password@postgres:5432/postgres?sslmode=disable"
Start OpenFGA and connect it to Postgres:
docker run --name openfga --network=openfga -p 3000:3000 -p 8080:8080 -p 8081:8081 openfga/openfga run \
--datastore-engine postgres \
--datastore-uri 'postgres://postgres:password@postgres:5432/postgres?sslmode=disable'
This setup ensures:
The Postgres database is running.
The OpenFGA migration process is completed.
The OpenFGA server is running and connected to Postgres.
OpenFGA Playground
The Playground facilitates rapid development by allowing you to visualize and model your application's authorization models and manage relationship tuples with a locally running OpenFGA instance.
It is enabled on port 3000 by default and accessible at http://localhost:3000/playground.
Let's understand OpenFGA Model for SaaS Project and visualise it using OpenFGA playground
model
schema 1.1
type user
type team
relations
define member: [user]
type organization
relations
define admin: [user, team#member] # Org admins
define member: [user, team#member] # Org members
define owner: [user] # Org owners
type service
relations
define admin: [user, organization#admin] # Admins include org admins
define member: [user, team] # Members inherit from team
define owner: [organization] # Services belong to an org
How This Model Works
User Roles
organization#admin → service#admin (Org admins inherit service admin access)
admin → member (Admins inherit all member permissions)
Team-Based Access
A team can include multiple users.
Teams can be granted organization#admin or organization#member permissions.
Service Ownership
Services are owned by an organization (service#owner).
Organization admins automatically get admin access to all services.
Example Use Cases
1️⃣ User is an Admin of an Organization
If a user is an admin of organization:org-1, they automatically have admin access to all services under that organization.
{
"user": "alice",
"relation": "admin",
"object": "organization:org-1"
}
✔ Alice has admin access to all services in org-1.
2️⃣ A Team Manages Multiple Services
If a team is assigned as an organization#admin, all its members automatically get admin access to services.
{
"user": "team-devops",
"relation": "admin",
"object": "organization:org-1"
}
✔ All members of team-devops inherit service admin access.
3️⃣ A User is a Member of a Specific Service
If a user is a service#member, they only have access to that specific service.
{
"user": "bob",
"relation": "member",
"object": "service:feature-x"
}
✔ Bob can collaborate on feature-x but does not have admin access.
Authorization Model with OpenFGA (Using Go SDK)
Let’s implement this using the OpenFGA Go SDK.
- Install the SDK
go get github.com/openfga/go-sdk
- Initialise the client and create the store After running OpenFGA locally, we will setup the envs as follows
Make sure to avoid creating store id multiple times once we get the id after one run
export FGA_API_URL=http://localhost:8080
export FGA_STORE_ID=01JR3M255RHWEE4KXGHE71H3F3 // example id
func main() {
// Initialize OpenFGA client
fgaClient, err := NewSdkClient(&ClientConfiguration{
ApiUrl: os.Getenv("FGA_API_URL"), // e.g., "https://api.fga.example"
StoreId: os.Getenv("FGA_STORE_ID"), // Required after store creation
})
if err != nil {
log.Fatalf("Failed to create OpenFGA client: %v", err)
}
// Create OpenFGA store
resp, err := fgaClient.CreateStore(context.Background()).
Body(ClientCreateStoreRequest{Name: "Akuity Org"}).
Execute()
if err != nil {
log.Fatalf("Failed to create OpenFGA store: %v", err)
}
fmt.Println("Store created:", resp.GetId())
}
- Define and Write the Authorization Model This model defines roles and permissions for organizations, services, teams, and users.
func configureAuthorizationModel(fgaClient *OpenFgaClient) {
var writeAuthorizationModelRequestString = `{
"schema_version": "1.1",
"type_definitions": [
{
"type": "user"
},
{
"type": "team",
"relations": {
"member": {
"this": {}
}
},
"metadata": {
"relations": {
"member": {
"directly_related_user_types": [
{ "type": "user" }
]
}
}
}
},
{
"type": "organization",
"relations": {
"admin": {
"union": {
"child": [
{ "this": {} },
{ "computedUserset": { "relation": "member", "userset": "team#member" } }
]
}
},
"member": {
"this": {}
},
"owner": {
"this": {}
}
},
"metadata": {
"relations": {
"admin": {
"directly_related_user_types": [
{ "type": "user" },
{ "type": "team", "relation": "member" }
]
},
"member": {
"directly_related_user_types": [
{ "type": "user" }
]
},
"owner": {
"directly_related_user_types": [
{ "type": "user" }
]
}
}
}
},
{
"type": "service",
"relations": {
"owner": {
"this": {}
},
"admin": {
"union": {
"child": [
{ "this": {} },
{
"tupleToUserset": {
"tupleset": { "relation": "owner" },
"computedUserset": { "relation": "admin" }
}
}
]
}
},
"member": {
"union": {
"child": [
{ "this": {} },
{
"computedUserset": {
"relation": "admin",
"userset": "service#admin"
}
}
]
}
}
},
"metadata": {
"relations": {
"owner": {
"directly_related_user_types": [
{ "type": "organization" }
]
},
"admin": {
"directly_related_user_types": [
{ "type": "user" }
]
},
"member": {
"directly_related_user_types": [
{ "type": "user" }
]
}
}
}
}
]
}`
var body ClientWriteAuthorizationModelRequest
if err := json.Unmarshal([]byte(writeAuthorizationModelRequestString), &body); err != nil {
log.Fatalf("Failed to parse model JSON: %v", err)
}
// Write the model to OpenFGA
response, err := fgaClient.WriteAuthorizationModel(context.Background()).Body(body).Execute()
if err != nil {
log.Fatalf("Failed to write authorization model: %v", err)
}
fmt.Println("Authorization model configured, Model ID:", response.GetAuthorizationModelId())
}
- Assign roles(Set Relationships) This function assigns users to specific roles in organizations or services.
func assignRole(fgaClient *OpenFgaClient, user, object, relation string) {
_, err := fgaClient.Write(context.Background()).Body(ClientWriteRequest{
Writes: []ClientTupleKey{
{
User: "user:" + user,
Relation: relation,
Object: object,
},
},
}).Execute()
if err != nil {
log.Fatalf("Failed to assign role: %v", err)
}
fmt.Printf("Assigned %s as %s to %s\n", user, relation, object)
}
e.g.
assignRole(fgaClient, "james", "organization:akuity", "admin")
assignRole(fgaClient, "alice", "organization:akuity", "member")
Assigned james as admin to organization:akuity
- Check permissions This function verifies if a user has a specific permission.
func checkPermission(fgaClient *OpenFgaClient, user, object, relation, modelID string) {
options := ClientCheckOptions{
AuthorizationModelId: PtrString(modelID),
}
body := ClientCheckRequest{
User: "user:" + user,
Relation: relation,
Object: object,
}
data, err := (*fgaClient).Check(context.Background()).
Body(body).
Options(options).
Execute()
if err != nil {
log.Fatalf("Failed to check permission: %v", err)
}
fmt.Printf("User %s has %s access to %s: %v\n", user, relation, object, data.Allowed)
}
e.g.
checkPermission(fgaClient, "alice", "member:org1", "admin", "01JR4FY69VK3G33EFTT6A1372E")
User alice has admin access to organization:org1: 0x1400021e117
Conclusion
In this second part of the series, we took a hands-on approach to implementing fine-grained authorization using OpenFGA in the context of a SaaS project. From setting up OpenFGA with Docker and Postgres to defining an extensible authorization model with the Go SDK, we've covered the foundational steps to get your access control logic up and running.
By modeling roles like admin and member, introducing hierarchical permission inheritance, and incorporating organizational ownership, we've laid the groundwork for a scalable and maintainable permissions system.
This model not only supports flexibility but also aligns with real-world requirements of multi-tenant SaaS platforms, writing relationship tuples, validating permissions via API calls,
and showing real-world scenarios to test access logic.
Until then, feel free to experiment with the Playground, tweak the model, and explore how OpenFGA can be tailored to your specific authorization needs. 🔐
Let me know if you'd like me to add this directly to the doc or tweak the tone!
Top comments (0)