Add attribute check to db call


#1

In my application I was using a transaction function like this to delete an entity:
(d/transact conn [[:db.fn/retractEntity id]]).

I decided to add a check that the given entity also has the same user ID attribute that’s coming from a token in the incoming request, to mitigate that someone could potentially try to just a lot of requests with different IDs and sabotage for other users. Currently solved by doing this:

(let [{:keys [user-id]} token
       entity-user-id (-> (d/pull (d/db conn) '[*] entity-id) :notebook/user :db/id)]                                                                                                                              

  (when (= user-id entity-user-id)                                                                                                                                                                         
     (d/transact conn [[:db.fn/retractEntity id]]))))})

I’m guessing this requires two database calls and since I’m using this pattern in all mutations now, I was wondering if anyone knows if there is a simpler/better way to do that in just one db call? I read through the documentation but couldn’t find anything.

In SQL I’d do this with:
DELETE FROM X WHERE id = y AND userId = z;

Best regards,


#2

It seems like it should be part of your authorization mechanism in your application. Could you use filters to restrict a database to the entities “owned” by a user? You would first lookup the entity id in this filtered database and fail if you can’t find it.


#3

Are you using Datomic Cloud or Datomic On-Prem?

If you’re using the Peer library with Datomic On-Prem, the n+1 roundtrip issue is not generally a problem, since much of the work you’re performing is happening locally on the peer (and is likely in cache).

If you’re using Client you could use a compare-and-swap on a “permission” attribute (perhaps with an external user ID value). If the cas fails, the entire transaction will fail.


#4

Thanks for the replies. I like both suggestions with filters and cas, I think I’d like to try cas. Am I understanding this correctly: I’d do two updates with every update, one being to set the user ID of an attribute to the same user ID again it already has like this:

[[:db/cas entity-id :notebook/user real-user-ID user-id-from-token]
[:db/retractEntity entity-ID]]]

And these two would be done in a transaction. So if the :db/cas fails because the user-id-from-token doesn’t match up with the real-user-id, the other :db/retractEntity fails as well?

If I understood it correctly, how would I get the real-user-id in that case, a lookup-ref?


#5

Yes, essentially. However you won’t be able to have the cas assert the same value (i.e. the same ID) and use retractEntity in the same transaction. The reason for this is that retractEntity will generate a datom that retracts the real-user-ID, while the CAS will create a datom that asserts the same value. This will result in a “two datoms conflict” error.

I would recommend something like:

[[:db/cas entity-ID :notebook/user user-id-from-token "<userID>+RETRACTED"]
 [:db/retractEntity entity-ID]]

This cas structure now says “change the value of the :notebook/user attribute on the entity with entity-ID to “+RETRACTED” only if the user-id that came from the request token is the current value for that attribute”

As you surmised, if the compare fails (because the request token id doesn’t match the actual user ID in the db) the entire transaction will abort.


#6

Note that the one downside of this approach is you will be left with a single datom present for this entity, the [eid :notebook/user "<userID>+RETRACTED"] datom, as it will be asserted by the cas during this transaction.