ThreeDotsLabs / wild-workouts-go-ddd-example

Go DDD example application. Complete project to show how to apply DDD, Clean Architecture, and CQRS by practical refactoring.
https://threedots.tech
MIT License
5.14k stars 472 forks source link

Conceptualizing policy layer #15

Closed blaggacao closed 3 years ago

blaggacao commented 3 years ago

While thinking about https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/blob/186a2c4a912e485ac7bb4d18c2892df7617e9ec9/internal/trainings/domain/training/repository.go#L16-L27

I realized that user is passed to implement access policy. As a fan of OPA I immediately wondered how an integration would be best devised.

  1. Generically speaking an external agent only can ever interact with the domain through the application service layer.
  2. The necessary metadata to decide upon an access policy:
    • Identifier of the actor
    • eventually an elevation token
    • an instance of the domain entity (to base decisions on domain values)
    • the command to be executed (probably the most important data point to make a policy decision)
  3. Hence, I'm inclined to thing the policy could be well implemented at the application layer and the domain be freed of any policy decision. Although being part of the business logic, it will be flexibly implemented in the policy service (OPA) based on those data points.

I feel this mental model is advantageous, since the domain logic could be implemented free of any policy considerations.

// StartRecordingHandler knows how to start a recording
type StartRecordingHandler struct {
    aggregate domain.Repository
    commMgr domain.CommManager // interface
    policy app.Policy // policy interface implemented at the application, not the domain layer?
}

// Handle starts a recording
func (h StartRecordingHandler) Handle(ctx context.Context, livecallToStartRecordingFor uuid.UUID, userID uuid.UUID, elevationToken string) error {
    err := h.aggregate.Update(ctx, livecallToStartRecordingFor, func(l *livecall.Livecall) error {
        if ok := h.policy.Can(ctx, "StartRecording", userID, elevationToken, *l); !ok {
            return ErrNotAuthorizedToStartRecording
        }
        if err := l.StartRecording(h.commMgr); err != nil {
            return err
        }
        return nil
    })

    if err != nil {
        return ErrUnableToStartRecording
    }

    return nil
}

}
blaggacao commented 3 years ago

I implemented this idea in ddd-gen. I'm going to open a new issue with a couple of ideas while working on that. Closing here.