2

I am brand new to the Vapor framework, and am trying to protect multiple routes. Basically, I want to make sure that all routes under /campaigns/:id can only be accessed if the user actually has access to that particular campaign with that ID. So that I can't just enter any ID into the url and access any campaign.

Now, instead of adding logic for this to every single route (already 6 so far), I figured I'd use a middleware for this. This is what I came up with so far, with the help of some friendly folk over at the Vapor Discord:

final class CampaignMiddleware: Middleware {
  func respond(to request: Request, chainingTo next: Responder) throws -> EventLoopFuture<Response> {
    let user = try request.requireAuthenticated(User.self)
    return try request.parameters.next(Campaign.self).flatMap(to: Response.self) { campaign in
      guard try campaign.userID == user.requireID() else {
        throw Abort(.forbidden, reason: "Campaign doesn't belong to you!")
      }
      return try next.respond(to: request)
    }
  }
}

struct CampaignController: RouteCollection {
  func boot(router: Router) throws {
    let route = router.grouped("campaigns")

    let tokenAuthMiddleware = User.tokenAuthMiddleware()
    let guardMiddleware = User.guardAuthMiddleware()
    let tokenAuthGroup = route.grouped(tokenAuthMiddleware, guardMiddleware)

    tokenAuthGroup.get(use: getAllHandler)
    tokenAuthGroup.post(CampaignCreateData.self, use: createHandler)

    // Everything under /campaigns/:id/*, CampaignMiddleware makes sure that the campaign actually belongs to you
    let campaignRoute = tokenAuthGroup.grouped(Campaign.parameter)
    let campaignMiddleware = CampaignMiddleware()
    let protectedCampaignRoute = campaignRoute.grouped(campaignMiddleware)

    protectedCampaignRoute.get(use: getOneHandler)
    protectedCampaignRoute.delete(use: deleteHandler)
    protectedCampaignRoute.put(use: updateHandler)

    // Add /campaigns/:id/entries routes
    let entryController = EntryController()
    try protectedCampaignRoute.register(collection: entryController)
  }

  func getAllHandler(_ req: Request) throws -> Future<[Campaign]> {
    let user = try req.requireAuthenticated(User.self)
    return try user.campaigns.query(on: req).all()
  }

  func getOneHandler(_ req: Request) throws -> Future<Campaign> {
    return try req.parameters.next(Campaign.self)
  }

  // ...deleted some other route handlers...
}

The problem here is that the middleware is "eating up" the campaign parameter by doing request.parameters.next(Campaign.self). So in getOneHandler, where it also tries to access req.parameters.next(Campaign.self), it fails with error "Insufficient parameters". Which makes sense, since .next actually removes that param from the internal array of parameters.

Now, how can I write a middleware, that uses the parameter, without eating it up? Do I need to use the raw values and query the Campaign model myself? Or can I somehow reset the parameters after using .next? Or is there another better way to deal with model authorization in Vapor 3?

4

1 回答 1

4

Heeey, it looks like you could get your Campaign from request without dropping it like this

guard let parameter = req.parameters.values.first else {
    throw Abort(.forbidden)
}
try Campaign.resolveParameter(parameter.value, on: req)

So your final code may look like

final class CampaignMiddleware: Middleware {
  func respond(to request: Request, chainingTo next: Responder) throws -> Future<Response> {
    let user = try request.requireAuthenticated(User.self)
    guard let parameter = request.parameters.values.first else {
        throw Abort(.forbidden)
    }
    return try Campaign.resolveParameter(parameter.value, on: request).flatMap { campaign in
      guard try campaign.userID == user.requireID() else {
        throw Abort(.forbidden, reason: "Campaign doesn't belong to you!")
      }
      return try next.respond(to: request)
    }
  }
}
于 2019-03-09T23:36:39.517 回答