Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
nurzhan-saktaganov committed Oct 27, 2018
0 parents commit 2c52084
Show file tree
Hide file tree
Showing 13 changed files with 461 additions and 0 deletions.
21 changes: 21 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2018 Nurzhan Saktaganov

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
156 changes: 156 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# React RBAC Guard #

`react-rbac-guard` is a module allowing to manage visibility of particular components depending on user credentials (or current permissions set). Module uses approach that was inspired by "react-redux-connect" module.

## Dependensies ##

React RBAC requires either React [new context API](https://reactjs.org/docs/context.html) or React [legacy context API](https://reactjs.org/docs/legacy-context.html) support.
Module tries to use new context API (React version >= 16.3) if available. Otherwise fallbacks to legacy context API. Legacy context API has problem with returning `false` from `shouldComponentUpdate` in intermediate components (see docs).

## Installation ##

```bash
#TODO
```

## Integration in 5 easy steps ##

1. Define your own class derived from `Requirement` class. It must implement `isSatisfied` method that takes credentials as parameter. The method must return `true` if requirement is satisfied by credentials and `false` otherwise
```js
import { Requirement } from "react-rbac-guard";

class Need extends Requirement {
constructor(permission) {
super();
this.permission = permission;
}

isSatisfied(credentials) {
// assume credentilas is an object
return credentials[this.permission] ? true : false;
}
}

```

2. Create requirements
```js
const NeedManagePost = new Need("CanManagePost");
const NeedManageComment = new Need("CanManageComment");
const NeedManageUser = new Need("CanManageUser");
```

3. Create guards
```js
import { guardFactory } from "react-rbac-guard";

const PostManager = guardFactory(NeedManagePost);
const CommentManager = guardFactory(NeedManageComment);
const UserManager = guardFactory(NeedManageUser);
```

4. Provide credentials via `CredentialProvider` and use guards as components
```jsx
import { CredentialProvider } from "react-rbac-guard";

class App extends Component {
render() {
const credentials = {}; // you have to provide it

return (
<CredentialProvider value={credentials}>

<PostManager>
<button>Edit Post</button>
<button>Delete Post</button>
</PostManager>

<CommentManager>
<button>Edit Comment</button>
<button>Delete Comment</button>
</CommentManager>

<UserManager>
<button>Block User</button>
</UserManager>

</CredentialProvider>
);
}
}

```

5. Enjoy!

## Capabilities ##

1. You can use `all`, `any`, `not` functions to combine requirements.
```jsx
// Let's assume we have NeedAdmin, NeedManager, NeedUser, NeedGuest requirements.
// You can produce new ones by combining them.
import { any, not } from "react-rbac-gurad";

const NeedAuthorized = not(NeedGuest);
const NeedExtendedRights = any(NeedAdmin, NeedManager);

```
In other words, you can define arbitrary predicate (in terms of mathematical logic) based on your _requirements_.

2. You can use Guard component directly (without guardFactory).
```jsx
<Guard requirement={NeedAdmin}>
<button>Restart Server</button>
</Guard>
```

3. You can use `protect` to protect your components (it behaves like `connect` from `react-redux-connect` ).
```jsx
import { protect } from "react-rbac-guard";

class CustomersList extends Component {
...
}

// exports decorated component (like connect in "react-redux-connect")
export default protect(NeedExtendedRights)(CustomersList);
```
or to protect external components
```jsx
import { Button } from "react-bootstrap";
import { protect } from "react-rbac-guard";

const SignUpButton = protect(NeedGuest)(Button);
const SignOutButton = protect(NeedAuthrized)(Button);
```

4. It's also possible to make a decision inside credentials object
```jsx
class Credentials {
satisfies(requirement) {
return ...// return boolean depending on requirement
}
}

class MyRequirement extends Requirement {
...
isSatisfied(credentials) {
return credentilas.satisfies(this);
}
}

const credentials = new Credentials(...);

<CredentialProvider value={credentials}>
...
</CredentialProvider>

```

## Demo ##

To see demos please visit [TODO].


## License ##
[MIT](./LICENSE.md)
25 changes: 25 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "react-rbac-guard",
"version": "0.0.1",
"description": "Module allowing to manage visibility of particular components depending on user credentials",
"main": "src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nurzhan-saktaganov/react-rbac-guard.git"
},
"keywords": [
"rbac",
"guard",
"protect",
"congitional render"
],
"author": "Nurzhan Saktaganov <nur92world@mail.ru>",
"license": "MIT",
"bugs": {
"url": "https://github.com/nurzhan-saktaganov/react-rbac-guard/issues"
},
"homepage": "https://github.com/nurzhan-saktaganov/react-rbac-guard#readme"
}
26 changes: 26 additions & 0 deletions src/Guard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { Component } from "react";

