Fine-Grained Access Control (FGAC): Comprehensive Guidance

This guide is tailored to explain Fine-Grained Access Control (FGAC), highlight its significance, and provide a step-by-step implementation for your applications using Permify


In this guide, we'll cover several key areas:

Securing who can access what under which conditions - also known as authorization - is a crucial part of software systems due to scaled cloud-native environments, distinct and multi-service architectures, never-ending business requirements and so on.

Role Based Access Control (RBAC) is one of the most popular and traditional way to apply access controls in your applications and services.

To give a brief explanation of RBAC, someone is assigned a role and they inherit the permissions associated with that role. For instance, managers might have access to certain files that entry-level employees do not.

The pitfalls of RBAC model is; its coarse-grained, inflexible, and cannot scale.

That's why most companies choose Fine-Grained Access Control over coarse grained RBAC.

This guide is tailored to explain Fine-Grained Access Control (FGAC), highlight its significance, and provide a step-by-step implementation for your applications.

What is Fine-Grained Access Control (FGAC)?

Fine-Grained Access Control is a detailed and nuanced approach to access control within your company's requirements.

Unlike coarse grained access control models that might grant access to large sections of data or functions based on a single factor like roles, fine-grained authorization allows you to specify access rights at a much more specific level, including Attribute-Based Access Control (ABAC) and Relationship-Based Access Control (ReBAC).

This means you can define not just who can access a resource, but under what precise conditions they can do so, including actions like viewing, editing, sharing, or deleting.

Read More: Fine-Grained Access Control Where RBAC falls short

Imagine a healthcare application that manages patient records.

With fine-grained authorization, you can set up access controls that reflect the complex needs and privacy requirements of the healthcare industry.

Here’s how it might work:

  • Doctors: Can view and edit the medical records of their current patients but cannot access records of patients they are not treating. Additionally, they might be allowed to share records with other doctors within the same hospital for consultation, but only if the patient has consented to this sharing.
  • Nurses: Have view access to patient records but can only edit sections related to nursing care, such as notes on medication administration or patient vitals. Their access is limited to patients they are currently assigned to.
  • Administrative Staff: Can access patient contact information and billing details but cannot view medical history or notes made by the healthcare professionals.
  • Patients: Can view their own medical records through a patient portal but cannot make any edits. They may be given the option to share their records with external healthcare providers, but this action requires explicit patient consent and generates an audit trail.

By defining specific access controls for different user roles and conditions, the healthcare application can protect sensitive information, comply with privacy regulations, and ensure that users have the access they need to perform their roles effectively.

Why Companies Should Look for Fine-Grained Access Control?

Here are the compelling reasons why companies should prioritize fine-grained authorization:

Enhanced Security

By defining access with precision, fine-grained authorization minimizes the risk of unauthorized access to sensitive data.

This precision ensures that individuals have access only to the data and functions necessary for their roles, significantly reducing the attack surface for potential cyber threats.

Compliance and Privacy

Many industries are governed by strict regulatory requirements regarding data access and privacy (e.g., GDPR in Europe, HIPAA in healthcare).

Fine-Grained Access Control allows companies to meet these regulations head-on by enforcing access policies that protect personal and sensitive information, thereby avoiding hefty fines and reputational damage.

Operational Flexibility and Efficiency

In the dynamic landscape of business operations, roles and responsibilities can change rapidly.

Fine-Grained Access Control facilitates quick adjustments to access rights, ensuring that employees have the resources they need when they need them, without compromising security. This agility enhances overall operational efficiency and productivity.

Audit and Oversight

Implementing fine-grained authorization enables detailed logging and auditing of access to resources, providing clear visibility into who accessed what and when.

This capability is invaluable for investigating security incidents, monitoring compliance, and refining access controls over time.

How to Build a Fine-Grained Access Control?

In this section, we'll show how to implement Fine-Grained Access Control in our example Golang application

For implementation we'll use Permify, an open source authorization service that enables developers to implement fine-grained access control scenarios easily.

Let's dive deeper into how to use Permify to build a fine-grained authorization system, focusing particularly on the critical testing and validation phase.

Understanding Permify

Permify provides a robust platform for defining, managing, and enforcing fine-grained access controls.

It allows you to specify detailed authorization rules that reflect real-world requirements, ensuring that users only access the resources they are allowed to, in accordance with their permissions to those resources.

Setting Up Permify

  1. Installation: Begin by running Permify as a Docker container. This approach simplifies setup and ensures consistency across environments.

    docker run -p 3476:3476 -p 3478:3478 ghcr.io/permify/permify serve
    

    This command starts the Permify service, making it accessible via its REST and gRPC interfaces.

  2. Verify Installation with Postman: Postman is an effective tool for testing API endpoints. After launching Permify:

  • Open Postman and create a new request.

  • Set the request type to GET.

  • Enter the URL http://localhost:3476/healthz.

  • Send the request. A successful setup is indicated by a 200 OK response, confirming that Permify is operational.

    screenshot.png

