DEV Community

Yash Raj
Yash Raj

Posted on

WTH is openFGA and implementing Role-Based Access Control in GO servers.

While I have given a task to implement the Role-Based Access Control in the product that I am working on in my internship, I dug a bit. I looked for some open source project that can help me do this. During that period, I came across a project called openFGA, which is an open-source Fine-Grained Authorisation solution inspired by Google's Zanzibar paper, which is currently under CNCF and built by the engineers at Auth0(Okta). I looked at the docs, which instantly got my interest as the implementation was easy and straightforward. OpenFGA divides the whole Role-Based Access thing into a few components and makes it really easy to implement it in any complex project.

So what really openFGA is?
OpenFGA is an open source Fine-Grained Authorisation solution inspired by Google's Zanzibar paper, and helps you to implement complex Role-Based Access Control Authorisation shamelessly. Example - what we have similar in Google Docs, where the owner of a file can give view/edit access to a file/folder with one click.

Components and how they work?

  • OpenFGA Server: A permission engine that implements the fine-grained authorisation logic
  • Authorisation Models: Define the rules and structure for permissions in the system. Consists of definitions, which specify how different objects relate to each other. Used to define roles and relationships (e.g. viewer, owner, editor).
  • Relationship Tuples: Relationship tuples are the instances of relationships defined in the authorisation model (e.g. user:anne, viewer, document:1)
  • Stores: The data storage for tuples, which represent relationships between users and objects. Each store contains a specific authorisation model and its associated data.
  • Client SDKs: Libraries available for various programming languages to interact with the OpenFGA server.

How does it work? and Architecture!
we will take an example of a Go server which has simple auth and the user can create-file(/create-file), view-file (/files) and share-file(/share-file) on respective routes.

  • First of all we need to run the openFGA locally, and for that, we will be using Docker Compose. its the easiest way and get work done and set up in one click. (make sure to have already installed)
  • Make a docker-compose.yml at the root of your Go web server, paste this and run the cmd docker compose up -d. this will run the openFGA server at port 8080.
version: '3.8'

networks:
  openfga:

services:
  postgres:
    image: postgres:17
    container_name: postgres
    networks:
      - openfga
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
    healthcheck:
      test: [ "CMD-SHELL", "pg_isready -U postgres" ]
      interval: 5s
      timeout: 5s
      retries: 5

  migrate:
    depends_on:
      postgres:
        condition: service_healthy
    image: openfga/openfga:latest
    container_name: migrate
    command: migrate
    environment:
      - OPENFGA_DATASTORE_ENGINE=postgres
      - OPENFGA_DATASTORE_URI=postgres://postgres:password@postgres:5432/postgres?sslmode=disable
    networks:
      - openfga

  openfga:
    depends_on:
      migrate:
        condition: service_completed_successfully
    image: openfga/openfga:latest
    container_name: openfga
    environment:
      - OPENFGA_DATASTORE_ENGINE=postgres
      - OPENFGA_DATASTORE_URI=postgres://postgres:password@postgres:5432/postgres?sslmode=disable
      - OPENFGA_LOG_FORMAT=json
    command: run
    networks:
      - openfga
    ports:
      # Needed for the http server
      - "8080:8080"
      # Needed for the grpc server (if used)
      - "8081:8081"
      # Needed for the playground (Do not enable in prod!)
      - "3000:3000"
Enter fullscreen mode Exit fullscreen mode
  • Now when server is running we need to create the Authorisation Models that Define the rules and structure for permissions in the system. (e.g. viewer, owner, editor) for this Go to http://localhost:3000/playground and create a store and make the models

    Here, there are two types, user and file and defined respective relations.

  • Now, during the api calls, we will be creating tuples and saving them through the openFGA server while we save the file data into the database. example.

  • The architecture of our app will look something like this.

Code implementation

  • Install the Go client Sdk with go get -u github.com/openfga/go-sdk and import it in the file.
  • initialise the openFGA client and mongoDB
func init() {
    // initialize MongoDB
    mongoURL := "mongodb+srv://........"
    client, _ := mongo.Connect(options.Client().ApplyURI(mongoURL))
    filesCollection = client.Database("api").Collection("Files")

    // initialize OpenFGA client
    var err error
    fgaClient, err = fgaclient.NewSdkClient(&fgaclient.ClientConfiguration{
        ApiUrl:  "http://localhost:8080",
        StoreId: "01K7J7Y4NTKW4W2RJNG6HRTQC1", 
        AuthorizationModelId: "01K7J8F5X3EKXPPZ931M2ZKNW8", 
    })
    if err != nil {
        fgaClient = nil
    }
}

Enter fullscreen mode Exit fullscreen mode
  • The code is using a simple JWT token to sign up and an Auth middleware to protect other routes. and basic struct for file and user struct.
type User struct {
    Username string `json:"username"`
    Email    string `json:"email"`
}

type FileRecord struct {
    ID       string `json:"id" bson:"_id"`
    Filename string `json:"filename" bson:"filename"`
}

func authMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.Next()
            return
        }

        parts := strings.Split(authHeader, "Bearer ")
        if len(parts) != 2 {
            c.Next()
            return
        }

        token, err := jwt.Parse(strings.TrimSpace(parts[1]), func(token *jwt.Token) (interface{}, error) {
            return jwtSecret, nil
        })

        if err == nil && token.Valid {
            if claims, ok := token.Claims.(jwt.MapClaims); ok {
                c.Set("user", User{
                    Username: claims["username"].(string),
                    Email:    claims["email"].(string),
                })
            }
        }

        c.Next()
    }
}

