JiriChara / vue-kindergarten

Modular security for Vue, Vuex, Vue-Router and Nuxt
MIT License
312 stars 24 forks source link

Different routing decisions based on purpose / context #5

Closed Kinvaras closed 7 years ago

Kinvaras commented 7 years ago

Hi @JiriChara,

First, thanks for this lib, which is very interesting to use.

I am stuck in my authorization based application using your lib. In your excellent article, you say:

This code works, however as I mentioned earlier the governess is responsible for the default behavior when user is trying to access protected resource.

In order to handle a "not allowed" case correctly, I have implemented the guard() method of my RouteGoverness according to your example. However, I don't have just one default behavior but several different behaviors based on "why you were not allowed to do this".

In other words, I would like to be able to:

and so on...

The logic is actually executed by the guard() method of my RouteGoverness and I am pretty sure there's a better way to do it. On top of that, it doesn't work as expected.

Here's a sample of what I tried to do:

import { HeadGoverness } from 'vue-kindergarten';

export default class RouteGoverness extends HeadGoverness {
  constructor({from, to, next}) {
    super();

    this.next = next;
    this.from = from;
    this.to = to;
  }

  guard(action) {
    if (super.isNotAllowed(action)) {
      // Redirection based on the "to" route the user wanted to reach
      console.warn('UNAUTHORIZED access to', this.to.name);

      switch (this.to.name) {
        case 'login':
          this.redirectTo('index');
          break;
        case 'index':
          this.redirectTo('login');
          break;
        default:
          this.redirectTo('unauthorized');
          break;
      }
    } else {
      this.next();
    }
  }

  redirectTo(routeName) {
    this.next({
      name: routeName
    });
  }
};

Is there a way to do it efficiently or am I wrong about the process ? Thanks.

JiriChara commented 7 years ago

Hi @Kinvaras,

Thanks for the feedback, I am really happy that you like vue-kindergarten!

I think in this case it's better to user per-component guards:

<template>
  <div>Hello</div>
</template>

<script>
  import myPerimeter from '@/perimeters/my';
  import MyComponentGoverness from '@/governesses/MyComponentGoverness';

  export default {
    perimeters: [
      myPerimeter,
    ],

    governess: new MyComponentGoverness(),

    beforeRouteEnter(to, from, next) {
      next((vm) => {
        vm.$guard('view', { to, from, next });
      });
    },
  };
</script>

Please see my comment for more information.

Kinvaras commented 7 years ago

Hi @JiriChara,

Thanks for your reply. Actually, you have confirmed what I found on my own (using per-component governesses).

Just a little difference, I usually tend to separate the concerns as far as I can. In this case, all my component-specific governesses are used directly in the routes declaration, instead of the components themselves.

This way, components and governesses remain decoupled. In the following example, I use meta attribute of a route record to forward the right governess:

// router.js

// Callback for redirection based on role
// This way, you're automatically redirected to the right route
const indexRedirect = function (to) {
  if (store.getters['auth/isAdmin']) {
    return {
      name: 'admin'
    }
  }

  return {
    name: 'dashboard'
  };
};

const routes = [
  {
    path: '/',
    component: AppContent,
    children: [
      {
        path: '',
        name: 'index',
        component: Layout,
        meta: {
          governess: AuthenticatedRouteGoverness
        },
        redirect: indexRedirect,
        children: [
          {
            path: 'dashboard',
            name: 'dashboard',
            component: Dashboard,
            meta: {
              governess: RegularUserRouteGoverness
            }
          },
          {
            path: 'admin',
            name: 'admin',
            component: Admin,
            meta: {
              governess: AdminRouteGoverness
            }
          }
        ]
      },
      {
        path: 'login',
        name: 'login',
        component: Login,
        meta: {
          governess: UnauthenticatedRouteGoverness
        }
      },
      {
        path: 'unauthorized',
        name: 'unauthorized',
        component: Unauthorized
      },
      {
        path: '*',
        name: 'default',
        redirect: {
          name: 'index'
        }
      }
    ]
  }
];

const router = new VueRouter({
  mode: 'history',
  routes
});

This code shows how I declare which governess to use. The next step consists on instantiating this governess using router.beforeEach() the same way you use it for perimeters.

// router.js

router.beforeEach((to, from, next) => {
  let hasBeenSandboxed = false;
  let Governess = RouteGoverness;

  to.matched.forEach((routeRecord) => {
    const perimeter = perimeters[`${routeRecord.name}Perimeter`];

    if (perimeter) {
      if (routeRecord.meta && routeRecord.meta.governess) {
        Governess = routeRecord.meta.governess;
      }

      const sandbox = createSandbox(child(store), {
        governess: new Governess({from, to, next}),
        perimeters: [
          perimeter
        ]
      });

      hasBeenSandboxed = true;

      return sandbox.guard('route');
    }
  });

  if (!hasBeenSandboxed) {
    return next();
  }
});

By the way, I added a check on whether the record has been sandboxed to avoid multiple potentially opposite routing decision:

  if (!hasBeenSandboxed) {
    return next();
  }

If no governess is present for a record, I use a default one (the classical RouteGoverness you provide in your example).

Here's a sample of RouteGoverness and AuthenticatedRouteGoverness to give an idea:

// RouteGoverness.js

import { HeadGoverness } from 'vue-kindergarten';

export default class RouteGoverness extends HeadGoverness {
  constructor({from, to, next}) {
    super();

    this.next = next;
    this.from = from;
    this.to = to;
  }

  guard(action) {
    if (super.isNotAllowed(action)) {
      return this.redirectTo('unauthorized');
    }

    return this.next();
  }

  redirectTo(routeName) {
    this.next({
      name: routeName
    });
  }
};
// AuthenticatedRouteGoverness.js

import RouteGoverness from './RouteGoverness';

export default class AuthenticatedRouteGoverness extends RouteGoverness {
  constructor({from, to, next}) {
    super({from, to, next});
  }

  guard(action) {
    if (super.isNotAllowed(action)) {
      return this.redirectTo('login');
    }

    return this.next();
  }
};

As you can see, AuthenticatedRouteGoverness extends RouteGoverness so it can access redirectTo() method.

The same logic can be extended to perimeters I guess.

Hope this can help ;)

JiriChara commented 7 years ago

@Kinvaras this is pretty cool! I will try to find some time this or next week to add this to README, or to create a documentation page.

Kinvaras commented 7 years ago

I'm glad I could help, you gave some strong advices as well ;)

Kinvaras commented 7 years ago

I have updated my example so the router is now consistent for efficient routing.

I added an indexRedirect callback for some sort of "automatic" routing.

The "index" named route now acts as a magic route, you can simply redirect your user to it (using the name, not the path) in order for kindergarten to handle a role based redirection for you

JiriChara commented 7 years ago

@Kinvaras good work! I am closing this issue for now. Feel free to re-open or raise a new issue if you have any more questions.