Authorization
Control access to resources based on user roles and permissions.
Overview
Authorization determines what an authenticated user can do. Common patterns:
- Role-Based Access Control (RBAC) - Users have roles with permissions
- Attribute-Based Access Control (ABAC) - Policies based on attributes
- Resource-Based Access Control - Permissions tied to specific resources
Role-Based Access Control
Define Roles
const (
RoleUser = "user"
RoleEditor = "editor"
RoleAdmin = "admin"
)
type User struct {
ID string `json:"id"`
Email string `json:"email"`
Role string `json:"role"`
}
Role Middleware
type RoleMiddleware struct {
allowedRoles []string
}
func RequireRole(roles ...string) *RoleMiddleware {
return &RoleMiddleware{allowedRoles: roles}
}
func (m *RoleMiddleware) Handle(ctx types.Context, next types.Next) any {
// Get user role from context (set by auth middleware)
userRole := ctx.Get("user_role").(string)
// Check if user has required role
for _, role := range m.allowedRoles {
if userRole == role {
return next()
}
}
return ctx.Status(403).JSON(map[string]string{
"error": "Insufficient permissions",
})
}
Apply to Routes
func (c *AdminController) Routes() types.Routes {
return types.Routes{
// User routes
{Method: "GET", Path: "/profile", Handler: c.Profile,
Middlewares: []any{&AuthMiddleware{}}},
// Editor routes
{Method: "POST", Path: "/posts", Handler: c.CreatePost,
Middlewares: []any{&AuthMiddleware{}, RequireRole("editor", "admin")}},
// Admin only routes
{Method: "GET", Path: "/admin/users", Handler: c.ListUsers,
Middlewares: []any{&AuthMiddleware{}, RequireRole("admin")}},
{Method: "DELETE", Path: "/admin/users/:id", Handler: c.DeleteUser,
Middlewares: []any{&AuthMiddleware{}, RequireRole("admin")}},
}
}
Permission-Based Control
Define Permissions
const (
PermissionReadPosts = "posts:read"
PermissionWritePosts = "posts:write"
PermissionDeletePosts = "posts:delete"
PermissionReadUsers = "users:read"
PermissionWriteUsers = "users:write"
PermissionDeleteUsers = "users:delete"
)
// Role to permissions mapping
var rolePermissions = map[string][]string{
"user": {
PermissionReadPosts,
},
"editor": {
PermissionReadPosts,
PermissionWritePosts,
},
"admin": {
PermissionReadPosts,
PermissionWritePosts,
PermissionDeletePosts,
PermissionReadUsers,
PermissionWriteUsers,
PermissionDeleteUsers,
},
}
Permission Check
func HasPermission(role string, permission string) bool {
permissions, ok := rolePermissions[role]
if !ok {
return false
}
for _, p := range permissions {
if p == permission {
return true
}
}
return false
}
Permission Middleware
type PermissionMiddleware struct {
requiredPermission string
}
func RequirePermission(permission string) *PermissionMiddleware {
return &PermissionMiddleware{requiredPermission: permission}
}
func (m *PermissionMiddleware) Handle(ctx types.Context, next types.Next) any {
userRole := ctx.Get("user_role").(string)
if !HasPermission(userRole, m.requiredPermission) {
return ctx.Status(403).JSON(map[string]string{
"error": "Permission denied",
})
}
return next()
}
Apply Permissions
func (c *PostController) Routes() types.Routes {
return types.Routes{
{Method: "GET", Path: "/posts", Handler: c.Index,
Middlewares: []any{&AuthMiddleware{}, RequirePermission(PermissionReadPosts)}},
{Method: "POST", Path: "/posts", Handler: c.Create,
Middlewares: []any{&AuthMiddleware{}, RequirePermission(PermissionWritePosts)}},
{Method: "DELETE", Path: "/posts/:id", Handler: c.Delete,
Middlewares: []any{&AuthMiddleware{}, RequirePermission(PermissionDeletePosts)}},
}
}
Resource-Based Authorization
Ownership Check
type PostController struct {
postService *PostService `inject:""`
}
func (c *PostController) Update(ctx types.Context) any {
postID := ctx.Param("id")
userID := ctx.Get("user_id").(string)
userRole := ctx.Get("user_role").(string)
// Get the post
post, err := c.postService.GetByID(postID)
if err != nil {
return ctx.Status(404).JSON(map[string]string{
"error": "Post not found",
})
}
// Check ownership or admin
if post.AuthorID != userID && userRole != "admin" {
return ctx.Status(403).JSON(map[string]string{
"error": "You can only edit your own posts",
})
}
// Proceed with update...
var dto UpdatePostDTO
ctx.Bind(&dto)
return c.postService.Update(postID, dto)
}
Resource Authorization Service
type AuthorizationService struct{}
func (s *AuthorizationService) CanEdit(user *User, resource interface{}) bool {
switch r := resource.(type) {
case *Post:
return r.AuthorID == user.ID || user.Role == "admin"
case *Comment:
return r.AuthorID == user.ID || user.Role == "admin" || user.Role == "moderator"
default:
return false
}
}
func (s *AuthorizationService) CanDelete(user *User, resource interface{}) bool {
if user.Role == "admin" {
return true
}
switch r := resource.(type) {
case *Post:
return r.AuthorID == user.ID
case *Comment:
return r.AuthorID == user.ID
default:
return false
}
}
Policy Pattern
Define Policies
type Policy interface {
CanView(user *User, resource interface{}) bool
CanCreate(user *User) bool
CanUpdate(user *User, resource interface{}) bool
CanDelete(user *User, resource interface{}) bool
}
type PostPolicy struct{}
func (p *PostPolicy) CanView(user *User, resource interface{}) bool {
post := resource.(*Post)
// Published posts are viewable by all
if post.Published {
return true
}
// Drafts only by author or admin
return post.AuthorID == user.ID || user.Role == "admin"
}
func (p *PostPolicy) CanCreate(user *User) bool {
return HasPermission(user.Role, PermissionWritePosts)
}
func (p *PostPolicy) CanUpdate(user *User, resource interface{}) bool {
post := resource.(*Post)
return post.AuthorID == user.ID || user.Role == "admin"
}
func (p *PostPolicy) CanDelete(user *User, resource interface{}) bool {
post := resource.(*Post)
return post.AuthorID == user.ID || user.Role == "admin"
}
Use Policies
type PostController struct {
postService *PostService `inject:""`
policy *PostPolicy
}
func (c *PostController) Update(ctx types.Context) any {
user := ctx.Get("user").(*User)
post, _ := c.postService.GetByID(ctx.Param("id"))
if !c.policy.CanUpdate(user, post) {
return ctx.Status(403).JSON(map[string]string{
"error": "Cannot update this post",
})
}
// Proceed...
}
Scoped Queries
Filter data based on user:
type PostService struct {
db *gorm.DB `inject:""`
}
// Get posts user can see
func (s *PostService) GetVisiblePosts(user *User) []Post {
var posts []Post
query := s.db.Model(&Post{})
if user.Role == "admin" {
// Admin sees all
query.Find(&posts)
} else {
// Others see published or own posts
query.Where("published = ? OR author_id = ?", true, user.ID).Find(&posts)
}
return posts
}
Guard Pattern
Group authorization logic:
type AdminGuard struct {
authService *AuthService `inject:""`
}
func (g *AdminGuard) Handle(ctx types.Context, next types.Next) any {
// Check authentication
claims, err := g.authService.ValidateToken(ctx.Header("Authorization"))
if err != nil {
return ctx.Status(401).JSON(map[string]string{"error": "Unauthorized"})
}
// Check admin role
if claims.Role != "admin" {
return ctx.Status(403).JSON(map[string]string{"error": "Admin access required"})
}
ctx.Set("user_id", claims.UserID)
ctx.Set("user_role", claims.Role)
return next()
}
Best Practices
- Fail secure - Deny by default
- Check authorization on every request
- Use middleware for consistent enforcement
- Combine roles with resource ownership
- Log authorization failures
- Test authorization thoroughly
- Keep authorization logic centralized
Common Patterns
Admin Override
func (s *AuthService) CanPerformAction(user *User, action string, resource interface{}) bool {
// Admin can do anything
if user.Role == "admin" {
return true
}
// Check specific permissions
return s.checkPermission(user, action, resource)
}
Hierarchical Roles
var roleHierarchy = map[string]int{
"user": 1,
"editor": 2,
"admin": 3,
}
func HasRoleOrHigher(userRole, requiredRole string) bool {
return roleHierarchy[userRole] >= roleHierarchy[requiredRole]
}
Next Steps
- Authentication - User identity
- Middleware - Request processing
- Guards - Route protection