在RedwoodJS项目中实施灵活的授权
#javascript #网络开发人员 #fullstack #redwood

RedwoodJS是用于构建现代Web应用程序的自以为是的全栈框架。它为您做出了一些最关键的决策 - 例如使用REACT用于UI开发,用于API的GraphQL以及用于数据库编程的PRISMA等 - 因此,您可以停止选择选择并专注于构建应用程序。

关于授权,Redwoodjs对RBAC(基于角色的访问控制)具有内置支持,该支持可以很好地适用于简单的应用程序,但可以轻松地达到其复杂场景的极限。在本文中,我们将探讨实施授权的另一种方法,这可能使您感到惊讶。我们没有在GraphQL API和服务代码级别上进行操作,而是将其移至ORM层。

场景

每个Redwoodjs用户都通过官方教程开始,这是一个简单的博客应用程序。它具有代表博客文章的User系统,该模型代表博客文章,该文章与Comment模型有一对多的关系,代表对帖子的评论。

该教程通过实施以下要求来证明RBAC:

  • 任何用户都可以阅读帖子和评论。
  • 具有“ admin”角色的用户可以创建帖子,并更新/删除自己的帖子。
  • 任何用户都可以创建和阅读评论。
  • 具有“主持人”角色的用户可以删除注释。

使用@requireAuth GraphQl指令和requireAuth服务助手很好地实现了这些要求。例如:

// api/src/graphql/adminPosts.sdl.js

type Mutation {
  createPost(input: CreatePostInput!): Post! @requireAuth(roles: ["admin"])
  updatePost(id: Int!, input: UpdatePostInput!): Post! @requireAuth(roles: ["admin"])
  deletePost(id: Int!): Post! @requireAuth(roles: ["admin"])
}
// api/src/services/comments/comments.js

export const deleteComment = ({ id }) => {
  requireAuth({ roles: 'moderator' })
  return db.comment.delete({
    where: { id },
  });
}

现在,让我们稍微调整要求,然后通过添加以下规则来超越简单的RBAC:

  • Post具有额外的published属性,指示它是否已发布。
  • 无法查看未发表的帖子(Admin UI中的“ Admin”用户除外)。
  • 评论无法在未发表的帖子中查看或创建。

我们的授权模型已从纯RBAC演变为RBAC和ABAC的混合物(基于属性的访问控制)。

扩展当前实施

实现新要求的最直接方法是在服务层中添加更多逻辑:

// api/src/services/posts/posts.js

export const posts = (...args) => {
  return db.post.findMany(
+ { 
+   where: { published: true }
+ }
  );
}

// api/src/services/comments/comments.js

export const comments = ({ postId }) => {
  return db.comment.findMany({
    where: {
-     postId      
+     AND: [
+       { postId },
+       { post: { published: true } }
+     ]
    }
  });
}

export const createComment = ({ input }) => {
+ const post = await db.post.findUnique({ where: { id: input.postId } });
+ if (!post.published) {
+   throw new ForbiddenError('Cannot create comment for unpublished post');
+ }
  return db.comment.create({
    data: input,
  });
}

尽管这有效,但我们的授权逻辑已经开始渗入代码库中的许多地方,并且系统更难推理和维护。

让我们尝试其他方法。

让Orm进行繁重的举重

所有授权逻辑最终都会做一件事:防止不应该读取或修改的数据被读取或修改 - 换句话说,这是数据过滤器。谁住在数据附近?是的,ORM!为什么不让它为我们繁重呢?

在这篇文章中,我们将实现使用ZenStack工具包在ORM层中实施授权的目标。 Zenstack是一个建在Prisma上方的Nodejs工具包。它在许多方面扩展了Prisma的力量,“添加访问控制”是最重要的。

这就是它的发展。

建模部分

Zenstack使用称为“ Zmodel”的架构语言来建模数据和访问控制策略。 Zmodel是Prisma模式语言的超集。它的数据建模部分与Prisma中基本相同,但具有表达访问策略规则的其他属性(下面显示的@@allow属性)。

这是我们的PostComment模型在zmodel中的样子:

// api/schema.zmodel

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  body      String
  comments  Comment[]
  user      User     @relation(fields: [userId], references: [id])
  userId    Int
  createdAt DateTime @default(now())
  published Boolean @default(false)

  // 🔐 Admin user can do everything to his own posts
  @@allow('all', auth().roles == 'admin' && auth() == user)

  // 🔐 Posts are visible to everyone if published
  @@allow('read', published)
}

model Comment {
  id        Int      @id @default(autoincrement())
  name      String
  body      String
  post      Post     @relation(fields: [postId], references: [id])
  postId    Int
  createdAt DateTime @default(now())

  // 🔐 Moderator user can do everything to comments
  @@allow('all', auth().roles == 'moderator')

  // 🔐 Everyone is allowed to view and create comments for published posts
  @@allow('create,read', post.published)
}

就是这样!我们已经在架构中表达了所有授权规则,集中和简洁。规则语法的灵活性使您能够实现不严格遵循任何预定义范式的特定授权模型。现在,您的模式是两个最重要且密切相关的事物的真实性来源:数据和授权。

zenstack CLI将Zmodel汇编为纯Prisma模式(剥夺了策略部分),该模式可用于您当前的Prisma Workflows(生成,迁移等)中。访问策略被转换为元数据对象,该对象将支持在运行时执行策略规则。

运行时部分

在服务代码中,我们将使用Zenstack的运行时API来创建了解访问策略的“增强” PRISMA客户端实例。这样的情况是透明的代理,因此它们具有与原始Prisma客户端相同的API。他们拦截了CRUD电话,应用政策检查并根据需要进行注射。

默认情况下,增强的Prisma客户端拒绝所有CRUD调用。您必须设置规则以授予权限。

我们可以通过两个步骤采用它:

1.添加助手为当前用户创建增强的客户端

// api/src/lib/db.js

import { withPolicy } from '@zenstackhq/runtime';

/*
 * Returns ZenStack wrapped Prisma Client with access policies enabled.
 */
export function authDb() {
  return withPolicy(db, { user: context.currentUser });
}

2.更改服务代码以使用增强的客户端

// api/src/services/posts/posts.js

export const posts = () => {
  return authDb().post.findMany();
}
// api/src/services/comments/comments.js

export const comments = ({ postId }) => {
  return authDb.comment.findMany();
}

export const createComment = ({ input }) => {
  return authDb().comment.create({
    data: input,
  });
}

每次致电authDb()而不是缓存结果至关重要。

您可以看到,除了使用authDb()助手之外,我们已经从服务层删除了所有命令授权代码。但是,我们的后端仍然安全,因为访问策略是由ORM执行的。

结论

授权是一个非常具有挑战性的话题,无论您选择哪种框架。这篇文章展示了Zenstack如何以集中,简洁和声明的方式帮助您实施授权。您可以在下面找到完整的项目代码:

https://github.com/zenstackhq/sample-redwood-blog

如果您想了解有关Zenstack的更多信息,请查看此全面的简介here

我们基于以下信念创建了Zenstack:

由于授权主要是关于数据,因此应将其与数据一起建模并靠近数据。

您是否共享相同的观点?是或否,加入我们的Discord community,让我们知道您的想法!