Авторизация

Лекция 19

Что это?

Что это?

Авторизация

  • предоставление доступа к определенному ресурсу/действию
  • на основании каких-то признаков (флагов, ролей, привилегий, ...)

Виды

  • дискреционное управление доступом (DAC)
  • пользователю user_1 разрешено читать файл file_1
  • пользователю user_2 разрешено читать и писать в файл file_1

Виды

  • мандатное управление доступом (MAC)
  • разделение пользователей по уровням допуска
  • пользователю с уровнем допуска L1 разрешено читать файл file_1

Виды

  • управление доступом на основе ролей (RBAC)
  • пользователю с ролью role_1 разрешено читать файл file_1
  • наиболее часто используется в веб-приложениях

Варианты реализации

  • флаг (isAdmin)
  • роли (одна, несколько)
  • роли + привилегии

Сервис авторизации


const permissions = {
  '/posts/create': 'user',
  ...
};

class AuthorizationService {
  async checkPermissions(user, route) {
    if (!permissions[route]) {
      return;
    }

    if(!user) {
      throw this.errors.accessDenied;
    }
    ...
  }
}
   

Флаг


async checkPermissions(user, route) {
  ...

  const role = user.isAdmin ? 'admin' : 'user';

  if (permissions[route] !== role) {
      throw this.errors.accessDenied;
  }
}
    

Роли


async checkPermissions(user, route) {
  ...

  const role = await this.rolesRepository
    .findOne({
      where: {
        name: permissions[route]
      }
    });

  const hasRole = await user.hasRole(role);

  if (!hasRole) {
    throw this.errors.accessDenied;
  }
}
    

Роли + привилегии


const permissions = {
  '/posts/create': [{
      resource: 'post',
      action: 'create'
  }],
  ...
};
    

Роли + привилегии


async checkPermissions(user, route) {
  ...

  if (!user.can(permissions[route])) {
    throw this.errors.accessDenied;
  }
}
    

Middleware авторизации


const shouldHave = permissions =>
  async (req, res, next) => {
    await authorizationService
      .checkPermissions(req.user, permissions);

    next();
  };
    

Middleware авторизации


class PostsController {
  constructor(postsService, cahceService) {
    ...
    this.routes['/create'].unshift({
      method: 'post',
      cb: shouldHave([{
        resource: 'post',
        action: 'create'
      }])
    });
  }
  ...
}
    

Node ACL

Node ACL

  • Access Control Lists for Node
  • встроенная интеграция с redis, mongo
  • возможно использование RAM
  • github.com/OptimalBits/node_acl

Node ACL


npm install acl
    

Node ACL


const acl = require('acl');

acl = new acl(new acl.memoryBackend());
    

Node ACL


// guest is allowed to view blogs
acl.allow('guest', 'blogs', 'view')

// allow function accepts arrays as any parameter
acl.allow(
  'member',
  'blogs',
  ['edit', 'view', 'delete']
)
    

Node ACL


acl.addUserRoles('joed', 'guest')

acl.addRoleParents('baz', ['foo', 'bar'])

acl.allow('admin', ['blogs', 'forums'], '*')
    

Node ACL


acl.isAllowed('joed', 'blogs', 'view', (err, res) => {
  if(res){
    console.log("User joed is allowed to view blogs")
  }
})
    

Node ACL


acl.isAllowed(
  'jsmith',
  'blogs',
  ['edit', 'view', 'delete']
)
    

Node ACL


acl.allowedPermissions(
  'james',
  ['blogs', 'forums'],
  (err, permissions) => console.log(permissions)
)

[
  {'blogs': ['get', 'delete']},
  {'forums':['get', 'put']}
]
    

Node ACL


// req.url
// req.session.userId
app.put(
  '/blogs/:id',
  acl.middleware(),
  (req, res, next) => { ... }
)
    

Node ACL


addUserRoles
removeUserRoles

userId   {String|Number}
roles    {String|Array}
callback {Function}
    

Node ACL


userRoles

userId   {String|Number}
callback {Function}
    

Node ACL


