Goal: Allow end users to have the ability to
- Define user roles
- Assign users to defined roles
- Define authorization rules for each role dynamically without code change.
Solution:
- CRUD roles UI, which can assign user to roles, and define authorization matrix for each role.
- The authorization matrix has to be extensible as controllers and actions grow. This is done by inspecting Rails routes: Rails.application.routes.routes.map(&:requirements) tell us what actions are in which controllers.
- CanCan Ability runs can {|action, subject_class, subject| user.role.authorized?(action, subject_class, subject) }
- Authorization matrix must also be forward-compatible with default rule for actions not covered by the current matrix.
- The value for authorization matrix can simply be boolean or some code block for different checking mode, for example, record level authorization: owner only, owner or superior, owner + peer + superior, etc. The rules are dynamic but these modes are not. Code change is required when there is a new authorization mode.
- Code blocks cannot be stored in the database directly without the unsafe eval. There should be an object that contains these authorization modes which can be referred to through string or symbol. We can then store these symbols in the authorization matrix. An example would be a hash with symbol key and lambda value. The lambda block must have a certain signature which can be called consistently. The authorized? method then call these lambda blocks with a fixed set of parameters retrieved from the arguments or somewhere else.
- Testing is ridiculously complex. Instead of testing which role can do what, we must test that if a role can perform an action, it must be able to. The responsibility of testing which role can do what is pushed to the UAT level.