node.jskoa2koa-router

Koa-router getting parsed params before hitting route


I'm using koa2 and koa-router together with sequelize on top. I want to be able to control user access based on their roles in the database, and it's been working somewhat so far. I made my own RBAC implementation, but I'm having some trouble.

I need to quit execution BEFORE any endpoint is hit if the user doesn't have access, considering endpoints can do any action (like inserting a new item etc.). This makes perfect sense, I realize I could potentially use transactions with Sequelize, but I find that would add more overhead and deadline is closing in.

My implementation so far looks somewhat like the following:

// initialize.js

initalizeRoutes()
initializeServerMiddleware()

Server middleware is registered after routes.

// function initializeRoutes
app.router = require('koa-router')

app.router.use('*', access_control(app))

require('./routes_init')

routes_init just runs a function which recursively parses a folder and imports all middleware definitions.

// function initializeServerMiddleware
// blah blah bunch of middleware
app.server.use(app.router.routes()).use(app.router.allowedMethods())

This is just regular koa-router.

However, the issue arises in access_control.

I have one file (access_control_definitions.js) where I specify named routes, their respective sequelize model name, and what rules exists for the route. (e.g. what role, if the owner is able to access their own resource...) I calculate whether the requester owns a resource by a route param (e.g. resource ID is ctx.params.id). However, in this implementation, params don't seem to be parsed. I don't think it's right that I have to manually parse the params before koa-router does it. Is anyone able to identify a better way based on this that would solve ctx.params not being filled with the actual named parameter?

edit: I also created a GitHub issue for this, considering it seems to me like there's some funny business going on.


Solution

  • So if you look at router.js

    layerChain = matchedLayers.reduce(function(memo, layer) {
      memo.push(function(ctx, next) {
        ctx.captures = layer.captures(path, ctx.captures);
        ctx.params = layer.params(path, ctx.captures, ctx.params);
        ctx.routerName = layer.name;
        return next();
      });
      return memo.concat(layer.stack);
    }, []);
    
    return compose(layerChain)(ctx, next);
    

    What it does is that for every route function that you have, it add its own capturing layer to generate the params

    LayerMap

    Now this actually does make sense because you can have two middleware for same url with different parameters

    router.use('/abc/:did', (ctx, next) => {
        // ctx.router available
        console.log('my request came here too', ctx.params.did)
        if (next)
            next();
    });
    
    router.get('/abc/:id', (ctx, next) => {
        console.log('my request came here', ctx.params.id)
    });
    

    Now for the first handler a parameter id makes no sense and for the second one parameter did doesn't make any sense. Which means these parameters are specific to a handler and only make sense inside the handler. That is why it makes sense to not have the params that you expect to be there. I don't think it is a bug

    And since you already found the workaround

    const fromRouteId = pathToRegexp(ctx._matchedRoute).exec(ctx.captures[0])
    

    You should use the same. Or a better one might be

    var lastMatch = ctx.matched[ctx.matched.length-1];
    params = lastMatch.params(ctx.originalUrl, lastMatch.captures(ctx.originalUrl), {})