Authorizing actions through microservices

A modern multi-user web application imposes many restrictions on the actions that users can take. In other words, actions require authority. For example, a user can only change their personal information, and only members of a group can submit content to that group. In a classic monolithic application, such constraints are easily enforced by combining multiple database tables and acting on the results of the queries. However, with the help of microservices, it becomes less clear where and how such restrictions should be regulated.

For the sake of argument, consider a Facebook clone. The entire application consists of several parts:

  • An interface written in JS and other web technologies.
  • A backend consisting of multiple microservices
  • API for fetching and sending data to the backend i.e. Gateway

In terms of business logic, there are (among others) two well-known objects:

  • Events (like concerts, birthdays, etc.).
  • Messages (text entries existing on walls, pages, events, etc.).

Let's assume that these two objects are managed by separate EventService and PostService. Then consider the following limitation:

A message for an event can be deleted by two types of users: the author of the message and the host (s) of the event.

In a monolith, this limitation would be conceptually very easy to deal with. When prompted to delete a post, supply the post id and user id,

  • Get the event the message belongs to.
  • Check if the user is the author of the post.
  • If so, delete the message. If not, select event hosts.
  • Check if the user is among the hosts.
  • If so, delete the message.

However, with a microservice strategy I am having a hard time figuring out how to separate the responsibilities of such an operation, as it does through services.

Alternative 1

An easy way would be to put logic like this in the gateway. Thus, you can essentially perform the same procedure as described above, but with service calls rather than directly into the database. Rough sketch:

// Given postId and userId
// Synchronous solution for presentational purposes

const post = postClient('GET', `/posts/${postId}`);
const hosts = eventClient('GET', `/events/${post.parentId}/hosts`);
const isHost = hosts.find(host => host.id == userId);

if (isHost) {
    postClient('DELETE', `/posts/${postId}`);
}

      

However, I am not happy with this solution. Once I start injecting logic into the gateway, it will be very tempting to always do this as it is a quick and easy way to get things done. All business logic will eventually go into the gateway, and services will become "dumb" CRUD endpoints. This will lead to victory in the goal of providing separate services with clearly defined areas of responsibility. Also, it can be slow as it can lead to a lot of service calls as operations get more complex.

I would essentially reinvent the monolith, replacing database queries with slow and limited network calls.

Alternative 2

Another option is to allow unrestricted communication between services by letting the PostService simply ask the EventService if the user is the host of the corresponding event before performing the delete. However, I fear that having a potentially large number of microservices communicating with each other could lead to a lot of mix in the long run. Experts generally advise against direct interuniversity communication.

Alternative 3

With a robust event publishing and subscribing system, services can stay up to date with events happening in other services. For example, every time a user is promoted to post to an EventService, the event will be posted (for example events.participant-status-changed, {userId: 14323, eventId: 12321, status: 'host'}

). The PostService can subscribe to the event and remember this fact when a request is received to delete the post.

However, I am not entirely happy with this. This would create a very confusing and error prone system, where an unhandled (but potentially rare) event could cause services to go out of sync. In addition, there is a risk that the logic will end up in the wrong place. For example, the limitation in this question will be handled by PostService, although conceptually it is a property of an event object.

I must emphasize that I am very optimistic about the usefulness of events when implementing applications using microservices. I'm just not sure if they answer this problem.


How would you tackle this hypothetical but realistic challenge?

+3


source to share


2 answers


A message for an event can be deleted by two types of users: the author of the message and the host (s) of the event.

So the first thing I'll start with is to determine where the authority to delete a post is - using as a guiding principle the idea that there should be one writer responsible for maintaining any given invariant.

In this case, it looks like it would be a Post service, which is reasonable enough.

I'm guessing there is some mechanism in plumbing to detect that the user is who they say - the authenticated id is the service login.

In the case where an author deletes a post, it should be trivial to verify that this rule is being followed, since we have the right to create and delete a post in the same place. No cooperation is required here.