roleUsers

rolename {String|Number}
callback {Function}
    

Node ACL


hasRole

userId   {String|Number}
rolename {String|Number}
callback {Function}
    

Node ACL


addRoleParents
removeRoleParents

role     {String}
parents  {String|Array}
callback {Function}
    

Node ACL


removeRole

role     {String}
callback {Function}
    

Node ACL


allow

roles       {String|Array}
resources   {String|Array}
permissions {String|Array}
callback    {Function}
    

Node ACL


removeAllow

role        {String}
resources   {String|Array}
permissions {String|Array}
callback    {Function}
    

Node ACL


allowedPermissions

userId    {String|Number}
resources {String|Array}
callback  {Function}
    

Node ACL


isAllowed

userId      {String|Number}
resource    {String}
permissions {String|Array}
callback    {Function}
    

Node ACL


areAnyRolesAllowed

roles       {String|Array}
resource    {String}
permissions {String|Array}
callback    {Function}
    

Node ACL


whatResources

role        {String|Array}
permissions {String|Array} # optional
callback    {Function}
    

Node ACL


middleware

numPathComponents {Number}
userId            {String|Number|Function}
permissions       {String|Array}
    

CASL

CASL

  • изоморфная библиотека
  • возможность задавать условия доступа
  • есть интеграция с mongo
  • документация

CASL


npm install casl
    

CASL


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

...

const ability = AbilityBuilder.define((can, cannot) => {
  can('read', 'all');

  can('manage', 'Post', { author: req.user.id });

  cannot('delete', 'Post',
    { 'comments.0': { $exists: true } }
  );
});
    

CASL


ability.can('read', 'Post') // true
ability.can('create', 'Post') // true
ability.can('comment', 'Post') // false

const post = new Post({ title: 'What is CASL?' })
ability.can('read', post) // true
    

CASL


function defineAbilitiesFor(user) {
  return new Promise(resolve => {
    resolve(AbilityBuilder.define((can, cannot) => {
      can('read', 'Post');

      if (user && user.hasRole('regular')) {
        can('comment', 'Post'});
      }

      if (user && user.hasRole('premium')) {
        can('comment', 'Post'});
        can('manage', 'Post'}, { author: user.id });
      }

      if (user && user.hasRole('admin')) {
        can('comment', 'Post'});
        can('manage', 'Post'});
      }
    }));
  });
}
    

CASL


const { AbilityBuilder, Ability } = require('casl')

function defineAbilitiesFor(user) {
  return new Promise(resolve => {
    const { rules, can, cannot }
        = AbilityBuilder.extract()

    ...

    resolve(new Ability(rules));
  });

}
    

CASL


can('manage', 'Post') // CRUD
can('read', 'all')
can(['update', 'delete'], ['Post', 'Comment'])
    

CASL


Ability.addAlias('modify', ['update', 'delete'])

AbilityBuilder.define(can => {
 can('modify', 'Post')
})
    

CASL


can(
  'read',
  'Project',
  { active: true, ownerId: user.id }
)

can(
  'delete',
  'Post',
  { 'comments.0': { $exists: false } }
)
    

CASL


// OR
can('read', 'Post', { published: true })
can('read', 'Post', { preview: true })
    

CASL


// Override
can('manage', 'Post')
cannot('delete', 'Post')
    

CASL


ability.update([]) // removes all rules
ability.update([{ subject: 'all', actions: 'read' }])

ability.on('update', ({ rules, ability }) => {
  ...
})
    

CASL


try {
  const post = new Post({ private: true })

  ability.throwUnlessCan('delete', post)

  post.destroy()
} catch (error) {
  if (error instanceof ForbiddenError) {
    console.log('Access denied!')
  }
}
    

CASL


res.json({
  ...,
  rules: req.ability.rules
});

... on client side ...

const ability = new Ability(rules);
    

CASL

  • в глобальном контроллере аутентификации собираем ability, сохраняем его в req/res
  • пишем middleware авторизации (если не нужно проверять данные)
  • либо делаем проверки в сервисах, передавая туда ability