I am going to implement authorisation within a Hanami project and I am looking how best to implement scoping with the Repository / Entity model, and instead of re-inventing the wheel I was hoping someone else had done so successfully. e.g. not passing the relation to the authorisation layer and chaining expressions where(admin: true)
My current thinking is to get the authorisation layer return a Proc, that can be passed into the repository method as a scope argument
This will be a plugin in rom 6.0 but for now it’s just a matter of overriding root method and using super, ie:
def root
super.where(admin: true)
end
As you can probably imagine, this can be encapsulated by some macro, like scope :admin that would define root automatically. It’s also nice to have such scopes defined as relation views for better encapsulation. This can feel like an overkill but only in the beginning
@solnic for clarification: the idea is to have separate repos for different levels of authorization? Since overriding root is only possible on the repo level I suppose.
I am currently injecting the repositories as a dependency in my controllers e.g.
include Import[entity_repo: 'repositories.entities.entity']
So I suppose the only way to achieve this would to either take the dependency out of the import, and as @apohllo suggests have a separate instance per level of authorisation, although this does pose a problem when the scope has a dynamic part to it e.g. user_id as we would have to pass this in as part of the constructor, and I would also lose all the benefit of reduced object allocation by freezing and memoizing the repository dependency which is what I am currently doing at the moment.
Well, generally I think that the reason for introducing repositories is the fact that you don’t want any details of the data layer leaking into the controllers. Passing the proc from controller to the repo, I believe, is a direct violation of this rule. There are the following options I suppose:
have different repos, for different levels of authorization, e.g. admin API would use AdminRepos (this works well with memoization - you just need to define an AdminRepo which inherits from the “default” repo and changes the root as @solnic suggests; yet it doesn’t work with the user_id)
make the user id/authorization level/whatever a part of the API of the repository - similar to your solution, but the difference here is that you don’t pass any db-layer details from the authorization layer. E.g. instead of passing the scope, you pass user_id or user role or some other type of requirement. How it is interpreted is essentially the responsibility of the db-layer
use dry-effect/reader - say with_scope which would be consumed by the repo. You can pass the scope, but also you can pass more model-oriented data, like the user_id, user roles etc.
To elaborate a bit on the “AdminRepo” - I mean you would need an AdminRepo class, eg. AdminNewsRepo or Admin::NewsRepo for each regular repo, eg. NewsRepo. This plays well only if you have two levels of authorization, it does not play well with the user_id.
Using multiple repo classes for the same struct type for this purposes is indeed a solution. I did exactly this and typically I’d have ie Public::StuffRepo and Admin::StuffRepo with admin-specific logic.
If there’s some “context” from the outside that’s needed to perform a query, then I 100% encourage you to try out dry-effects like @apohllo suggested. It’s very easy to add it to an existing code base and it simplifies many things.
I actually started down the route of dry-effect/reader, although there is a gem dependency which prevents it being used with Hanami which if memory serves me correctly was https://dry-rb.org/gems/dry-initializer/3.0/. I may try forking the repo and see if I can downgrade the dependency to use the same version I am currently locked to.
@solnic / @apohllo thank you for your thoughts, I am going hopefully work on this next week and I will report back with what I decide.
Which was being thrown by the following in rom-repository: https://github.com/rom-rb/rom-repository/blob/v1.4.0/lib/rom/repository/changeset/stateful.rb#L16
(I have a limit of 2 links per post, hence the disabled hyperlink)
@flash-gordon have we missed some improvement in dry-initializer and forgot to update ROM::Initializer accordingly? Seems like we have Maybe we should fix it and bump dry-initializer in current 5.x line?
@solnic I believe it would be a case of doing a search and replace for __data__ which is not allowed as a option in the latest version of dry-initializer e.g. I renamed it to private_data
BTW. this breaking change has just arrived in the latest version of dry-initializer
@__options__ ||= self.class.dry_initializer.definitions.values.each_with_object({}) do |item, obj|
obj[item.target] = instance_variable_get(item.ivar)
end
Gives a different behaviour I will fork rom v3.3.0 and use that instead of what I proposed