So the tricky part is determining if the authenticated id belongs to the event host. Presumably, the authority to determine the event owner lives in the Event service.

Now reality check: if you query the event service to find out who is the owner of the event without holding a lock on that information, then the owner can change concurrently while processing the delete command. In other words, there is a potential data race between the ChangeOwner command and the DeletePost command.

Udi Dahan remarked:



The microsecond timing difference should not affect basic business behavior.

In particular, the correct behavior should not depend on the fact that your message transport system is executing commands.

An event-driven approach is close to this idea.

However, I am not entirely happy with this. This would create a very confusing and error prone system, where an unhandled (but potentially rare) event could cause services to go out of sync. In addition, there is a risk that the logic will end up in the wrong place. For example, the limitation in this question will be handled by PostService, although conceptually it is a property of an event object.

Practically so - the key idea is this: if the Mail service is a proxy for the message life, then it should be allowed to announce that it has changed its mind. You build in your design that the authority makes the best decision with the information it has and applies corrections (sometimes called offsetting events) when new information will invalidate an earlier choice.

So, when the delete command comes in, you check if it is from the host. If so, you can flag the message immediately . If it's not from the host, you remember that someone wanted to delete the message, and if later it turns out that an event update tells you that the same someone is the host, then you can apply the flag.

And the same approach works in the opposite case - the deletion happened from the host, so the message was flagged. Oops! we just found out that the impostor was not the master. OK, so show the message again.

+1


source


Followup:

I have pondered this intensively over the past week, and perhaps I have found a flaw in my reasoning about the problem. I thought of the mail object as being solely on the mail service, but perhaps this is a worrying oversimplification.

What if each service (i.e. bounded context) has its own concept of what mail is and therefore organizes its storage? The event service stores a table of messages posted to events, wall services write messages on the walls, etc.

Such objects would be pretty thin, consisting mostly of GUIDs, the identity of the poster, perhaps its content. They can also contain special attributes that are used only in this context. For example, events, but no other services, can be assigned to messages.

(Nota bene: the term "event" also uses a completely different expression, namely a message that is sent between processes, for example, Apache Kafka, describing something that happened.)

Every time a message is sent to a service, an event is sent to the event bus. For example, when a user logs into an event, the event service creates a mail object and issues events.post-posted {id: ..., authorId: ..., contents: ...}

. Likewise, the wall will place wall.post-posted {id: ..., authorId: ..., receiverId: ..., contents: ...}

.

The messaging service, in turn, listens for all such events. Each time a message is sent to another service, a corresponding mail object is created in the mail service, separating the identifier of the original message. It is a smart message object with all the functions common to messages in the application. This can include sending notifications, organizing streams, detecting abuse, recording, etc.

This means that each service has much more freedom with its mail objects, as they no longer belong to one source of information residing in one service. It allows the gateway to choose one of several methods of receiving mail data, depending on the situation. For example, to tell the UI that a message is pinned, it needs to talk to the event service, but to get the text content, it might need to talk to the mail service. Perhaps the post office in the wall service has a special facility to handle birthday wishes posted on peoples walls.



Going back to the original question: This removes the need for service communication when dealing with event deletion. Instead of deleting the mail service, the event service job receives requests to delete messages. Since he has information about the author of the message and the host of the event, he can make his own decision.

Criticism of this idea :

While I feel like I'm on the right track here, I have two main problems.

The first is that this clearly does not quite answer the original question of how to deal with authority when deploying a web application using microservices. Perhaps this is just an answer to one hypothetical scenario, while other minor variations of the problem are not mitigated at all by this approach.

My second concern is that I fall into the passive-aggressive trap of events . I describe what happens as a series of events, but maybe I am actually issuing commands? After all, the reason for posting an event event.post-posted

is to trigger the creation of an event in the mail service. On the other hand, the application will not break if these events are not listened to; events will have really dry posts.


What are your thoughts on this approach?

0


source







All Articles