JS Tower
Node.jsМодуль 6: Безопасность

Авторизация

Роли, права доступа, middleware защиты

Цель урока

В этом уроке ты научишься:

  • Реализовывать ролевую модель
  • Создавать middleware для проверки прав
  • Защищать маршруты

Ролевая модель (RBAC)

Модель пользователя

const userSchema = new mongoose.Schema({
  email: String,
  password: String,
  role: {
    type: String,
    enum: ['user', 'moderator', 'admin'],
    default: 'user'
  }
});

Middleware проверки роли

function authorize(...roles) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Not authenticated' });
    }
    
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    
    next();
  };
}

// Использование
app.get('/admin/users', 
  authenticate, 
  authorize('admin'), 
  async (req, res) => {
    const users = await User.find();
    res.json(users);
  }
);

app.delete('/posts/:id',
  authenticate,
  authorize('admin', 'moderator'),
  async (req, res) => {
    await Post.findByIdAndDelete(req.params.id);
    res.status(204).send();
  }
);

Проверка владельца ресурса

function isOwner(model, paramName = 'id') {
  return async (req, res, next) => {
    const resource = await model.findById(req.params[paramName]);
    
    if (!resource) {
      return res.status(404).json({ error: 'Not found' });
    }
    
    // Проверяем владельца или админа
    const isOwner = resource.author?.toString() === req.user.id;
    const isAdmin = req.user.role === 'admin';
    
    if (!isOwner && !isAdmin) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    
    req.resource = resource;
    next();
  };
}

// Использование
app.put('/posts/:id',
  authenticate,
  isOwner(Post),
  async (req, res) => {
    const post = await Post.findByIdAndUpdate(
      req.params.id,
      req.body,
      { new: true }
    );
    res.json(post);
  }
);

Права доступа (Permissions)

Модель с правами

const rolePermissions = {
  user: ['read:posts', 'create:posts', 'update:own-posts', 'delete:own-posts'],
  moderator: ['read:posts', 'create:posts', 'update:posts', 'delete:posts'],
  admin: ['*']  // Все права
};

function hasPermission(permission) {
  return (req, res, next) => {
    const userPermissions = rolePermissions[req.user.role] || [];
    
    const hasAccess = 
      userPermissions.includes('*') || 
      userPermissions.includes(permission);
    
    if (!hasAccess) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    
    next();
  };
}

// Использование
app.delete('/posts/:id',
  authenticate,
  hasPermission('delete:posts'),
  async (req, res) => {
    await Post.findByIdAndDelete(req.params.id);
    res.status(204).send();
  }
);

Комбинированная проверка

function checkAccess(options) {
  return async (req, res, next) => {
    const { roles, permissions, ownerField } = options;
    
    // Проверка роли
    if (roles && !roles.includes(req.user.role)) {
      // Проверка владельца
      if (ownerField) {
        const resource = await options.model.findById(req.params.id);
        if (resource?.[ownerField]?.toString() === req.user.id) {
          req.resource = resource;
          return next();
        }
      }
      return res.status(403).json({ error: 'Forbidden' });
    }
    
    next();
  };
}

// Использование
app.put('/posts/:id',
  authenticate,
  checkAccess({
    roles: ['admin', 'moderator'],
    ownerField: 'author',
    model: Post
  }),
  updatePost
);

Практика

Задание: API с ролями

Задача: Создай API где:

  • user может читать и создавать свои посты
  • admin может всё

Решение:

// Middleware
const authenticate = require('./middleware/authenticate');

function authorize(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
}

// Routes
app.get('/posts', authenticate, async (req, res) => {
  const posts = await Post.find();
  res.json(posts);
});

app.post('/posts', authenticate, async (req, res) => {
  const post = await Post.create({
    ...req.body,
    author: req.user.id
  });
  res.status(201).json(post);
});

app.delete('/posts/:id', 
  authenticate,
  async (req, res, next) => {
    const post = await Post.findById(req.params.id);
    
    if (!post) {
      return res.status(404).json({ error: 'Not found' });
    }
    
    const isOwner = post.author.toString() === req.user.id;
    const isAdmin = req.user.role === 'admin';
    
    if (!isOwner && !isAdmin) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    
    await post.deleteOne();
    res.status(204).send();
  }
);

Проверь себя

  1. Чем отличается 401 от 403?
  2. Что такое RBAC?
  3. Как проверить владельца ресурса?