NilCoalescing / djangochannelsrestframework

A Rest-framework for websockets using Django channels-v4
https://djangochannelsrestframework.readthedocs.io/en/latest/
MIT License
603 stars 84 forks source link

Object level permission #178

Closed oasisMystre closed 1 year ago

oasisMystre commented 1 year ago

Support object level permissions, model_observer permissions flow is unclear.

hishnash commented 1 year ago

Can you provide an example of what you're expecting here.

Any solution for this needs to not add extra un-expected db access.

One important factor to keep in mind with the subscription model when using Channels is that when the object is being changed it is impossible to who/if anyone is subscribed. All we can do is send a notification over one (or more) channel groups. And the subscribers and subscribe to these groups.

Default behaviour by using the ObserverModelInstanceMixin is to subscribe to a group for that instances id.

If however you want to subscribe to multiple instances at once (or not subscribe by id but rather by some other attribute) then the best way to do this is construct a channel group from that. A given DB record when changed then might send a message over multiple groups, one group for each permission group.

Eg if you have a social network users might want to subscribe to given hashtags. The Post record when saved sends N messages were N is the number of distinct hashtags in the message. The consumer then lets the user subscribe to the groups/hashtags they want.

Do you have an example DB relationship and the permissions model in mind you can share?

oasisMystre commented 1 year ago

What about if I have a Message model

class Message:
    pass 

Then I want to observe the message model, but want to send / emit event to only user that have access to Message e.g sender = scope.user or received = scope.user

I have a fix, but I don't think it work

def model_observer(message, scope, observer: ModelOberserver):
     user = scope.get("user")
     # this is what I met by object level permission 
     if observer.instance.sender == user or observer.instance.receiver == user:
           self.send_json(message)

And also object level permissions for viewsets

hishnash commented 1 year ago

In that situation for channels the best way to do this is to send the event that a model has changed over a channel based on the senders and receivers pk.


class MessageConsumer(GenericAsyncAPIConsumer):
    ...

    @model_observer(models.Message)
    async def post_change_handler(self, message, observer=None, **kwargs):
        # called when a subscribed item changes
        await self.send_json(message)

    @post_change_handler.groups_for_signal
    def post_change_handler(self, instance: models.Post, **kwargs):
        # DO NOT DO DATABASE QURIES HERE
        # This is called very often through the lifecycle of every instance of a Message model
        yield f'-sender-{instance.sender__pk}'
        yield f'-receiver-{instance.receiver__pk}'

    @post_change_handler.groups_for_consumer
    def post_change_handler(self, list=False, **kwargs):
        # This is called when you subscribe/unsubscribe
        user = self.scope.get("user")
        yield f'-sender-{user.pk}'
        yield f'-receiver-{user.pk}'

    @action()
    async def subscribe(self, **kwargs):
        await self.post_change_handler.subscribe()
        return {}, 201

    @action()
    async def unsubscribe(self, hashtag, **kwargs):
        await self.post_change_handler.unsubscribe()
        return {}, 204

This is adapted from the post here https://nilcoalescing.com/blog/BuildingARealtimeSocialNetworkUsingDjangoChannels/

The idea here is when changes are made to the Message Model 2 channels are notified of those changes, one for the sender and one for the receiver.

Then on the consumer side the user can subscribe to both of these channels based on thier pk. This way they will not get any messages that do not related to them.

oasisMystre commented 1 year ago

Okay I get now, thanks