Camas - 极简 React 权限管理库

266 阅读3分钟

Camas 是我最近在处理项目中的权限时写的一个极简权限管理库。API 的设计参考自一个 Ruby 的权限管理库 Pundit

定义权限规则

整个库围绕 Policy 的概念来定义权限。假设我们有一篇文章只允许当前用户是管理员或文章还没发布的时候才允许编辑,那么我们可以针对 post 这个模型定义这样一个 Policy:

class PostPolicy {
  constructor(context) {
    this.user = context.user;
  }

  canEdit(post) {
    return this.user.isAdmin || !post.isPublished;
  }
}

可以看到,这是一个很纯粹的类,这个类在实例化时会接受一个 context 作为参数,当前用户的信息等影响权限判断的内容都会放在 context 里。然后我们定义了一个 canEdit 方法,这个方法接受当前的文章作为参数,并且返回一个 boolean 来确定该文章能否被编辑。

当然,在 Policy 定义多了以后我们可以抽取一个基类来处理 context:

class BasePolicy {
  constructor(context) {
    this.user = context.user;
  }
}

class PostPolicy extends BasePolicy {
  update(post) {
    return this.user.isAdmin || !post.isPublished;
  }
}

使用 Policy

在使用 Policy 之前我们需要先在 React 应用的最外层增加一个 Provider 来初始化 context

import { Provider } from 'camas';

const App = () => {
  const currentUser = useCurrentUser(); // 获取当前用户信息的 hook,这里省略实现

  return (
    <Provider context={{ user: currentUser }}>
      <Routes />
    </Provider>
  )
};

使用 Hook

import { usePolicy } from 'camas';

const PostList = ({ posts }) => {
  const postPolicy = usePolicy(PostPolicy);

  return (
    <div>
      <ul>
        {posts.map(post => (
          <li>
            {post.title}
            {postPolicy.canEdit(post) && <span>Edit</span>}
          </li>
        ))}
      </ul>
    </div>
  );
};

usePolicy 这个 hook 会去实例化 Policy,并且注入 context,返回值就是 Policy 的实例。

使用组件

import { Authorize } from 'camas';

const PostList = ({ posts }) => {
  return (
    <div>
      <ul>
        {posts.map(post => (
          <li>
            {post.title}
            <Authorize with={PostPolicy} if={policy => policy.canEdit(post)}>
              <span>Edit</span>
            </Authorize>
          </li>
        ))}
      </ul>
    </div>
  );
};

同样的, Authorize 会实例化 Policy 并注入 context,然后你可以在 if 这个回调函数上拿到 Policy 的实例。

HOC 注入

如果你是用类组件的,Camas 还提供以 HOC 的方式注入 Policy:

import { withPolicies } from 'camas';

@withPolicies({
  postPolicy: PostPolicy,
})
class PostList extends React.Component {
  render() {
    const { posts, postPolicy } = this.props;
    return (
      <div>
        <ul>
          {posts.map(post => (
            <li>
              {post.title}
              {postPolicy.canEdit(post) && <span>Edit</span>}
            </li>
          ))}
        </ul>
      </div>
    );
  }
}

可以看到,withPolicies 接受一个 map 参数,这个 map 的 key 是注入的属性名,value 就是要注入的 Policy。

权限测试

因为 Policy 的定义就是一个很存粹的 JS 类,所以为 Policy 写测试也极其简单,这里以 jest 为例:

import PostPolicy from './PostPolicy';

describe('PostPolicy', () => {
  const admin = {
    isAdmin: true;
  };

  const normalUser = {
    isAdmin: false;
  }

  const publishedPost = {
    isPublished: true;
  }

  const unpublishedPost = {
    isPublished: false;
  }

  it("denies access if post is published", () => {
    expect(new PostPolicy({ user: normalUser }).update(publishedPost)).toBe(false);
  });

  it("grants access if post is unpublished", () => {
    expect(new PostPolicy({ user: normalUser }).update(unpublishedPost)).toBe(true);
  });

  it("grants access if post is published and user is an admin", () => {
    expect(new PostPolicy({ user: admin }).update(publishedPost)).toBe(true);
  });
});

结语

整个 Camas 库的实现和使用都非常简单,在 gzip 后只有 800 多个字节,另外对 TypeScript 也有完美的类型支持。