Enter fullscreen mode Exit fullscreen mode
  • /create-file will store the file in the DB(mongodb), and using the openFGA client, it will create a tuple in the OpenFGA PostgreSQL DB through the openFGA server, with an owner relation to the user with the file.
func createFile(c *gin.Context) {
    userVal, exists := c.Get("user")
    if !exists {
        c.JSON(401, gin.H{"error": "Please login"})
        return
    }

    user := userVal.(User)
    var req struct {
        ID       string `json:"id"` // file id
        Filename string `json:"filename"`
    }
    c.ShouldBindJSON(&req)

    existingFile, _ := getFileRecordById(req.ID)
    if existingFile != nil {
        c.JSON(400, gin.H{"error": "file with id " + req.ID + " already exists!"})
        return
    }

    createFileRecord(FileRecord{ID: req.ID, Filename: req.Filename})

    // Use OpenFGA SDK 
    if fgaClient != nil {
        body := fgaclient.ClientWriteRequest{
            Writes: []fgaclient.ClientTupleKey{{
                User:     "user:" + user.Username,
                Relation: "owner",
                Object:   "file:" + req.ID,
            }},
        }

        _, err := fgaClient.Write(context.Background()).Body(body).Execute()
        if err != nil {
            c.JSON(500, gin.H{"error": "Failed to set file ownership"})
            return
        }
    }

    c.JSON(201, gin.H{"message": "File created success!"})
}
Enter fullscreen mode Exit fullscreen mode
  • /files will filter out the IDs of files associated with the user with view access (owner or can_view) and return the files associated with these IDs.
func getFiles(c *gin.Context) {
    userVal, exists := c.Get("user")
    if !exists {
        c.JSON(401, gin.H{"error": "Please login"})
        return
    }

    user := userVal.(User)

    // Use OpenFGA SDK 
    if fgaClient != nil {
        body := fgaclient.ClientListObjectsRequest{
            User:     "user:" + user.Username,
            Relation: "can_view",
            Type:     "file",
        }

        allowedFiles, err := fgaClient.ListObjects(context.Background()).Body(body).Execute()
        if err != nil {
            c.JSON(500, gin.H{"error": "Failed to get allowed files"})
            return
        }

        // Extract file IDs from objects
        var allowedFileIds []string
        for _, obj := range allowedFiles.Objects {
            parts := strings.Split(obj, ":")
            if len(parts) == 2 {
                allowedFileIds = append(allowedFileIds, parts[1])
            }
        }

        // Only fetch allowed files from database (much more efficient!)
        var allowed []FileRecord
        if len(allowedFileIds) > 0 {
            // Use MongoDB $in operator to fetch only allowed files
            filter := bson.M{"_id": bson.M{"$in": allowedFileIds}}
            cursor, err := filesCollection.Find(context.Background(), filter)
            if err == nil {
                defer cursor.Close(context.Background())
                cursor.All(context.Background(), &allowed)
            }
        }

        c.JSON(200, gin.H{"files": allowed})
    } else {
        // Fallback when OpenFGA not configured
        files, _ := getAllFileRecords()
        c.JSON(200, gin.H{"files": files})
    }
}

Enter fullscreen mode Exit fullscreen mode
  • /share-file will check if the current user has the "can-edit" file permission. If yes, then a tuple with and viewer relation will be created with the file ID and the user for whom the access is going.

unc shareFile(c *gin.Context) {
    userVal, exists := c.Get("user")
    if !exists {
        c.JSON(401, gin.H{"error": "Please login"})
        return
    }

    user := userVal.(User)
    var req struct {
        ID       string `json:"id"` // file id
        Username string `json:"username"` // username to which we need to associate that file
    }
    c.ShouldBindJSON(&req)

    // SECURITY CHECK: Verify the current user can edit access of this file
    if fgaClient != nil {
        // Check if user has permission to edit access (can_edit file acces permission)
        checkBody := fgaclient.ClientCheckRequest{
            User:     "user:" + user.Username,
            Relation: "can_edit",
            Object:   "file:" + req.ID,
        }

        checkResp, err := fgaClient.Check(context.Background()).Body(checkBody).Execute()
        if err != nil {
            c.JSON(500, gin.H{"error": "Failed to verify permissions"})
            return
        }

        if checkResp.Allowed == nil || !*checkResp.Allowed {
            c.JSON(403, gin.H{"error": "You don't have permission to edit access for this file"})
            return
        }

        // If authorized, create the viewer relationship
        body := fgaclient.ClientWriteRequest{
            Writes: []fgaclient.ClientTupleKey{{
                User:     "user:" + req.Username,
                Relation: "viewer",
                Object:   "file:" + req.ID,
            }},
        }

        _, err = fgaClient.Write(context.Background()).Body(body).Execute()
        if err != nil {
            c.JSON(500, gin.H{"error": "Failed to share file"})
            return
        }
    }

    c.JSON(200, gin.H{"message": "Access Added"})
}


Enter fullscreen mode Exit fullscreen mode

Conclusion
Successfully implementing the Role-Based Access Control (RBAC) and doing this manually can quickly get messy as an application grows in complexity. That’s where OpenFGA truly shines. it brings structure, scalability, and flexibility to authorisation without forcing you to reinvent the wheel. With just a few API calls, you can define and enforce permissions across users, roles, and resources in a clean way.

You can access the full code here (https://github.com/yash-raj10/Go-openFGA-implementation)

Top comments (0)