@K.A.Buhr, wow! Thank you for such a detailed reply. You are correct that this is an XY problem, and you've pretty-much nailed the actual problem that I'm trying to solve. Another important piece of context is that, at some point these type-level permissions will have to be "reified" at the value-level. This is because the final check is against the permissions granted to the currently signed-in user, which are stored in the DB.
Taking this into account, I'm planning to have two "general" functions, say:
requiredPermission :: (RequiredPermission p ps) => Proxy p -> AppM ps ()
optionalPermission :: (OptionalPermission p ps) => Proxy p -> AppM ps ()
Here's the difference:
requiredPermission
will simply add the permission to the type-level list and it will be verified whenrunAppM
is called. If the current user does not have ALL the required permissions, thenrunAppM
will immediately throw a 401 error to the UI.- On the other hand,
optionalPermission
will extract theuser
from theReader
environment, check the permission, and return aTrue / False
.runAppM
will do nothing withOptionalPermission
s. These will be for cases where the absence of a permission should NOT fail the entire action, but skip a specific step in the action.
Given this context, I'm not sure if I would end-up with functions, like grantA
or grantB
. The "unwrapping" of ALL the RequestPermission
s in the AppM
constructor will be done by runAppM
, which will also ensure that the currently sign-in user actually has these permissions.
Also, are ConstraintKinds
something that I should look at, to make writing these type signatures easier? For example:
type HasRequiredPermissions p ps = RequiredPermission p ps ~ True