Статьи

Управление разрешениями пользователей в приложении Vue.js

В аутентифицированных интерфейсных приложениях мы часто хотим изменить то, что видно пользователю, в зависимости от назначенной ему роли. Например, гостевой пользователь может видеть сообщение, но только зарегистрированный пользователь или администратор видит кнопку для редактирования этого сообщения.

Управление разрешениями в внешнем приложении может быть грязным. Вы могли написать код, подобный этому ранее:

if (user.type === ADMIN || user.auth && post.owner === user.id ) {
  ...
}

В качестве альтернативы есть небольшая библиотека под названием CASL, которая помогает очень просто управлять правами пользователей. После того, как вы определили свои разрешения с помощью CASL и настроили активного пользователя, вы можете изменить приведенный выше пример на что-то вроде этого:

if (abilities.can('update', 'Post')) {
  ...
}

В этой статье я покажу, как управлять разрешениями в внешнем приложении с помощью Vue.js и CASL.

Примечание: вам не нужно было использовать CASL, прежде чем следовать этому!

CASL Crash Course

CASL позволяет вам определить набор правил, которые ограничивают доступ к ресурсам данного пользователя.

Например, правила CASL могут указывать, какие операции CRUD (создание, чтение, обновление и удаление) пользователь может выполнять над данным ресурсом или объектом (например, публикацией, комментарием, статьей и т. Д.).

Допустим, у нас есть сайт объявлений с простыми сообщениями «на продажу». Очевидный набор правил для этого приложения будет:

  • Гость пользователь может просматривать любой пост.
  • Администратора пользователь может просматривать любой пост, и может обновить или удалить сообщение.

В CASL мы используем AbilityBuilderдля определения правил. Новое правило создается с помощью вызова can, например,

const { AbilityBuilder } = require('casl');

