Implementing ACL (Access Control List) with Domain-Driven Design in Golang

Implementing Access Control Lists (ACL) using DDD principles in Go can help build secure applications. This post explores the basics of ACL and provides a practical example of its implementation.

Implementing ACL (Access Control List) with Domain-Driven Design in Golang
Implementing ACL (Access Control List) with Domain-Driven Design in Golang

Introduction

Implementing Access Control Lists (ACL) is an essential aspect of application security. In this blog post, we'll explore how to implement ACL using Domain-Driven Design (DDD) principles in Go (Golang). We'll cover the basic concepts of ACL, understand how it fits into the DDD architecture, and provide practical examples to help you implement ACL in your own projects. Let's dive in!

What is an Access Control List?

An Access Control List (ACL) is a security mechanism that defines permissions for users or groups to access certain resources or perform specific actions. It provides fine-grained control over who can access what in an application.

ACLs typically consist of a list of permissions and associated roles or users. The permissions can be categorized as either allow or deny, and they determine whether a user or role is granted or denied access to a specific resource.

Domain-Driven Design and Access Control

Domain-Driven Design (DDD) is an architectural approach that focuses on modeling complex business domains. It promotes the separation of concerns by dividing the application into different layers, such as the presentation layer, domain layer, and infrastructure layer.

When implementing ACL in a DDD application, we typically tie the access control logic to the domain layer. This ensures that the access rules and permissions are closely aligned with the business logic, making it easier to understand and maintain the application.

The following are the key components of implementing ACL with DDD:

1. Entities

The entities in DDD represent the core business concepts of your application. When implementing ACL, entities can represent resources that need access controls, such as a User, Document, or Product.

2. Value Objects

Value objects in DDD are immutable objects that encapsulate attributes. In the context of ACL, value objects can represent the permissions associated with a role or user. For example, a Permission value object can contain attributes like read, write, delete, etc.

3. Aggregates

In DDD, aggregates are clusters of related entities and value objects that are treated as a single unit for consistency and transactional operations. For ACL implementation, aggregates can represent the access control rules for a specific resource.

4. Repositories

Repositories in DDD are responsible for persisting and retrieving entities and aggregates. In the context of ACL, repositories can be used to store and retrieve access control rules and permissions.

Implementing Access Control List with Domain-Driven Design in Golang

Let's now see how we can implement ACL using DDD in Go. We'll walk through a practical example to demonstrate the concepts.

Step 1: Define the Basic Entities and Value Objects

First, let's define the basic entities and value objects for our ACL implementation. Assume we have a User entity, a Document entity, and a Permission value object like the following:

type User struct {
    ID   int
    Name string
}

type Document struct {
    ID      int
    Content string
}

type Permission struct {
    Read   bool
    Write  bool
    Delete bool
}

Step 2: Create the Access Control Aggregate

Next, let's create the access control aggregate that defines the access control rules for a specific resource. We'll define an AccessControl aggregate with the necessary methods to manage permissions.

type AccessControl struct {
    resourceID    int
    resourceType  string
    rules         map[string]Permission
}

func NewAccessControl(resourceID int, resourceType string) *AccessControl {
    return &AccessControl{
        resourceID:   resourceID,
        resourceType: resourceType,
        rules:        make(map[string]Permission),
    }
}

func (ac *AccessControl) AddRule(role string, permission Permission) {
    ac.rules[role] = permission
}

func (ac *AccessControl) RemoveRule(role string) {
    delete(ac.rules, role)
}

func (ac *AccessControl) GetPermission(role string) (Permission, bool) {
    permission, ok := ac.rules[role]
    return permission, ok
}

Step 3: Define Repositories

Now, let's define repositories to store and retrieve the access control aggregates. We'll create an AccessControlRepository interface and an in-memory implementation for simplicity:

type AccessControlRepository interface {
    Save(accessControl *AccessControl)
    Get(resourceID int, resourceType string) (*AccessControl, error)
}

type InMemoryAccessControlRepository struct {
    accessControls map[string]*AccessControl
}

func NewInMemoryAccessControlRepository() *InMemoryAccessControlRepository {
    return &InMemoryAccessControlRepository{
        accessControls: make(map[string]*AccessControl),
    }
}

func (r *InMemoryAccessControlRepository) Save(accessControl *AccessControl) {
    key := fmt.Sprintf("%s:%d", accessControl.resourceType, accessControl.resourceID)
    r.accessControls[key] = accessControl
}

func (r *InMemoryAccessControlRepository) Get(resourceID int, resourceType string) (*AccessControl, error) {
    key := fmt.Sprintf("%s:%d", resourceType, resourceID)
    accessControl, ok := r.accessControls[key]
    if !ok {
        return nil, errors.New("access control not found")
    }
    return accessControl, nil
}

Step 4: Implement Access Control Logic

With the entities, value objects, and repositories in place, we can now implement the access control logic in our application. Here's an example of how to use the AccessControl aggregate and repository:

// Create a new access control for a document
accessControl := NewAccessControl(documentID, "document")

// Define the access control rules
accessControl.AddRule("admin", Permission{Read: true, Write: true, Delete: true})
accessControl.AddRule("user", Permission{Read: true, Write: false, Delete: false})

// Save the access control to the repository
accessControlRepository := NewInMemoryAccessControlRepository()
accessControlRepository.Save(accessControl)

// Get the access control for a document
retrievedAccessControl, err := accessControlRepository.Get(documentID, "document")
if err != nil {
    log.Fatalf("Access control not found: %s", err)
}

// Get the permission for a user or role
permission, ok := retrievedAccessControl.GetPermission("admin")
if ok {
    fmt.Printf("Admin has permission: %+v\n", permission)
}

Conclusion

Implementing Access Control Lists (ACL) using Domain-Driven Design (DDD) principles in Go can help you build secure and maintainable applications. By aligning the access control logic with your domain models, you can ensure that the permissions are consistent with your business requirements. In this blog post, we covered the basics of ACL, how it fits into the DDD architecture, and provided a practical example of implementing ACL in Go. I hope you found this post informative and useful for your own projects. Happy coding!