From 2c52084129b23efc3fd68d9311b38fb0af426e23 Mon Sep 17 00:00:00 2001 From: Nurzhan Saktaganov Date: Mon, 10 Sep 2018 18:16:26 +0300 Subject: [PATCH] initial commit --- LICENSE.md | 21 +++++ README.md | 156 ++++++++++++++++++++++++++++++++++++ package.json | 25 ++++++ src/Guard.jsx | 26 ++++++ src/Requirement.js | 14 ++++ src/RequirementAll.js | 9 +++ src/RequirementAny.js | 9 +++ src/RequirementNot.js | 14 ++++ src/RequirementPredicate.js | 26 ++++++ src/createContext.jsx | 77 ++++++++++++++++++ src/guardFactory.jsx | 18 +++++ src/index.js | 31 +++++++ src/protect.jsx | 35 ++++++++ 13 files changed, 461 insertions(+) create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 package.json create mode 100644 src/Guard.jsx create mode 100644 src/Requirement.js create mode 100644 src/RequirementAll.js create mode 100644 src/RequirementAny.js create mode 100644 src/RequirementNot.js create mode 100644 src/RequirementPredicate.js create mode 100644 src/createContext.jsx create mode 100644 src/guardFactory.jsx create mode 100644 src/index.js create mode 100644 src/protect.jsx diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..e9e74c7 --- /dev/null +++ b/LICENSE.md @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a66094 --- /dev/null +++ b/README.md @@ -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 ( + + + + + + + + + + + + + + + + + + ); + } +} + +``` + +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 + + + +``` + +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(...); + + + ... + + +``` + +## Demo ## + +To see demos please visit [TODO]. + + +## License ## +[MIT](./LICENSE.md) diff --git a/package.json b/package.json new file mode 100644 index 0000000..258354e --- /dev/null +++ b/package.json @@ -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 ", + "license": "MIT", + "bugs": { + "url": "https://github.com/nurzhan-saktaganov/react-rbac-guard/issues" + }, + "homepage": "https://github.com/nurzhan-saktaganov/react-rbac-guard#readme" +} diff --git a/src/Guard.jsx b/src/Guard.jsx new file mode 100644 index 0000000..1c0045a --- /dev/null +++ b/src/Guard.jsx @@ -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 ( + + {credentials => + requirement.isSatisfied(credentials) ? this.props.children : null + } + + ); + } +} + +export { Guard, Provider }; diff --git a/src/Requirement.js b/src/Requirement.js new file mode 100644 index 0000000..1f5854d --- /dev/null +++ b/src/Requirement.js @@ -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; diff --git a/src/RequirementAll.js b/src/RequirementAll.js new file mode 100644 index 0000000..ec04af3 --- /dev/null +++ b/src/RequirementAll.js @@ -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; diff --git a/src/RequirementAny.js b/src/RequirementAny.js new file mode 100644 index 0000000..64b4571 --- /dev/null +++ b/src/RequirementAny.js @@ -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; diff --git a/src/RequirementNot.js b/src/RequirementNot.js new file mode 100644 index 0000000..5cc2abc --- /dev/null +++ b/src/RequirementNot.js @@ -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; diff --git a/src/RequirementPredicate.js b/src/RequirementPredicate.js new file mode 100644 index 0000000..43d39b2 --- /dev/null +++ b/src/RequirementPredicate.js @@ -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; diff --git a/src/createContext.jsx b/src/createContext.jsx new file mode 100644 index 0000000..1949042 --- /dev/null +++ b/src/createContext.jsx @@ -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); +} diff --git a/src/guardFactory.jsx b/src/guardFactory.jsx new file mode 100644 index 0000000..eec9c3d --- /dev/null +++ b/src/guardFactory.jsx @@ -0,0 +1,18 @@ +import React, { Component } from "react"; + +import Requirement from "./Requirement"; +import { Guard } from "./Guard"; + +function guardFactory(requirement) { + if (!(requirement instanceof Requirement)) { + throw new TypeError("requirement is expected to be Requirement instance"); + } + + return class extends Component { + render() { + return {this.props.children}; + } + }; +} + +export default guardFactory; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..d9e2509 --- /dev/null +++ b/src/index.js @@ -0,0 +1,31 @@ +import Requirement from "./Requirement"; +import RequirementAll from "./RequirementAll"; +import RequirementAny from "./RequirementAny"; +import RequirementNot from "./RequirementNot"; + +import { Guard, Provider as CredentialProvider } from "./Guard"; +import guardFactory from "./guardFactory"; +import protect from "./protect"; + +function all(...requirements) { + return new RequirementAll(...requirements); +} + +function any(...requirements) { + return new RequirementAny(...requirements); +} + +function not(requirement) { + return new RequirementNot(requirement); +} + +export { + Requirement, + all, + any, + not, + CredentialProvider, + Guard, + guardFactory, + protect +}; diff --git a/src/protect.jsx b/src/protect.jsx new file mode 100644 index 0000000..ae5a2c7 --- /dev/null +++ b/src/protect.jsx @@ -0,0 +1,35 @@ +import React, { Component } from "react"; + +import Requirement from "./Requirement"; +import { Guard } from "./Guard"; + +function protect(requirement) { + if (!(requirement instanceof Requirement)) { + throw new TypeError("requirement is expected to be Requirement instance"); + } + + return function(ComponentToProtect) { + const isComponent = + ComponentToProtect && + ComponentToProtect.prototype && + ComponentToProtect.prototype instanceof Component; + + if (!isComponent) { + throw new TypeError("expected a class derived from React.Component"); + } + + return class extends Component { + render() { + return ( + + + {this.props.children} + + + ); + } + }; + }; +} + +export default protect;