如何在Express应用程序中实施授权
#node #express #accesscontrol #rbac

介绍

很难想象没有明确框架的Node.js生态系统。它不仅是最受欢迎的框架之一,而且还启发了许多其他框架。如今,几乎每个现代网络框架都包含起源于Express的风味。

如果您曾经在生产中使用Express,则很有可能需要使用授权/权限解决方案嵌入它 - 可以确定用户在快递端点内可以做什么和不能做什么的中间件

即使它总是开始简单,正如单行票证所述:在项目中添加RBAC,它始终以编写大量代码和随之而来的烦人的错误。

在本文中,我建议一种应对明确申请的权限的新的,有效的方法。我将演示如何使用Permit为您的Express应用程序创建精益和快速的授权中间件。到本文结尾,您将能够使用更少的代码和错误为应用程序实现更好的授权解决方案。让我们潜水。

自制授权问题

我们描述的授权烂摊子的主要原因之一是中间件内的策略逻辑的用法。在理想的世界中,中间件只会在现实世界中执行政策,但它也负责政策逻辑。让我们以一个经典的代码示例:在开始时,您只有一行检查用户角色并验证DB的权限:

if (user.role === 'admin') {
    next();
} else {
    res.status(403).send('Forbidden');
}

但是,您需要添加一个新角色,因此您在中间件中添加了新行:

if (user.role === 'admin' || user.role === 'manager') {
    next();
} else {
    res.status(403).send('Forbidden');
}

然后,您需要添加新的权限,然后在中间件中添加新行:

if (user.role === 'admin' || user.role === 'manager' || user.role === 'user' && user.permissions.includes('read')) {
    next();
} else {
    res.status(403).send('Forbidden');
}

继续这种模式,您最终会得到一个很难维护且难以测试的巨大中间件。有两种最佳实践可以帮助您避免这种混乱:

  1. 以不取决于应用程序实现的方式设计您的权限模型。而不是在项目中添加RBAC,而是考虑设计应用程序的权限模型,然后才能实现与您相关的详细信息。

  2. 不要在中间件内使用策略逻辑。而不是使用该专用服务,该服务将负责政策逻辑。我们将讨论您如何通过plum.io解决它。

我们将从设计您的应用程序权限模型开始。为此,让我们看一下演示应用程序:

演示应用程序

出于本文的目的,我们将使用Express-based blogging application。该应用程序代码可在GitHub上找到,我们鼓励您克隆它并进行交互式工作。该代码由三个文件组成:

app.js-主要的应用程序文件。由工作博客应用程序的相关API端点组成(实际功能只是模拟)。

...
app.get('/post', mockPublic);
app.get('/post/:id', mockPublic);
app.post('/post', authentication, mockPrivate);
app.put('/post/:id', authentication, mockPrivate);
app.delete('/post/:id', authentication, mockPrivate);

app.get('/author', mockPublic);
app.get('/author/:id', mockPublic);
app.post('/author', authentication, mockPrivate);
app.put('/author/:id', authentication, mockPrivate);
app.delete('/author/:id', authentication, mockPrivate);

app.get('/comment', mockPublic);
app.get('/comment/:id', mockPublic);
app.post('/comment', authentication, mockPrivate);
app.put('/comment/:id', authentication, mockPrivate);
app.delete('/comment/:id', authentication, mockPrivate);
...

middleware/authentication.js-身份验证中间件。一个简单的身份验证模拟将将用户对象从JWT令牌添加到请求对象。

...
const authentication = (req, res, next) => {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
    if (token == null) return res.sendStatus(401);

    jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
        if (err) return res.sendStatus(403);
        req.user = user;
        next();
    });
};
...

app.test.js-测试文件。一个简单的测试文件,将验证该应用程序是否按预期工作。

