Skip to main content

Externalize authorization in a Go Gin service so policy changes don't need a redeploy

Short answer: To externalize authorization in a Go service, call permitClient.Check() from Gin middleware and keep the rules in Permit, so a policy change needs no rebuild or redeploy. Use it when roles and permissions change more often than your releases. Don't use it when a few fixed role checks in one binary cover your needs.

FactValue
Question this page answersHow do I externalize authorization in Gin so policy changes don't need a redeploy?
Where the check runsthe middleware on POST /posts, which calls permitClient.Check().
SDKGo SDK, github.com/permitio/permit-golang. The SDK feature parity page lists version v1.2.8. See Check permissions with the Go SDK.
PDPA container PDP at http://localhost:7766 in this tutorial, or the Cloud PDP. See PDP overview.
Models usedRBAC for the Author and Reader roles. ReBAC and ABAC are also supported.
Policy changesYou change roles and permissions in the Permit dashboard. The application code and the deployment stay the same.
Free tierCommunity plan, labeled "Free Forever" on the Permit.io pricing page: MAU 1000, Tenants 20, Authorization Queries No Limit, Environments 3, PDP Instances No Limit.
AuditEach check appears on the Audit Log screen of the Permit dashboard.
Open sourceThe Edge PDP (permitio/PDP), OPAL, cedar-agent, the Permit SDKs, and the Permit CLI are open source. See Open-source fallback.

Build a Gin API in Go for a blogging platform that registers users in Permit.io and allows only users with the Author role to create posts. This tutorial is for Go backend developers who want to enforce Permit.io policies from Gin routes and middleware.

When you finish, your Gin app has two endpoints:

EndpointWhat it does
POST /registerSyncs a user to Permit.io and assigns the user the Reader role in the default tenant
POST /postsRuns middleware that calls permitClient.Check() and returns 403 unless the user in the X-User header has permission to create a Post

Prerequisites​

1. Configure the policy in Permit​

Create the blogging platform policy with the Permit CLI. If your environment already has a policy with a Post resource and an Author role that can create posts, skip to 2. Get your API key.

1

Install the Permit CLI​

The Permit CLI creates policies and runs the PDP from your terminal. Install the CLI with npm:

npm install -g @permitio/cli

Run permit to confirm that the CLI is installed.

2

Sign in with the Permit CLI​

Authenticate the CLI with your Permit.io account:

permit login

The command opens a browser window where you sign in. After you sign in, the CLI uses your default environment. To use a different environment, run permit env select and choose the environment.

3

Apply the blogging platform template​

Permit CLI templates create a policy with predefined resources, roles, and rules. To see the available templates, run permit env template list. The template source files are in the Permit CLI repository.

Terminal output of the Permit CLI template list command showing the available policy templates

Apply the blogging-platform template to your environment:

permit env template apply --template blogging-platform

The CLI prints a success message when the template is applied.

4

Review the policy in the Policy Editor​

In the Permit dashboard, select your project and open the Policy screen.

Permit Policy Editor showing the Post and Comment resources with permissions for the Admin, Reader, Author, and Premium Reader roles

The blogging-platform template creates:

Policy elementWhat the template defines
ResourcesPost (with a premium boolean attribute) and Comment, each with create, read, update, and delete actions
RolesAdmin (all actions), Author (create and read posts, read comments), Reader (create and read comments), and Premium Reader (read posts and comments)
RelationshipA Post is the parent of its Comment instances. An Author of a post instance becomes a Moderator of the comments on that post. This rule is relationship-based access control (ReBAC).
Resource setFree Post contains posts where premium is false. Readers can read free posts. This rule is attribute-based access control (ABAC).

This tutorial uses one rule from the policy: the Author role can create a Post, and the Reader role cannot. To change which role can perform an action, check or clear the box in the Policy Editor.

2. Get your API key​

Your Gin app and the PDP authenticate with Permit.io with your environment API key. Copy the API key of the environment where you applied the template. See Get your API key.

Keep the API key out of your code

Anyone with the environment API key can change that environment's policy through the Permit API. Load the key from an environment variable, and don't commit it.

3. Run the PDP​

The PDP evaluates each permission check against your policy. Start a PDP container with the Permit CLI:

permit pdp run

The command starts the PDP in Docker and prints the container ID and name. The PDP listens on port 7766, so your app connects to it at http://localhost:7766.

Terminal output of permit pdp run showing the PDP container details

The Free Post resource set is an ABAC rule, and the Cloud PDP doesn't evaluate ABAC rules, so run the container PDP for this policy. To run the container with docker run instead, or to check that the PDP is healthy, see Run the PDP.

4. Build the Gin app​

