I've been wanting to add some permissions around my routes and I thought I'd write my thoughts down as as suggestions or ideas for future development. I'm new to Vapor and backend development, so please take this with a grain of salt.
Roles/Permissions
First, I thought it would be great to be able to add some ability to define permissions levels or roles in conjunction with the User
model. It seems like the most flexible system would allow the author to define these levels on their own (These could be defined as either one-to-one or one-to-many). Alternatively, Vapor could provide preset permissions levels, but that seems like possible overkill. Presumably this could be built as a Protocol any model could conform to, but concrete implementations could be provided as well.
Authorizable
Could be designed like Timestampable
:
extension User: Authorizable {
// Adds mapping to Authorizable key path properties
static var permissionsKey: WritableKeyPath<User, [PermissionType]> { return \.roleLevel }
}
Rules/Authorization
Now, to add Authorization
the routes you could add an extra function to the route chain like .authorize(using: .FooPolicy)
. This could be done with a closure or function that accepts arguments that could be used to define a true/false or pass/fail test. Laravel does something similar with their Authorization system.
If you start with a collection of routes like this:
let athleteRoutes = router.grouped("api", "athletes")
athleteRoutes.get(use: index)
athleteRoutes.post(use: create)
athleteRoutes.delete(Athlete.parameter, use: delete)
With Authorization
the route group could end up looking like the following:
let athleteRoutes = router.authorize(using: athletePolicy).grouped("api", "athletes")
athleteRoutes.get(use: index)
athleteRoutes.post(use: create)
athleteRoutes.delete(Athlete.parameter, use: delete)
Or:
let athleteRoutes = router.grouped("api", "athletes")
athleteRoutes.get(use: index)
athleteRoutes.authorize(using: athleteEditPolicy).post(use: create)
athleteRoutes.authorize(using: athleteEditPolicy).delete(Athlete.parameter, use: delete)
Ideally, you could add this authorization component at both the individual route or route group level.
The authorization function could look something like this:
func athletePolicy(_ req: Request) throws -> Future<Bool> {
return user.permissions.contains(.athleteEditor) // pseudocode for getting user permissions levels
}
Or:
func athleteEditPolicy(_ req: Request) throws -> Future<Bool> {
return try req.content.decode(Athlete.self).map(to: Bool.self) { athlete in
return athlete.ownerID == user.ID //pseudocode for getting user.ID
}
}
In the event that the Authorization
process fails, I'd be great if Vapor sent the appropriate HTTP error code (I think this would be a 403, but I could be wrong).