Fine-Grained Access Control (FGAC): Comprehensive Guidance
Ege Aytin
Posted on June 12, 2024
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
-
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.
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.
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
, andtime
for context management, logging, and time-related operations, respectively. - We import the Permify SDK packages
v1
andpermify
. - 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
, anddocument
. - Each entity can have relationships with other entities, specified using the
relation
keyword. - We define permissions (
admin
andmember
) for theorganization
entity based on its relationships with other entities. - The
document
entity has actions (edit
andview
) 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:
- Ensure your Permify server is running and accessible at the specified endpoint.
- Execute the Go application using:
go run main.go handlers.go permify_client.go
- Navigate to
http://localhost:8080
in your web browser to interact with the application.
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.
Posted on June 12, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.