This topic concerns how to implement business constraints that look like authorization constraints, i.e. user must be a specific business member X of a specific business domain instance Y to complete action Z scenario.
Suppose we have a simple OrderAcceptance
saga that processes either an AcceptOrder
command or DeclineOrder
command, both of which only supply an OrderId
property. Each command handler in the saga publishes an appropriate event and completes the saga. The saga is created by an OrderPlaced
event that supplies some context state for the saga to store as SagaData, such as OrderId
and SellerId
. Just for clarification, a saga is appropriate because a timeout may need to be implemented.
Let’s say we have the following requirement:
Only SellerOperators can accept or decline their seller’s orders for fulfillment.
Firstly, let’s discuss who owns the constraint relationship information in that statement.
The statement appears at first to be an authorization constraint and therefore an ITOPS responsibility, i.e. do some role check at the edge of the system before sending the command to NSB. However, the term “their seller’s orders” implies a business role constraint that is specific to a seller instance.
We know we need to model and maintain a SellerOperators
list of UserIds
related to SellerId
. Is this model owned by ITOPS or a business service?
If the SellerOperator
is a concept in the specific business service so not an ITOPS concept then it should be modelled explicitly in the business domain.
If the responsibility lays with ITOPS we could imagine a generic scoped authorization implementation that just supports mapping a string-ified RoleName
to a UserId
and a ScopeId
, where RoleName
is any string a business service requires and ScopeId
is the instance Id of a business service’s domain concept.
I’d be interested to hear if others favour an ITOPS implementation or a specific business one instead.
Secondly, let’s discuss how the constraint is enforced in the system. I’ll propose some possible implemenations:
-
Should each of the saga’s command handlers do a request/response to some
RequestSellerOperatorVerification
handler that checks if the user (assuming we’ve propogatedUserId
via message headers) is aSellerOperator
for the given SagaDataSellerId
? I’m assuming stateless handler here so it can inject code to access DB or API to obtain data it needs (theSellerOperators
list).This feels more aligned to the business process however complicates the process of handling responses for each individual command.
For example, the
RequestSellerOperatorVerification
message can be sent from bothAcceptOrder
andDeclineOrder
command handlers in the saga. TheRequestSellerOperatorVerificationResponse
handler in the saga needs to know which of the original command handlers sent its request so that it can reply with eitherAcceptOrderResponse
orDeclineOrderResponse
as appropriate, assuming we wanted to send responses back via say SignalR.We could pass in some string context into the
RequestSellerOperatorVerification
request, a header perhaps, that the response handler can use to determine its next course of action. Just feels a bit wrong though. -
Instead of no. 2 approach, should we assume that the saga is for business orchestration only and these type of constraints should be done at the edge always by some infrastructure code?
For example, assuming we have some server-side viewmodel composition AC in the same business service. Web request for
/orders/{id}/seller/accept
is handled in AC. It loads relevant order and obtains the SellerId from a read model (can’t query saga). Calls some ITOPS service interface to authorize current user against given SellerId and SellerOperator scope.This approach feels like it’s pushing these business “membership” constraints out of the business services where I think they belong. It also feels wrong to be querying for data prior to sending the command. This would be similar to when command data validation does more than just checking data constraints, like the check-for-unique-value in the domain scenarios.
-
A slight variation to no. 3 approach that can avoid the lookup from a read model is to modify the url to include the
SellerId
, e.g./sellers/{sellerId}/orders/{orderId}/accept
. The viewmodel AC handler would not lookup an order but only enforce the constraint check now that it has aSellerId
.The commands need to change to now include a
SellerId
. The command handlers in the saga check both the message’sOrderId
andSellerId
values against the saga data and if they don’t both match the handler returns without modifying saga state.In essence this is policy-based authorization at the edge, maybe implemented using AspNetCore AuthorizationPolicy.
I’d be interested to hear people’s thoughts on this subject.