socketio / socket.io

Realtime application framework (Node.JS server)
https://socket.io
MIT License
61.08k stars 10.11k forks source link

Authorize/Block events depending on users permissions #4712

Open khelf opened 1 year ago

khelf commented 1 year ago

Suppose we have many clients connected, but each one has a set of permissions, how can we block/allow the execution of of all connected listeners without adding the code to check authorizations in each event handler. something that maybe close to what I am talking about is prependAnyOutgoing. I think something like an express middle ware would be very useful in lot of situations, it should provide the option to intercept all events and can block other listener or update data before it get sent to an event handler.

notifyOutgoingListeners is making a copy of all _anyOutgoingListeners before calling them one by one and it is using an iterator, so it is not wise or possible to override the connected events and set them again to the eventName via a setTimeout for example.


  /**
   * Notify the listeners for each packet sent (emit or broadcast)
   *
   * @param packet
   *
   * @private
   */
  private notifyOutgoingListeners(packet: Packet) {
    if (this._anyOutgoingListeners && this._anyOutgoingListeners.length) {
      const listeners = this._anyOutgoingListeners.slice();
      for (const listener of listeners) {
        listener.apply(this, packet.data);
      }
    }
  }

The only way to do what I am looking for was to override the socket.on function before setting any event handler.

const eventsRequiredPrivileges = {
    event_a: [permissions.event_a],
    event_b: [permissions.event_a],
}
// save the original socket.on function
const socketOn = socket.on;
// override socket.on
socket.on = function (eventName) {
    const notifyNotAutorized = () => {
        workspace.to(userID).emit("notification", { error: "not authorized" });
    }
    const args = [...arguments];
    // permissions map can change in real time (for example the admin change the authorizations of the user)
    if (!isAutorized(eventsRequiredPrivileges[eventName], permissionsMap)) {
        // override the event handler code with notifyNotAutorized
        // this will get called when the action is performed and not when we set the event 
        args.splice(1, 1, notifyNotAutorized);
    }
    socketOn.apply(this, args)
}
socket.on(event_a, () => {
    // do something that require permissions.event_a
});

socket.on(event_b, () => {
    // do something that require permissions.event_b
});

Without this we would have to write in each event handler

socket.on(event_a, () => {
    if (!isAutorized(eventsRequiredPrivileges[event_a], permissionsMap)) {
         return workspace.to(userID).emit("notification", { error: "not authorized" });
    }
    // do something that require permissions.event_a
});

The motivation behind doing the first way is that the possibility to manage all authorizations from single location to make the code more manageable and avoid duplication.

I would love to see this functionality implemented but I am not sure how it should look but I know for sure that it would be very useful in a lot of use cases.

uncaught commented 1 year ago

I've opened an issue about pretty much the same thing two days before you ;)

https://github.com/socketio/socket.io/issues/4709

I was entertaining the idea to have a middleware at the very place where the individual clients are sent their packet. I didn't find any listeners that would do that on a per-client-basis.

khelf commented 1 year ago

Hello, Yes I noticed before creating this one, I am not sure if we are looking for the same thing, I think you are looking for a way to prevent sending sensitive data based on their permissions "per-socket basis" where in my case it was per permissions (that can change in real-time). So in my case permissions can change after the socket is created, I am not sure if I got what you are looking for exactly but it could be solved via rooms, where in my case I have to change the socket room if permissions changes. From how I can see it, they look similar, but my problem is more general, and yours is a case when nothing changes. why does it matter? because the express middleware runs only once and can be used to do this kind of checking and register the user to only the events where they are authorized to see. We faced something like what you are trying to solve, we used namespaces, and after finding out what kind of user is connected, we disconnect it and connect it to a namespace where he will see only what he is supposed to see. I think there should be a way to solve both cases, and I am not sure if it is just a duplication of your case, please let me know if you are still thinking it is the same and why.

uncaught commented 1 year ago

Okay, I see. I thought our problems were more alike because I assumed your permissions would eventually have to come from a user, which means a socket. So I thought if you emitted an event to a room with multiple users, you had the same problem I have, to filter or modify the event or data for each user (aka socket).

darrachequesne commented 1 year ago

Hi! You can use socket.use() to filter incoming packets:

io.on("connection", (socket) => {
  socket.use(([event, ...args], next) => {
    if (isAuthorized(event, socket.data)) {
      next();
    } else {
      // do something, maybe close the connection
    }
  });
});

Reference: https://socket.io/docs/v4/server-api/#socketusefn

Do you need something like this for outgoing packets too?

khelf commented 1 year ago

Thank you, that's what I was looking for. "Do you need something like this for outgoing packets too?" for now, no, but I think if would be helpful to leave a comment if someone need that in the future. Best regards.

madarco commented 4 weeks ago

Hi, I might need something for outgoing packets actually, is there any way to block clients to join a room or receive messages?

In our case we were able to prevent users from connecting if the JWT is not valid, and also to send messages to specific rooms if they don't have permissions, but couldn't find a way to prevent them to listen.

Eg if I emit("/types/123/status") only users allowed for 123 should be able to receive it