All the Go code in this section goes in one file, main.go, in your Go module.

1

Install the Go SDK​

In your Go module directory, install the Permit Go SDK:

go get github.com/permitio/permit-golang

The code in this tutorial also imports Gin (github.com/gin-gonic/gin), zap (go.uber.org/zap), and godotenv (github.com/joho/godotenv). After you add the code, run go mod tidy to download these modules. For all SDK options, see Check permissions with the Go SDK.

2

Declare the imports and the Permit client variable​

Add the imports, a package-level permitClient variable, and the UserIn struct to main.go:

package main

import (
"context"
"log"
"net/http"
"os"

"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"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

// struct for user input
type UserIn struct {
Email string `json:"email" binding:"required,email"`
FirstName string `json:"first_name" binding:"required"`
LastName string `json:"last_name" binding:"required"`
}

The UserIn struct binds the JSON body of a POST /register request. Gin's binding tags reject a request without a valid email, first_name, or last_name.

3

Initialize the Permit client and define the routes​

Add the main function to main.go:

// main.go
func main() {
_ = godotenv.Load()

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()

// health check endpoint
router.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Hello, Gin with Permit!"})
})

// register new user
router.POST("/register", registerUserHandler)

// protected endpoint: only authors can create posts
router.POST("/posts", CreatePostMiddleware(), func(c *gin.Context) {
c.JSON(http.StatusCreated, gin.H{
"message": "Post created successfully",
})
})

log.Println("Server running on http://localhost:8000")
router.Run(":8000")
}

The main function loads a .env file with godotenv, if one exists, and creates the Permit client from two environment variables:

VariableValue
PERMIT_API_KEYYour environment API key from 2. Get your API key
PDP_URLThe PDP address from 3. Run the PDP: http://localhost:7766

The function then registers three routes on port 8000:

RouteHandler
GET /Returns a health message
POST /registerregisterUserHandler
POST /postsCreatePostMiddleware(), then a handler that returns 201 with "Post created successfully"
4

Add the /register handler​

Add registerUserHandler to main.go. The handler syncs the user to Permit.io with permitClient.Api.Users.SyncUser(), then assigns the user the Reader role in the default tenant with permitClient.Api.Users.AssignRole().

// handler for /register endpoint
func registerUserHandler(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
}

// Assign the Reader role to the user in the 'default' tenant
roleAssignment, err := permitClient.Api.Users.AssignRole(context.Background(), user.Email, "Reader", "default")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to assign role"})
return
}

// ... proceed with your app's registration logic
// For example, create a user record in your database here

c.JSON(http.StatusCreated, gin.H{
"message": "User registered and role assigned",
"user": newUser,
"role_assignment": roleAssignment,
})
}

The user's email address is the user key in Permit.io. The /posts middleware passes the same key to permitClient.Check().

5

Protect the /posts route with middleware​

Add CreatePostMiddleware to main.go. The middleware reads the user key from the X-User header and asks the PDP whether that user can create a Post:

// middleware for checking if the user can create a post
func CreatePostMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
user := c.GetHeader("X-User")
if user == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Missing permission header: X-User"})
c.Abort()
return
}

action := "create"
resource := "Post"

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": "You are not authorized to create a post"})
c.Abort()
return
}
c.Next()
}
}

The middleware responds as follows:

ConditionResponse
The X-User header is missing400 with "Missing permission header: X-User"
The PDP call fails500 with "Permission check failed"
The PDP denies the request403 with "You are not authorized to create a post"
The PDP allows the requestGin runs the /posts handler, which returns 201

To protect other routes, such as commenting or editing, change the action and resource values.

note

In a production app, take the user key from your authenticated session. This example reads the user key from the X-User header so that you can test the route with curl.

6

Start the Gin app​

Set the environment variables and start the app, replacing <YOUR_API_KEY> with your API key:

export PERMIT_API_KEY=<YOUR_API_KEY>
export PDP_URL=http://localhost:7766
go run main.go

The app listens on http://localhost:8000.

5. Test the permission check​

Register two users, give one of them the Author role, and confirm that the PDP allows only that user to create a post.

1

Register two users​

In a second terminal, register John and Emma:

curl -X POST http://localhost:8000/register \
-H "Content-Type: application/json" \
-d '{"email": "john@example.com", "first_name": "John", "last_name": "Doe"}'

curl -X POST http://localhost:8000/register \
-H "Content-Type: application/json" \
-d '{"email": "emma@example.com", "first_name": "Emma", "last_name": "Den"}'

Each request returns HTTP 201 with "message": "User registered and role assigned", the synced user under user, and the role assignment under role_assignment, with "role": "Reader" and "tenant": "default".

