Best way to prevent changeset from associating with existing data?

I’ve started using more nested associations in my changesets, which work great for creating/updating/deleting the associated data at the same time as the parent. This however makes authorization much more difficult for me.

The issue I’m trying to prevent is a possible attack vector of a user passing in the ID of another user’s data in that nested data, so they can then see or modify it. Is there a way to prevent cast_assoc from taking the id of another record? Ideally I’d have cast_assoc create/delete nested records as needed, but would reject any attempt to associate an existing record.

I have authorization checks at the top level (User A can modify Data 1), but authorization gets really messy if I have to check/preload every piece of nested data as well.

%Element{
  element_photos: [%ElementPhoto{ order: 0, photo: %Photo{...}}]
}

Thanks!

I don’t think there is a way to do that. When passing an ID for a related record not yet associated to the parent this will at best become an insert operation, which will fail because a record with that ID already exists (it’s the primary key).

Ok, yeah that makes sense. I’m thinking maybe the best way to is to move (or add) authorization after my changeset function. Then I can easily iterate the element_photos and see if the changes include things I don’t want to permit.

The only thing I don’t like about this is I currently authorize first, then hand the data off to a create_element/update_element function which in turn uses the change_element function. Now I’ll need to do something like:

element
|> change_element(attrs)
|> authorize!(:change_element, user)
|> Repo.update()

I’d love to see a concrete example of how you think someone could get a hold of existing records by modifing the input in the first place.

Currently you can update a Photo that’s nested inside an Element. In other parts of my app I ensure that a user is authorized to modify this Photo. But now that it’s nested, they could potentially put another user’s Photo as a nested association inside their Element so that they can modify its fields.

I don’t think that’s possible. As mentioned you cannot use cast_assoc to freshly associate a parent to an existing child record. You can only update what is already associated to the parent or create new associations. The latter will fail on insert if someone tries to reuse an existing primary key.

Sorry I’m not explaining this very well. The problem is there’s a join table in the mix.
Element <> ElementPhoto <> Photo.

What I’m trying to prevent is a user using their ElementPhoto, to modify another user’s Photo.

attrs = %{
  element_photos: [%{photo: %{photo_attrs | id: other_user_photo.id}}]
}
update_element(element, attrs)

Again this will try to create a photo, not just create the ElementPhoto. This works no different for a many_to_many than for a has_many relationship as far as cast_assoc is concerned.

You’re right, I had a faulty test. I was asserting for failure in the creation with the other photo’s id, but it was actually succeeding by just creating a new photo. It looks like that field just gets dropped / has no effect.

Thank you!