Modeling Authorization with Permify Schema

The schema is the heart of your authorization system, defining the entities involved and how they relate to each other.

The provided schema example demonstrates a system similar to Google Docs, showcasing entities like user, organization, group, and document, along with their relationships and permissions.

  • Entities represent the main components of your system.
  • Relations outline how entities interact, e.g., which user owns a document or is part of a group.
  • Permissions specify allowed actions based on roles within these relationships.
entity user {}

entity organization {
    relation group @group
    relation document @document
    relation administrator @user @group#direct_member @group#manager
    relation direct_member @user

    permission admin = administrator
    permission member = direct_member or administrator or group.member
}

entity group {
    relation manager @user @group#direct_member @group#manager
    relation direct_member @user @group#direct_member @group#manager

    permission member = direct_member or manager
}

entity document {
    relation org @organization

    relation viewer  @user  @group#direct_member @group#manager
    relation manager @user @group#direct_member @group#manager

    action edit = manager or org.admin
    action view = viewer or manager or org.admin
}

In the schema, the @ symbol denotes the target of a relation (indicating a connection to another entity or a specific relation within an entity), while the # symbol specifies a particular relation within a target entity.

Here's a breakdown of the schema components for clarity:

  • entity user {}
    • Represents individual users in the system.
  • entity organization
    • relation group @group: Links an organization to one or more groups.
    • relation document @document: Connects an organization to documents.
    • relation administrator @user @group#direct_member @group#manager: Defines administrators of the organization as users who are either direct members of a group or managers within a group.
    • relation direct_member @user: Identifies users who are direct members of the organization.
    • permission admin = administrator: Grants administrator permissions to users defined as administrators.
    • permission member = direct_member or administrator or group.member: Assigns member permissions to users who are either direct members, administrators, or members of a group within the organization.
  • entity group
    • relation manager @user @group#direct_member @group#manager: Specifies the managers of the group, including users who are direct members of the group or designated as group managers.
    • relation direct_member @user @group#direct_member @group#manager: Denotes direct members of the group, who can also be group managers.
    • permission member = direct_member or manager: Provides member permissions to users who are either direct members or managers of the group.
  • entity document
    • relation org @organization: Associates documents with their respective organizations.
    • relation viewer @user @group#direct_member @group#manager: Defines viewers of the document as users who are either direct members or managers of a group.
    • relation manager @user @group#direct_member @group#manager: Identifies managers of the document, which includes users who are direct members or managers in a group.
    • action edit = manager or org.admin: Allows document editing by either the document's manager or the organization's administrators.
    • action view = viewer or manager or org.admin: Permits viewing of the document by viewers, managers, or organization administrators.

Implementing Permify with Go SDK

This section guides you through creating a simple Go application to implement Permify using its Go SDK. We assume that you already have your Permify environment set up and your schema ready.

The following implementation only covers the crucial steps. To access the complete project, including all code, HTML, and CSS files, you can use this GitHub Repo.

Step 1: Initialize the Permify Client

To use the Permify SDK in a Go application, the first step is to establish a connection with the Permify server by initializing a client that communicates with the Permify service.

This involves configuring the client with the endpoint of your Permify server and setting up the transport credentials.

permify_client.go

package main

