Ecto schema dependent fields

I’d like to have a single User “type” in my app, but users could have one of several roles. Assuming 3 roles of Reader/Author/Editor where Readers can read all articles but edit none; Authors can read all articles, compose new articles, and edit their own articles, and Editors can read/compose/edit everything. Assuming these roles are fixed – IOW, a Reader can never become an Editor or vice versa, how would you model this in an Ecto schema?
I have this:

schema "users" do 
  field :username, :string
  field :role, Role, default: Role.default # role is defined using EnumType lib
  has_many :articles, Article

but this could allow users with role Reader to also own Articles, which should be impossible. Is it possible to have schema fields required dependent on the value of other fields?

This is really enforcing authorisation rules and you could do this in a number of spots depending on the overall design of your application.

It sounds like you would like to enforce it close to the database so probably the easiest approach is to add a custom changeset validation function (see Where to write custom changeset validation functions which rely on current database values / records? - #2 by idi527) or google “ecto custom validation”. You then need to ensure the appropriate changeset is used. The advantage of this approach over, say, enforcing through database layer logic is you can provide nice error messages and it plays nicely with Phoenix (dead views & liveview).

The disadvantage is that you will likely need permission-checking logic elsewhere anyway to avoid serving up an annoying user interface where the user thinks they’re doing the right thing until they get an error message telling them they could never have saved an article in the first place.

Personally I tend to handle authorisation concerns in a separate area and interrogate it from the UI and business contexts, but I’m working on an enterprisey application that has quite complex permissions models.

2 Likes

So the idea should be enforce access to those fields in the application logic not in the database layout, correct?

Very good point, thanks!

That’s how I would see it, but again, depends on the overall application. If you really want to enforce at the database level then you would need triggers or similar. In my experience, this usually ends in tears - SQL is great for set based data transformations etc, but not for representing business logic. It is another layer to build tests around and often the test infrastructure isn’t great. It also makes debugging a lot more complex if you have logic scattered all over the place.