import createReactContext from "./createContext.jsx";
import Requirement from "./Requirement";

const { Provider, Consumer } = createReactContext();

class Guard extends Component {
render() {
const { requirement } = this.props;

if (!(requirement instanceof Requirement)) {
throw new TypeError("requirement is expected to be Requirement instance");
}

return (
<Consumer>
{credentials =>
requirement.isSatisfied(credentials) ? this.props.children : null
}
</Consumer>
);
}
}

export { Guard, Provider };
14 changes: 14 additions & 0 deletions src/Requirement.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class Requirement {
constructor() {
if (new.target === Requirement) {
throw new TypeError("Cannot construct Requirement instances directly");
}
this.isSatisfied = this.isSatisfied.bind(this);
}

isSatisfied(credentials) {
throw new Error("Must override isSatisfied");
}
}

export default Requirement;
9 changes: 9 additions & 0 deletions src/RequirementAll.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import RequirementPredicate from "./RequirementPredicate";

class RequirementAll extends RequirementPredicate {
isSatisfied(credentials) {
return this.requirements.every(r => r.isSatisfied(credentials));
}
}

export default RequirementAll;
9 changes: 9 additions & 0 deletions src/RequirementAny.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import RequirementPredicate from "./RequirementPredicate";

class RequirementAny extends RequirementPredicate {
isSatisfied(credentials) {
return this.requirements.some(r => r.isSatisfied(credentials));
}
}

export default RequirementAny;
14 changes: 14 additions & 0 deletions src/RequirementNot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import RequirementPredicate from "./RequirementPredicate";

class RequirementNot extends RequirementPredicate {
constructor(requirement) {
super(...[requirement]);
this.requirement = requirement;
}

isSatisfied(credentials) {
return !this.requirement.isSatisfied(credentials);
}
}

export default RequirementNot;
26 changes: 26 additions & 0 deletions src/RequirementPredicate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Requirement from "./Requirement";

class RequirementPredicate extends Requirement {
constructor(...requirements) {
super();
if (new.target === RequirementPredicate) {
throw new TypeError(
"Cannot construct RequirementPredicate instances directly"
);
}

if (requirements.length === 0) {
throw new Error("No requirement has been provided");
}

if (requirements.some(r => !(r instanceof Requirement))) {
throw new Error(
"requirements are expected to be instances of 'Requirement'"
);
}

this.requirements = requirements;
}
}

export default RequirementPredicate;
77 changes: 77 additions & 0 deletions src/createContext.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React, { Component } from "react";

import PropTypes from "prop-types";

function generateToken(length) {
const symbolSet =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let token = "";

for (let i = 0; i < length; i++) {
const position = Math.floor(Math.random() * symbolSet.length);
token += symbolSet.charAt(position);
}

return token;
}

let reservedToken = {};
function uniqueToken(length) {
let token;
do {
token = generateToken(length);
} while (reservedToken[token]);
reservedToken[token] = true;
return token;
}

// This is just polyfill to emulate new context API using legacy API.
// NOTE: it does not support default value.
function createContext(defaultValue) {
const TOKEN_LENGTH = 32;
const token = uniqueToken(TOKEN_LENGTH);
let contextTypes = {};
contextTypes[token] = PropTypes.any;

class Consumer extends Component {
render() {
const credentials = this.context[token];

const { children } = this.props;

if (!children) {
return null;
}

if (Array.isArray(children) || typeof children !== "function") {
throw new TypeError("Consumer expected exactly one function child");
}

return this.props.children(credentials);
}
}

Consumer.contextTypes = contextTypes;

class Provider extends Component {
getChildContext() {
let context = {};
context[token] = this.props.value;
return context;
}

render() {
return this.props.children;
}
}

Provider.childContextTypes = contextTypes;

return { Provider, Consumer };
}

export default function(defaultValue) {
// Try to use new context API, fallback to legacy API otherwise.
const func = React.createContext || createContext;
return func(defaultValue);
}
Loading

0 comments on commit 2c52084

Please sign in to comment.