-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 2c52084
Showing
13 changed files
with
461 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
Oops, something went wrong.