2

Assign John the Author role​

Both users have the Reader role, which can't create posts. Give John the Author role in the Permit dashboard:

  1. Open the Directory screen and select john@example.com to open the Edit User panel.
  2. Under Permissions Per Tenant, select the Default Tenant.
  3. In Top Level Access, add the Author role.
  4. Click Save.

Edit User panel in the Permit Directory with Reader and Author roles under Top Level Access for john@example.com

For other ways to assign roles, including the API and SDK, see Sync users.

3

Check that John can create a post and Emma can't​

Send a POST /posts request for John:

curl -X POST http://localhost:8000/posts \
-H "X-User: john@example.com"

The PDP allows the request because John has the Author role. The app returns HTTP 201 with {"message":"Post created successfully"}.

Send the same request for Emma:

curl -X POST http://localhost:8000/posts \
-H "X-User: emma@example.com"

The PDP denies the request because Emma has only the Reader role. The app returns HTTP 403 with {"message":"You are not authorized to create a post"}.

Each check also appears in the Audit Log screen of the Permit dashboard, with the user, action, resource, and decision.

6. Change the policy without redeploying​

Your Gin code contains the enforcement point: the middleware on POST /posts, which calls permitClient.Check(). The rule about which role may create a post is policy in Permit, not code in your app. Change the rule and watch the app respond without rebuilding or restarting the Go binary.

  1. Keep the app and the PDP running, with John's Author role from 5. Test the permission check.
  2. In the Permit dashboard, open the Policy screen. Clear the box for the create action on Post in the Author role column.
  3. Send John's POST /posts request from step 5 again. The PDP denies it, and the app returns the same 403 response that Emma received.
  4. Check the box again and send the request once more. The app returns the successful response again.

Updates reach a PDP in the background, so repeat the request if you still see the previous result. The change doesn't appear in a pull request or a build, because it is a policy change and not a code change. To review policy changes like code, see GitOps.

Go and policy as code​

A Go service doesn't embed a policy engine when you use Permit. The Go SDK sends each permitClient.Check() call to the policy decision point (PDP), a separate container that evaluates your policy. Permit generates the policy code from the Policy Editor, the API, an SDK, or Terraform, in Rego for Open Policy Agent (OPA) or in Cedar. Your Go code names the user, the action, and the resource, and doesn't contain role logic. See Policy engines in Permit.

If you want to write the policy yourself, you can manage custom Rego or Cedar in Git with GitOps and keep using permitClient.Check() in Go. See Write custom policy with GitOps.

Use Permit whenUse a policy engine you run yourself when
Several services, possibly in different languages, share the rules.One Go service owns all its rules.
People who don't write Go change access, or you need a dashboard, audit log, and user management.Your team writes and operates every policy and data update as code.
You want managed policy distribution to PDPs.You want the engine inside your own process and release pipeline.

For the open-source exit path, see Open-source fallback.

Frequently asked questions​

How do I externalize authorization in Gin so policy changes don't need a redeploy?​

Keep the enforcement point in your code and move the rules to Permit. Your handler asks the PDP whether a user can perform an action on a resource, using the Go SDK, and the PDP answers from the policy you edit in the Permit dashboard. Section 6 changes a rule and sends the same request again, with no code change.

Does a role or permission change require rebuilding or restarting the Go binary?​

No. Roles, permissions, and role assignments live in Permit. The PDP receives updates in the background, so the next check uses the updated policy.

Where does the authorization decision run?​

In the PDP, a separate service that your app calls over the network. This tutorial runs the PDP as a container at http://localhost:7766. The Cloud PDP is a managed alternative that supports RBAC and ReBAC. See Cloud PDP capabilities.

When should I keep authorization in Gin code instead?​

When a small set of fixed roles lives in one application and changes only with a code release, checks in the application are enough. Use Permit.io when roles change more often than releases, several services share the rules, or non-developers manage access.

Which Permit SDK does this tutorial use?​

Go SDK, github.com/permitio/permit-golang. The SDK feature parity page lists version v1.2.8. See Check permissions with the Go SDK.

What does the free tier include?​

The Community plan on the Permit.io pricing page lists MAU 1000, Tenants 20, Authorization Queries No Limit, and PDP Instances No Limit.

Should I use OPA, Cedar, OpenFGA, or Permit for policy as code in Go?​

Use an engine you run yourself when one team writes and operates all policy as code. Use Permit when you want a managed control plane with a Policy Editor, an audit log, and PDPs that receive policy updates, while still being able to write custom Rego or Cedar in Git. Permit generates Rego or Cedar for its own PDP, so the choice is mainly about who operates the engine and manages the data.

Next steps​