...
// Init a token for authenticated requests
const token = 'Bearer ' + jwt.sign({ username: 'admin@permit-blog.app' }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
...
test('CRUD Posts', async () => {
    await request(app).get('/post').expect(200);
    await request(app).get('/post/1').expect(200);
    await request(app).post('/post').expect(401);
    await request(app).put('/post/1').expect(401);
    await request(app).delete('/post/1').expect(401);

    await request(app).post('/post').set('Authorization', token).expect(200);
    await request(app).put('/post/1').set('Authorization', token).expect(200);
    await request(app).delete('/post/1').set('Authorization', token).expect(200);
});
...

要在行动中看到它,请运行以下命令(假设您的计算机上有Node.JS and NPM installed):

1.克隆您所需位置的存储库

git clone -b tutorial git@github.com:permitio/permit-express-tutorial.git

2.安装依赖项

npm install

3.运行的测试将验证一切都按预期工作

npm run test

如您在输出中所看到的,测试正在通过,并且应用程序正常工作。

设计许可模型

将权限集成到应用程序中时,仔细设计模型并确定应授予用户的权限至关重要。

为了实现这一目标,我们考虑了三个关键组成部分:用户的身份和角色 Resources 以及 Action 可以在这些资源上执行。

这些组件的合并称为“ 策略”。

牢记这些实体,我们可以将应用要求转化为反映所需权限的特定条件和政策。

让我们分析演示博客的API:

  • 角色可以分配给身份验证的用户,例如管理员,作者和评论者。

  • 操作可以与HTTP方法(获取,创建,更新,删除,补丁)相关联。

  • 资源表示我们需要管理的各种端点,包括帖子,作者,评论等。

通过映射所有这些元素,我们可以创建下表:

角色 资源 动作
管理员 post get
作者 作者 创建
评论者 评论 更新
delete
补丁

建立了基本角色,资源和行动后,我们现在可以根据特权的原则来绘制所需的权限:

  • 管理员有能力对任何资源执行任何操作。

  • 作家可以创建,更新,修补和删除帖子以及检索注释。

  • 评论者可以检索并创建评论。

通过遵守这些定义的条件,我们确保许可模型遵循最低特权的原则,仅授予用户各自的角色和任务所需的必要访问权限。

用许可证配置权限模型

现在我们的模型已经设计了,是时候将其付诸实践了!如前所述,we don't want to mix the policy code with the API logic。为了维护干净的结构,我们将使用专门设计用于定义和配置策略的单独服务。这种方法允许该服务专注于在应用程序代码处理关键应用程序逻辑时执行权限。

许可证,提供授权-As-a-Service,简化的权限配置和执行,并确保您的代码保持井井有条,并控制了对应用程序的访问。该平台提供了广泛的免费层,并以自助服务为基础。

要配置所需的应用程序权限,请执行以下步骤:

1.登录以允许.io here

2.登录后,导航到策略页面并创建以下角色:

Create roles blog.png
3.通过创建必要的资源及其各自的行动来进行:

Resources Blog.png
4.通过选择相关复选框来实现所需条件来自定义策略表:

Policy editor blog.png
5.要完成配置,创建三个用户帐户并使用用户屏幕分配适当的角色:

Users blog.png

就是这样!现在我们已经建立了我们的权限,是时候将它们与我们的Express应用程序集成在一起了。

在申请中使用许可证sdk

现在,我们在许可证中配置了权限模型,我们可以在应用程序中使用它。如您所记得的那样,我们的主要目标是保持中间件尽可能瘦。使其仅检查并对许可证中的配置执行权限。

安装和初始化许可证。IOSDK

1.在我们开始使用许可证之前,我们需要安装许可证sdk。

⁠npm install permitio

2.现在我们已经安装了SDK,我们需要从periber.io获取SDK键。转到Settings页面,然后抓住SDK键。

sdk_key.png
3.让我们将SDK键保存在项目根部的.env文件中

⁠PERMIT_SDK_SECRET=YOUR_SDK_KEY

4.使用我们的SDK密钥配置了,让我们创建一个名为middleware/authorization.js的新文件,然后向其添加以下代码:

const permit = require('permitio');

const permit = new Permit({
    token: process.env.PERMIT_SDK_SECRET,
    pdp: process.env.PDP_URL
});

const authorization = (req, res, next) => {

}

module.exports = authorization;

将授权添加到应用程序

1.带有空的授权中间件设置,我们可以将中间件添加到app.js文件中相关受保护的路由中。

const authorization = require('./middleware/authorization');

...
app.post('/post', authentication, authorization, mockPrivate);
app.put('/post/:id', authentication, authorization, mockPrivate);
app.delete('/post/:id', authentication, authorization, mockPrivate);
...
app.post('/author', authentication, authorization, mockPrivate);
app.put('/author/:id', authentication, authorization, mockPrivate);
app.delete('/author/:id', authentication, authorization, mockPrivate);
...
app.post('/comment', authentication, authorization, mockPrivate);
app.put('/comment/:id', authentication, authorization, mockPrivate);
app.delete('/comment/:id', authentication, authorization, mockPrivate);
...

2.让我们再次运行测试,看看一切仍然通过(请记住我们的授权逻辑仍然为空)。

npm test

3.现在,让我们将逻辑添加到中间件中。在功能中,添加呼叫允许。与我们要调用的用户,操作和资源检查。

...
const action = method.toLowerCase(),
    url_parts = url.split('/'),
    type = url_parts[1],
    key = url_parts[2] || null;
const allowed = await permit.check(username, action, {
    type,
    key,
    attributes: body || {}
});

if (!allowed) {
    res.sendStatus(403);
    return;
}
next();
...

4.让我们重新运行测试并查看失败的结果,因为我们的测试配置的用户没有管理员角色:

npm test
#  FAIL  ./app.test.js
#   API Test
#     ✕ CRUD Post (802 ms)
#     ✕ CRUD Author (615 ms)
#     ✕ CRUD Comment (664 ms)
#   ● API Test › CRUD Post
#     expected 200 "OK", got 403 "Forbidden"

5.要修复测试,让我们将测试中的用户名更改为:admin@permit-blog.app

...
const token = 'Bearer ' + jwt.sign({ username: 'admin@permit-blog.app' }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
...

6.测试现在应该通过!<​​br>

npm test

测试权限模型

在这一点上,当我们保护所有端点时,我们可以测试权限模型。

1.让我们在测试文件开始时为不同用户添加更多令牌。

...
    const token = 'Bearer ' + jwt.sign({ username: 'admin@permit-blog.app' }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
    const writer = 'Bearer ' + jwt.sign({ username: 'writer@permit-blog.app' }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
    const commenter = 'Bearer ' + jwt.sign({ username: 'commenter@permit-blog.app' }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
...

2.并为作者和评论者用户添加测试。

...
await request(app).post('/post').set('Authorization', writer).expect(200);
await request(app).put('/post/1').set('Authorization', writer).expect(200);
await request(app).delete('/post/1').set('Authorization', writer).expect(200);
await request(app).post('/post').set('Authorization', commenter).expect(403);
await request(app).put('/post/1').set('Authorization', commenter).expect(403);
await request(app).delete('/post/1').set('Authorization', commenter).expect(403);
...

通过ABAC提高授权

注意:执行ABAC策略需要部署本地PDP-要开始,请遵循本指南。

将我们的身份,资源和动作简化为角色,资源类型和行动名称的简洁列表,可以在现实世界中构成挑战,如我们上一个示例所示。

但是,如果我们遇到更复杂的要求,基于角色的简单访问控制(RBAC)可能不够。例如,如果我们想为博客内容创建批准流,仅允许批准的作者发布文章并限制特定地理位置的评论,或应用其他细粒度限制,我们需要考虑Attribute-Based Access Control (ABAC)

让我们通过合并属性来深入研究这些条件的细节:

  • 管理用户具有不受限制的访问权限,使他们能够对任何资源执行任何操作。

  • 作家有能力编辑和删除帖子,但他们只能创建未发表的帖子。

  • 批准的作家享受创建任何类型的帖子的自由。

  • 允许评论者创建评论。

通过将属性引入RBAC模型can be a complex task来实现ABAC。但是,允许io通过促进配置更改以支持新的许可模型而无需修改应用程序代码来简化此过程。

实施ABAC的有效方法涉及利用资源集和用户集,这些资源集和用户集是根据结合用户和资源属性的条件构建的。让我们探讨允许如何配置这些政策:

1.首先,要在许可证中启用ABAC,请转到策略编辑器中的ABAC Rules选项卡并切换ABAC Options Switch。

2.首先配置相关资源的属性。访问策略编辑器,单击资源表上的三个点,然后选择“添加属性”。

Resource attribute config blog.png
3.具有定义的资源属性,在策略编辑器中创建资源集以建立必要的条件。

Resource set config blog.png
4.要将策略与用户属性匹配,请还配置用户属性。访问用户屏幕,导航到“属性”选项卡,然后创建所需的批准属性。

Create user attributes blog.png
5.在作者角色中创建一个新用户,并在其个人资料中分配批准的属性。该用户将作为稍后评估ABAC策略的参考。让我们使用以下用户名approved_writer@permit-blog.app

Create roles ABAC.png
6.现在可以允许识别自定义用户属性在策略编辑器中创建用户集以适应这些条件。

User Sets blog.png
7.符合条件,请调整策略表中的策略配置,以与新定义的条件保持一致。

Policy editor ABAC blog.png

通过采用这种方法,我们可以执行权限,而无需重写我们的应用程序代码。我们最初为私人路线开发的中间件将通过基于我们分配的新策略模型配置执行权限来无缝地继续其角色。

将测试添加到我们的新ABAC政策中

  1. 通过运行我们当前的测试,您可以看到现在我们的作者无法创建帖子。

    npm test
    #  FAIL  ./app.test.js (8.677 s)
    #   API Test
    #     ✓ CRUD Post (2909 ms)
    #     ✕ CRUD Post by writer and commenter (893 ms)
    #     ✓ CRUD Author (2146 ms)
    #     ✓ CRUD Comment (2256 ms)
    #   ● API Test › CRUD Post by writer and commenter
    #     expected 200 "OK", got 403 "Forbidden"
    #       24 |
    #       25 |     test('CRUD Post by writer and commenter', async () => {
    #     > 26 |         await request(app).post('/post').set('Authorization', writer).expect(200);
    
  2. 让我们通过传递具有不同已发布属性的对象来修复这些测试。

    await request(app).post('/post').send({
    published: false,
    }).set('Authorization', writer).expect(200);
    await request(app).put('/post/1').send({
    published: false,
    }).set('Authorization', writer).expect(200);    
    
  3. 现在,让我们再次进行测试,看看它们现在通过了。

    npm test
    #  PASS./ app.test.js
    #   API Test
    #     ✓ CRUD Post(132 ms)
    #     ✓ CRUD Post by writer and commenter(212 ms)
    #     ✓ CRUD Author(92 ms)
    #     ✓ CRUD Comment(87 ms)
    
  4. 现在,让我们测试已批准的用户。我们将为批准的用户添加一个新的测试。

// Add a new token for the approved user we just created
const approvedWriter = 'Bearer ' + jwt.sign({ username: 'approved_writer@permit-blog.app' }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
...
// Add a test case
test('CRUD Post by approved writer', async () => {
    await request(app).post('/post').send({
        published: true,
    }).set('Authorization', approved).expect(200);
    await request(app).put('/post/1').send({
        published: true,
    }).set('Authorization', approved).expect(200);
});
...

您可以看到,无需更改应用程序代码即可执行新的策略。许可证的ABAC功能使我们能够实施复杂的政策,而无需修改我们的申请代码。

下一步是什么?

到目前

下一步是分析应用程序的特定要求,并将可靠的许可模型纳入其中。如文章所示,它不必过于复杂。

我们为此博客开发的插件很容易使用。只需对其进行调整以适应您的应用程序的相关请求字段,您就可以了。

如果您的组织已经实施了授权模型,并且您有兴趣更多地了解有关有效扩展的授权模型,那么许多开发人员和授权专家讨论了建立和实施授权的过程。