export function(type) {
  AbilityBuilder.define(can => {
    switch(type) {
      case 'guest':
        can('read', 'Post');
        break;
      case 'admin':
        can('read', 'Post');
        can(['update', 'delete'], 'Post');
        break;
      // Add more roles here
    }
  }
};

Теперь вы можете управлять своим приложением на основе проверок к определенным вами правилам, например:

import defineAbilitiesFor from './abilities';

let currentUser = {
  id: 999,
  name: "Julie"
  type: "registered",
};

let abilities = defineAbilitiesFor(currentUser.type);

Vue.component({
  template: `<div v-if="showPost">{{ post }}<div>
             <div v-else>Please log in</div>
            `,
  props: [ 'post' ],
  computed: {
    showPost() {
      return abilities.can('read', 'Post');
    }
  }
});

Вы можете узнать больше о CASL, проверив официальные документы .

Демо-проект

В качестве демонстрации я сделал простое приложение сервер / клиент, которое показывает объявления с классифицированными объявлениями. Правила разрешения для этого приложения: пользователь может прочитать любой пост или создать новый пост, но может только обновить или удалить пост, если это пост, который он создал.

Я использовал Vue.js с CASL, чтобы упростить реализацию и масштабирование этих правил в случае добавления других операций или объектов в будущем.

Теперь я расскажу вам, как настроить это приложение. Если вы хотите увидеть готовый код, посмотрите репозиторий GitHub .

Определение пользовательских разрешений

Давайте определим наши пользовательские разрешения в файле resources /ility.js . Одна из замечательных особенностей CASL заключается в том, что он не зависит от среды, то есть его можно использовать как в Node, так и в браузере.

Мы сделаем наше определение разрешений модулем CommonJS для обеспечения совместимости с Node (Webpack может преобразовать модуль для использования в клиенте).

ресурсы / ability.js

const casl = require('casl');

module.exports = function defineAbilitiesFor(user) {
  return casl.AbilityBuilder.define(
    { subjectName: item => item.type }, 
    can => {
      can(['read', 'create'], 'Post');
      can(['update', 'delete'], 'Post', { user: user });
    }
  );
};

Давайте разберем этот код немного:

Рассматривая второй аргумент defineметода, мы определяем правила доступа, делая вызовы can. Первый аргумент этого метода является операция CRUD (s) Вы хотите разрешить, то второй является ресурсы / объект, в данном случае Post.

Обратите внимание, что во втором canвызове функции мы передаем третий аргумент; объект. Это используется для проверки соответствия userсвойства userобъекта объекту, который мы предоставим при выполнении теста. Если мы этого не сделаем, любой пост может быть обновлен или удален любым пользователем, а не только его владельцем.

ресурсы / ability.js

...
casl.AbilityBuilder.define(
  ...
  can => {
    can(['read', 'create'], 'Post');
    can(['update', 'delete'], 'Post', { user: user });
  }
);

Когда CASL проверяет объект, чтобы определить разрешение, ему необходимо знать тип объекта, на который он смотрит. Один из способов сделать это — передать объект со свойством функции subjectNameв качестве первого аргумента defineметода. Эта функция будет возвращать тип объекта.

Мы осуществим это, вернув typeсобственность на наши объекты. Нам нужно убедиться, что это свойство присутствует, когда мы определим наши Postобъекты в данный момент.

ресурсы / ability.js

...
casl.AbilityBuilder.define(
  { subjectName: item => item.type }, 
  ...
);

Наконец, мы заключаем наше определение способности в функцию, которая позволяет нам передавать пользовательский объект в любое время, когда мы хотим проверить разрешения. Это будет лучше понято, когда мы будем использовать его в основном приложении ниже.

ресурсы / ability.js

const casl = require('casl');

module.exports = function defineAbilitiesFor(user) {
  ...
};

Доступ к правилам доступа в Vue

Теперь мы хотим иметь возможность протестировать объект в нашем внешнем приложении, чтобы увидеть, какие операции CRUD пользователь может выполнять над ним. Нам нужно будет предоставить доступ к правилам CASL в наших компонентах Vue. Вот как:

  1. Импорт Vue и плагин способностей . Этот плагин добавляет CASL к прототипу Vue, что позволяет нам вызывать его из компонентов.
  2. Импортируйте наш набор правил в приложение Vue (т.е. resources / abilities.js ).
  3. Определить текущего пользователя. В реальном приложении мы получили бы эти пользовательские данные с сервера. Для нашего примера мы просто жестко закодируем его.
  4. Помните, модуль способностей экспортирует функцию, которую мы будем вызывать defineAbilitiesFor. Мы передаем объект пользователя этой функции. Теперь каждый раз, когда мы тестируем объект, мы можем видеть, какие разрешения доступны для текущего пользователя.
  5. Добавьте плагин способностей, позволяющий нам делать тесты внутри компонента вроде this.$can(...).

SRC / main.js

import Vue from 'vue';
import abilitiesPlugin from './ability-plugin';

const defineAbilitiesFor = require('../resources/ability');
let user = { id: 1, name: 'George' };
let ability = defineAbilitiesFor(user.id);
Vue.use(abilitiesPlugin, ability);

Post Entity

Объекты, представляющие объявления классифицированных объявлений будут использоваться нашим приложением Они могут быть извлечены из базы данных и затем, например, переданы серверу во внешний интерфейс.

Наша Postсущность должна иметь два свойства :

  1. typeСобственности. CASL будет использовать subjectNameобратный вызов, определенный в abilities.js, чтобы проверить, какой тип объекта тестируется
  2. userСобственности. Это владелец поста. Помните, что у пользователя есть права на обновление и удаление, только если он владеет сообщением. В main.js мы уже говорили CASL, с кем является текущий пользователь defineAbilitiesFor(user.id). Все, что CASL нужно сделать сейчас, это проверить, соответствует ли идентификатор пользователя userсвойству.
let posts = [
  {
    type: 'Post',
    user: 1,
    content: '1 used cat, good condition'
  },
  {
    type: 'Post',
    user: 2,
    content: 'Second-hand bathroom wallpaper'
  }
];

Учитывая эти два объекта записи, у нашего текущего пользователя Джорджа, который имеет ID 1, будут права на обновление / удаление первого сообщения, но не второго.

Тестирование разрешения пользователя на объекте

Сообщения отображаются в нашем приложении через компонент под названием Post . Сначала посмотрите на код, а затем мы разберем его ниже:

SRC / компоненты / Post.vue

<template>
  <div class="post">
    <div class="content">
      {{ post.content }} 
      <br/><small>posted by {{ username }}</small>
    </div>
    <button @click="del">Delete</button>
  </div>
</template>
<script>
  import axios from 'axios';

  export default {
    props: ['post', 'username'],
    methods: {
      del() {
        if (this.$can('delete', this.post)) {
          ...
        } else {
          this.$emit('err', 'Only the owner of a post can delete it!');
        }
      }
    }
  }
</script>
<style lang="scss">...</style>

Когда пользователь нажимает кнопку « Удалить» , щелчок фиксируется и delвызывается метод обработчика.

Затем мы используем CASL, чтобы проверить, есть ли у текущего пользователя разрешение на эту операцию через this.$can('delete', post). Если у них есть разрешение, мы можем предпринять некоторые действия. В противном случае может отображаться сообщение об ошибке (например: «Только владелец сообщения может удалить его!»).

Тестирование на стороне сервера

В реальном приложении, после того как пользователь удалит сообщение в интерфейсе, мы будем использовать AJAX для отправки инструкции удаления в API, например:

SRC / компоненты / Post.vue

if (this.$can('delete', post)) {
  axios.get(`/delete/${post.id}`, ).then(res => {
    ...  
  });
}

Затем мы поместили бы тестовую логику CASL на сервер, поскольку сервер не должен доверять операции CRUD от клиента:

server.js

app.get("/delete/:id", (req, res) => {
  let postId = parseInt(req.params.id);
  let post = posts.find(post => post.id === postId);
  if (ability.can('delete', post)) {
    posts = posts.filter(cur => cur !== post);
    res.json({ success: true });
  } else {
    res.json({ success: false });
  }
});

Поскольку CASL изоморфен, abilityобъект на сервере можно импортировать из abilities.js , что избавляет нас от необходимости дублировать любой код!

Заворачивать

Благодаря этому у нас есть действительно хороший способ управления правами пользователей в простом приложении Vue.

Я считаю, this.$can('delete', post)что гораздо элегантнее, чем:

if (user.id === post.user && post.type === 'Post') {
  ...
}

Это не только сложнее для чтения, но и здесь есть неявное правило, то есть, что сообщение может быть удалено пользователем. Это правило, несомненно, будет использоваться в другом месте нашего приложения, и его действительно следует абстрагировать. Это то, что CASL может сделать для нас.

Спасибо Сергею Стоцкому , создателю CASL , за помощь в этой статье.