Gin (Go)
Learn how to leverage the Permit API and SDK to seamlessly integrate robust access control into your Gin application. This step-by-step guide walks you through setting up your environment, creating policies with the Permit CLI with minimal code and maximum flexibility.
In this guide, we will quickly go through the steps to integrate Permit.io with Gin. We will use Permit's CLI to create a new environment and a policy with a template. Then, we will use the Permit SDK to integrate it with our Gin application. We will also set up the PDP (Policy Decision Point) server locally using Docker.
Prerequisites
- Gin (Go) application
- Docker (for running the PDP server)
- Node.js (for installing and running the Permit CLI)
- Permit.io account (Follow this guide to create an account)
Installing the Permit CLI
We will first install the Permit CLI. Permit CLI is a command-line tool that allows you to create policies, run the PDP server, and perform other tasks. To install the CLI, execute the command below in your terminal.
npm install -g @permitio/cli
Once the installation is complete, execute permit
to confirm the installation.
Authenticating the CLI
To use the CLI, we need to authenticate it with our Permit account. To authenticate, execute
permit login
This will open a browser window and ask you to log in with your Permit account. After logging in, the CLI will be authenticated and ready to use. This will also log you into the default environment (a project can have multiple environments). We can change the environment by executing permit env select
and then selecting the environment we want to use.
Creating a policy with a template
Now, let's create a policy with a template. The benefit of using a template is that we can create a policy with predefined roles and resources using best practices. This will save us time and effort in creating a policy from scratch. To check the available templates, execute permit template list
. Based on our needs, we can apply the template to a new policy by executing permit template apply --template <template-name>
.
We will be using the blog-rbac template to create a new policy. This template is great for a blog application. It has a Viewer role that can view the blog posts and a Editor role that can edit the blog posts. It also has a Post resource that represents a blog post.....
To apply the template, execute the command below.
permit template apply --template blog-rbac
This will create a new policy with the blog-rbac template
Accessing the policy
Now, let's visit the Permit dashboard to check the created policy and also get the API key that we will use to authenticate our application when using the Permit SDK.
As we can see in the image below, the CLI has created Resources, Roles, and Policies. Now, let's add Read permission to the Viewer for the Visit resource. It is very easy to add permissions using the dashboard. We can also add more roles and resources as per our needs.
To get the API key, click on Projects from the left sidebar, then click on the three dots on the environment we want to use, and click on Copy API Key and store it in a safe place. We will be using this API key to authenticate our application with Permit.io. If you are having any difficulty getting the API key, follow this guide.
Once we are done, let's move toward the implementation part and integrate Permit.io with our Express.js application.
Gin Integration
Once you have a Gin application ready (or you can create a new one for the sake of this guide), we can integrate Permit.io with it. First, we need to install the Permit's Golang SDK. To install the SDK, execute the below command in your terminal.
go get github.com/permitio/permit-golang
Read more about the Golang SDK here.
Now, below is the application code we created. In this we will be syncing users, creating tenants, and checking access using the Permit SDK.
package main
import (
"context"
"log"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/permitio/permit-golang/pkg/config"
"github.com/permitio/permit-golang/pkg/enforcement"
"github.com/permitio/permit-golang/pkg/models"
"github.com/permitio/permit-golang/pkg/permit"
"go.uber.org/zap"
)
var permitClient *permit.Client
type UserIn struct {
Email string `json:"email" binding:"required,email"`
FirstName string `json:"first_name" binding:"required"`
LastName string `json:"last_name" binding:"required"`
}
type TenantIn struct {
Key string `json:"key" binding:"required"`
Name string `json:"name" binding:"required"`
Description *string `json:"description"` // Optional
}
type PermissionCheck struct {
User string `json:"user" binding:"required"`
Action string `json:"action" binding:"required"`
Resource string `json:"resource" binding:"required"`
}
func main() {
apiKey := os.Getenv("PERMIT_API_KEY")
pdpURL := os.Getenv("PDP_URL")
permitClient = permit.NewPermit(
config.NewConfigBuilder(apiKey).
WithLogger(zap.NewExample()).
WithPdpUrl(pdpURL).
Build(),
)
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Hello, Gin with Permit!"})
})
router.POST("/sync-user", syncUserHandler)
router.POST("/create-tenant", createTenantHandler)
// Add a protected route using the middleware
router.POST("/check-permission", PermissionMiddleware(), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "You have access to the protected resource!"})
})
log.Println("Server running on http://localhost:8000")
router.Run(":8000")
}
func syncUserHandler(c *gin.Context) {
var user UserIn
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
createUser := models.NewUserCreate(user.Email)
createUser.SetFirstName(user.FirstName)
createUser.SetLastName(user.LastName)
createUser.SetEmail(user.Email)
newUser, err := permitClient.Api.Users.SyncUser(context.Background(), *createUser)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync user"})
return
}
if newUser == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "User not found"})
return
} else {
c.JSON(http.StatusOK, newUser)
return
}
}
func createTenantHandler(c *gin.Context) {
var tenant TenantIn
if err := c.ShouldBindJSON(&tenant); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
createTenant := models.NewTenantCreate(tenant.Key, tenant.Name)
createTenant.Description = tenant.Description
createTenant.SetName(tenant.Name)
newTenant, err := permitClient.Api.Tenants.Create(context.Background(), *createTenant)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create tenant"})
return
}
if newTenant == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Tenant not found"})
return
} else {
c.JSON(http.StatusOK, newTenant)
return
}
}
// Middleware for permission check
func PermissionMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
user := c.GetHeader("X-User")
action := c.GetHeader("X-Action")
resource := c.GetHeader("X-Resource")
if user == "" || action == "" || resource == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing permission headers: X-User, X-Action, X-Resource"})
c.Abort()
return
}
enfUser := enforcement.UserBuilder(user).Build()
enfResource := enforcement.ResourceBuilder(resource).Build()
permitted, err := permitClient.Check(enfUser, enforcement.Action(action), enfResource)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Permission check failed"})
c.Abort()
return
}
if !permitted {
c.JSON(http.StatusForbidden, gin.H{"message": "User is not permitted"})
c.Abort()
return
}
c.Next()
}
}
At the top, we have imported the required packages and initialized the Permit SDK with the API key and PDP URL. We have also created a new permit.Client
instance with the API key and PDP URL. We are using the zap
logger for logging.
Then we have defined the UserIn
, TenantIn
, and PermissionCheck
structs to bind the incoming JSON request. The UserIn
struct is used to sync the user with Permit.io, the TenantIn
struct is used to create a new tenant, and the PermissionCheck
struct is used to check the access.
func main() {
apiKey := os.Getenv("PERMIT_API_KEY")
pdpURL := os.Getenv("PDP_URL")
permitClient = permit.NewPermit(
config.NewConfigBuilder(apiKey).
WithLogger(zap.NewExample()).
WithPdpUrl(pdpURL).
Build(),
)
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Hello, Gin with Permit!"})
})
router.POST("/sync-user", syncUserHandler)
router.POST("/create-tenant", createTenantHandler)
// Add a protected route using the middleware
router.POST("/check-permission", PermissionMiddleware(), func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "You have access to the protected resource!"})
})
log.Println("Server running on http://localhost:8000")
router.Run(":8000")
}
In the main
function, we have initialized the Permit SDK that needs two environment variables PERMIT_API_KEY
and PDP_URL
. The PERMIT_API_KEY
is the API key we copied from the Permit dashboard, and the PDP_URL
is the URL of the PDP server. We will set this up later.
Then we have created a new Gin router and defined the routes. We have defined three routes, /sync-user
, /create-tenant
, and /check-permission
. The /sync-user
route is used to sync the user with Permit.io, the /create-tenant
route is used to create a new tenant, and the /check-permission
route is used to check the access to a specific resource. We have also added a middleware to the /check-permission
route to check the access.
func syncUserHandler(c *gin.Context) {
var user UserIn
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
createUser := models.NewUserCreate(user.Email)
createUser.SetFirstName(user.FirstName)
createUser.SetLastName(user.LastName)
createUser.SetEmail(user.Email)
newUser, err := permitClient.Api.Users.SyncUser(context.Background(), *createUser)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to sync user"})
return
}
if newUser == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "User not found"})
return
} else {
c.JSON(http.StatusOK, newUser)
return
}
}
func createTenantHandler(c *gin.Context) {
var tenant TenantIn
if err := c.ShouldBindJSON(&tenant); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
createTenant := models.NewTenantCreate(tenant.Key, tenant.Name)
createTenant.Description = tenant.Description
createTenant.SetName(tenant.Name)
newTenant, err := permitClient.Api.Tenants.Create(context.Background(), *createTenant)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create tenant"})
return
}
if newTenant == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Tenant not found"})
return
} else {
c.JSON(http.StatusOK, newTenant)
return
}
}
In the syncUserHandler
function, we are syncing the user with Permit.io. We are binding the incoming JSON request to the UserIn
struct and then creating a new UserCreate
instance with the user data. We are then calling the SyncUser
method of the permitClient
to sync the user with Permit.io. If the user is synced successfully, we are returning the user data in the response. If there is an error, we are returning the error message in the response.
In the createTenantHandler
function, we are creating a new tenant with Permit.io. We are binding the incoming JSON request to the TenantIn
struct and then creating a new TenantCreate
instance with the tenant data. We are then calling the Create
method of the permitClient
to create the tenant with Permit.io. If the tenant is created successfully, we are returning the tenant data in the response. If there is an error, we are returning the error message in the response.
func PermissionMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
user := c.GetHeader("X-User")
action := c.GetHeader("X-Action")
resource := c.GetHeader("X-Resource")
if user == "" || action == "" || resource == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing permission headers: X-User, X-Action, X-Resource"})
c.Abort()
return
}
enfUser := enforcement.UserBuilder(user).Build()
enfResource := enforcement.ResourceBuilder(resource).Build()
permitted, err := permitClient.Check(enfUser, enforcement.Action(action), enfResource)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Permission check failed"})
c.Abort()
return
}
if !permitted {
c.JSON(http.StatusForbidden, gin.H{"message": "User is not permitted"})
c.Abort()
return
}
c.Next()
}
}
We have taken the middleware approach to check the access. In the PermissionMiddleware
function, we are checking the access to a specific resource. We are getting the user, action, and resource from the request headers. If any of the headers are missing, we are returning a bad request error. Then we are creating a new UserBuilder
and ResourceBuilder
instance with the user and resource data. We are then calling the Check
method of the permitClient
to check the access. If the user is permitted, we are returning a success message in the response. If the user is not permitted, we are returning a forbidden message in the response. We can easily add middleware to any route we want to protect.
Setting up the PDP server
Before we run the application and check out routes and enforcement, we need to set up PDP. To enforce the policy we use Permit's PDP (Policy Decision Point) to check if the user has access to the resource. So first let's set up the PDP.
We can leverage the Permit's CLI to start a PDP server locally. Otherwise, we would need to run long Docker commands to start the PDP server. The PDP server is a service that evaluates access requests and returns a decision (permit or deny) based on the policies defined in Permit.io. To start it, execute the below command in your terminal. Make sure Docker is running on your machine as this will start a PDP container locally.
permit pdp run
Once you execute the above command, you will see an output like below with information like container id, name, etc. It will start a PDP server locally on your machine. You can check the logs to see if the PDP server is running successfully. The PDP server will be running on port 7766
by default. You can also change the port.
Syncing the user
Once the PDP server is running, update the PDP_URL
environment variable in your application. Now, everything is ready, let's restart and test the application and test the sync-user
using curl
.
As we can see in the above image, the user is created successfully and we get a lot of information about the user in the response like, id
, project_id
, key
, etc.
Giving the user access to the resource
Let's now head over to the Permit dashboard and check if the user is created successfully. To do that, select Directory from the left sidebar and we can see the user we just created. Let's give this user access to the resource we created in the policy. To do that, select the user we just created. Then click on the Add Role button and select Tenant and then select the Viewer role and save it. This will give the user access to the Visit resource we created in the policy. We will verify this later when we check the access. Also, we can give the access programmatically, it was a demo to show how easy it is to add access using the dashboard.
We can also add more roles and resources as per our need.
Checking the access
Now, let's check the policy enforcement. To do that, we will use the /check-permission
route we created in the application. We will use curl
to check the access.
As we can see in the image above, the user is permitted to access the resource. Let's remove the access from the dashboard and check again.
Now this time we can see that the user is not permitted to access the resource. This is how we can enforce policies using Permit.io with FastAPI.
Conclusion
In this guide, we have seen how to integrate Permit.io with Gin application. We have created a new policy with a template using the Permit CLI. We have also seen how to sync users and create tenants with Permit.io. We have also set up the PDP server locally using Docker and checked the access using the /check-permission
route. This is how easy it is to integrate Permit.io with Gin application and enforce policies.