import (
	"context"
	"log"
	"time"

	v1 "github.com/Permify/permify-go/generated/base/v1"
	permify "github.com/Permify/permify-go/v1"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

var client *permify.Client

// setupPermifyClient initializes the Permify client and sets up the Permify schema.
func setupPermifyClient() {
	var err error
	// Initialize the Permify client
	client, err = permify.NewClient(
		permify.Config{
			Endpoint: "localhost:3478", // Replace with the actual address of your Permify deployment
		},
		grpc.WithTransportCredentials(insecure.NewCredentials()), // Use insecure credentials for development
	)
	if err != nil {
		log.Fatalf("Failed to create Permify client: %v", err)
	}

	// Setup Permify schema and other configurations
	initPermifySchema()
}

In this code snippet:

  • We import necessary packages such as context, log, and time for context management, logging, and time-related operations, respectively.
  • We import the Permify SDK packages v1 and permify.
  • We import the grpc package for setting up the gRPC connection with the Permify server.
  • We import credentials/insecure for setting up insecure transport credentials, suitable for development environments.
  • We define a setupPermifyClient() function that initializes the Permify client and sets up the Permify schema.

This function initializes the Permify client by specifying the endpoint of the Permify server and configuring transport credentials. It also handles any errors that occur during initialization.

Step 2: Define and Write the Schema

Once you've initialized the Permify client, the next step is to define the schema for your application. The schema defines the entities, relationships between them, and the permissions associated with those relationships. Once the schema is defined, it must be written to the Permify service to be enforced.

Schema Definition

The schema is defined using a domain-specific language (DSL) provided by Permify. This DSL allows you to specify entities, relationships, and permissions in a concise and human-readable format. Here's an example schema definition for a basic document management system:

entity user {}
entity organization {
    relation group @group
    relation document @document
    relation administrator @user @group#direct_member @group#manager
    relation direct_member @user
    permission admin = administrator
    permission member = direct_member or administrator or group.member
}
entity group {
    relation manager @user @group#direct_member @group#manager
    relation direct_member @user @group#direct_member @group#manager
    permission member = direct_member or manager
}
entity document {
    relation org @organization
    relation viewer @user @group#direct_member @group#manager
    relation manager @user @group#direct_member @group#manager
    action edit = manager or org.admin
    action view = viewer or manager or org.admin
}

In this schema:

  • We define four entities: user, organization, group, and document.
  • Each entity can have relationships with other entities, specified using the relation keyword.
  • We define permissions (admin and member) for the organization entity based on its relationships with other entities.
  • The document entity has actions (edit and view) associated with it, with permissions based on the relationships defined in the schema.

Writing the Schema

Once the schema is defined, it must be written to the Permify service using the Permify client. This ensures that the access control rules defined in the schema are enforced by the Permify system. Here's how you can write the schema using the Permify client:

func initPermifySchema() {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // Define the schema
    schema := `/* Schema definition goes here */`

    // Write the schema to the Permify service
    sr, err := client.Schema.Write(ctx, &v1.SchemaWriteRequest{
        TenantId: "t1",
        Schema:   schema,
    })
    if err != nil {
        log.Fatalf("Failed to write schema: %v", err)
    }
    schemaVersion = sr.SchemaVersion
    log.Printf("Schema version %s written successfully", schemaVersion)
}

In this code snippet:

  • We initialize a context with a timeout to ensure that the schema write operation doesn't hang indefinitely.
  • We define the schema as a string using the DSL provided by Permify.
  • We use the Permify client to write the schema to the Permify service, specifying the tenant ID and the schema itself.
  • If the schema write operation is successful, we store the schema version for future reference.

Step 3: Store Relationships and Permissions

Once the schema has been defined and written, the next crucial step is populating the Permify system with specific instances of relationships and permissions according to your schema definitions.

This involves creating data tuples that represent real-world relationships between the entities defined in your schema.

Understanding Relationships and Permissions

In the context of Permify, relationships and permissions are represented as tuples. These tuples articulate the connections between entities (such as a user and a document) and specify the kind of access allowed (e.g., viewing or editing).

This structured format enables Permify to quickly evaluate access requests based on the predefined rules in your schema.

Writing Data Tuples Using the Permify Client

To enforce the access control rules defined in your schema, you need to populate the Permify system with actual data that reflects the relationships in your application.

This is typically done by writing data tuples to Permify after defining your schema. Each tuple represents a specific permission or relationship instance between entities.

Here's how you can write data tuples using the Permify client in Go:

func storeRelationships() {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // Write relationships between entities
    rr, err := client.Data.Write(ctx, &v1.DataWriteRequest{
        TenantId: "t1",
        Metadata: &v1.DataWriteRequestMetadata{
            SchemaVersion: schemaVersion,
        },
        Tuples: []*v1.Tuple{
            {
                Entity:   &v1.Entity{Type: "document", Id: "1"},
                Relation: "viewer",
                Subject:  &v1.Subject{Type: "user", Id: "user1"},
            },
            {
                Entity:   &v1.Entity{Type: "document", Id: "1"},
                Relation: "manager",
                Subject:  &v1.Subject{Type: "user", Id: "user3"},
            },
            // Add more tuples as needed
        },
    })
    if err != nil {
        log.Fatalf("Failed to write data tuples: %v", err)
    }
    snapToken = rr.SnapToken
    log.Printf("Data tuples written successfully, snapshot token: %s", snapToken)
}

In this code snippet:

  • We initialize a context with a timeout to ensure that the data write operation doesn't hang indefinitely.
  • We use the Permify client to write data tuples to the Permify service, specifying the tenant ID, schema version, and the tuples themselves.
  • Each tuple defines a relationship between entities, such as a user being a viewer or manager of a document.
  • If the data write operation is successful, we store the snapshot token for future reference.

By storing relationships and permissions in the Permify system, you enable it to enforce the access control rules defined in your schema effectively.

Step 4: Perform Access Checks

After setting up the schema and storing relationships, the next critical step involves performing access checks to determine if a particular user has the necessary permissions to access a specific resource.

This step is pivotal for enforcing the access control rules defined and stored in the Permify system.

Understanding Access Checks

Access checks involve querying the Permify system to evaluate whether a specified subject (e.g., a user) is allowed to perform a certain action (e.g., view or edit) on a resource (e.g., a document) based on the existing relationships and permissions.

These checks enforce your application's security policies in real-time, ensuring that only authorized users can access specific resources.

Implementing Access Checks in the Application

Access checks are typically triggered by user actions that require validation of permissions. For example, when a user attempts to access a protected page or resource, the application queries Permify to confirm whether the access should be allowed.

Below is an example of how to implement an access check using the Permify client in a Go web application:

func checkPermission(username, permission string) bool {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	checkResult, err := client.Permission.Check(ctx, &v1.PermissionCheckRequest{
		TenantId: "t1",
		Entity: &v1.Entity{
			Type: "document",
			Id:   "1",
		},
		Permission: permission,
		Subject: &v1.Subject{
			Type: "user",
			Id:   username,
		},
		Metadata: &v1.PermissionCheckRequestMetadata{
			SnapToken:     snapToken,
			SchemaVersion: schemaVersion,
			Depth:         50,
		},
	})
	if err != nil {
		log.Printf("Failed to check permission '%s' for user '%s': %v", permission, username, err)
		return false
	}
	return checkResult.Can == v1.CheckResult_CHECK_RESULT_ALLOWED
}

In this code snippet:

  • We use the Permify client to perform a permission check, specifying the tenant ID, entity (document), permission, subject (user), and metadata.
  • The access check result indicates whether the action is allowed (CHECK_RESULT_ALLOWED) or denied.
  • Proper error handling ensures that any errors encountered during the access check process are appropriately logged.

By implementing access checks in your application using Permify, you can enforce fine-grained access control policies, ensuring that only authorized users can perform specific actions on protected resources.

Step 5: Run the Server and Handle HTTP Requests

Once the Permify client is initialized, and the schema is defined and written to the Permify service, the next step is to run the web server and handle HTTP requests. This involves setting up HTTP routes, handling user authentication, and performing authorization based on the permissions defined in Permify.

Initializing Routes and HTTP Server

In main.go, we initialize the HTTP routes and start the HTTP server:

func main() {
    setupPermifyClient() // Initialize Permify client and setup schema
    setupRoutes()       // Initialize HTTP routes
    log.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

The setupPermifyClient() function initializes the Permify client and sets up the schema. The setupRoutes() function initializes all the route handlers defined in handlers.go.

Handling HTTP Requests

In handlers.go, we define HTTP request handlers for different routes:

// setupRoutes initializes all the route handlers
func setupRoutes() {
    http.HandleFunc("/", serveHome)      // Handle requests to the home page
    http.HandleFunc("/login", handleLogin) // Handle user login requests
    http.HandleFunc("/protected", serveProtected) // Handle requests to protected content
}
  • serveHome: Handles requests to the home page (/). It serves the login page HTML template.
  • handleLogin: Handles user login requests (/login). It verifies user credentials and sets a session token cookie upon successful login.
  • serveProtected: Handles requests to protected content (/protected). It checks if the user is authenticated and authorized to access the protected content.

Step 6: Running the Application

To run the application:

  1. Ensure your Permify server is running and accessible at the specified endpoint.
  2. Execute the Go application using:
    go run main.go handlers.go permify_client.go
    
  3. Navigate to http://localhost:8080 in your web browser to interact with the application.

go-sdk-permify

Using Other SDKs in Production

While this example uses the Go SDK, Permify supports various SDKs suitable for different programming environments. Select the SDK that best fits your production needs to implement these functionalities seamlessly.

How Can You Save Time and Money with Fine-Grained Access Control?

Fine-Grained Access Control offers a powerful way to secure your enterprise while remaining agile. Here’s how it saves time and money:

  • Reduced Administrative Overhead: Automating access control reduces the need for manual intervention, freeing up your team to focus on other tasks.
  • Lower Risk of Data Breaches: By ensuring only the right people have access, you reduce the potential costs associated with data breaches.
  • Increased Productivity: Employees have the access they need when they need it, without unnecessary barriers.

In conclusion, fine-grained access control is not just about securing your enterprise; it’s about enabling it to move faster, more securely, and more efficiently.

By choosing the right authorization model, leveraging tools like Permify, and understanding the balance between granularity and manageability, you can build a robust access control system that scales with your needs.