Motivation:
Other server frameworks like Laravel have fully developed authorization systems that allow developers to specify whether or not a route can be accessed under certain conditions. These packages are more advanced than simply checking "is user.userType == .admin
," they are more about checking "is user.id == post.user_id
for each route. Drawing from Laravel's Gate system, I'm proposing the following API. I'd love to get some feedback on it about what people like, don't like, or want to see added.
AuthorizationPolicy
protocol AuthorizationPolicy {
func isAuthorized(req: Request) throws -> Future<Bool>
}
This protocol defines a policy that can be executed before a request.
Example AuthorizationPolicy implementation
struct PostOwnerPolicy: AuthorizationPolicy {
func isAuthorized(req: Request) throws -> EventLoopFuture<Bool> {
return req.parameters.next(Post.self).map { post in
return req.user().id == post.user_id
}
}
}
This policy gets the user from the request and the post from the request parameters and makes sure that the user is authorized to access it.
Registering Policies
This part needs some work/feedback. I'm imagining something like DatabaseConfig
:
var policyConfig = PolicyConfig()
try policyConfig.add(PostOwnerPolicy())
I would like to do something similar to DatabaseIdentifier
as well though so that it might look something like this:
var policyConfig = PolicyConfig()
try policyConfig.add(PostOwnerPolicy(), id: .userMustBePostOwner)
PolicyMiddleware
Middleware for validation might look something like this (I haven't put much effort into optimizing this code):
final class PolicyMiddleware: Middleware {
let policies: [AuthorizationPolicy]
init(policies: [AuthorizationPolicy]) {
self.policies = policies
}
func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response> {
return try policies.map { try $0.isAuthorized(req: request) }.flatten(on: request).flatMap { results in
guard !results.contains(false) else { throw Abort(.unauthorized) }
return try next.respond(to: request)
}
}
}
In a controller
final class PolicyController: RouteCollection {
func boot(router: Router) throws {
router.group([PolicyMiddleware(policies: [PostOwnerPolicy()])]) { build in
// register routes here
}
}
}
Ideally this is where the identifier would come in so that you could do something like this instead:
final class PolicyController: RouteCollection {
func boot(router: Router) throws {
router.group([PolicyMiddleware(policies: [.userMustBePostOwner])]) { build in
// register routes here
}
}
}