First commit

This commit is contained in:
Zixuan Liu 2019-11-17 15:15:25 +08:00
parent c305247a1e
commit a1bd00e542
22 changed files with 12074 additions and 1 deletions

16
.eslintrc Normal file
View File

@ -0,0 +1,16 @@
{
"parser": "@typescript-eslint/parser",
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier/@typescript-eslint"
],
"rules": {
"prettier/prettier": "error",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-non-null-assertion": "off"
}
}

21
.gitignore vendored Executable file
View File

@ -0,0 +1,21 @@
# OSX
#
.DS_Store
# node.js
#
node_modules/
npm-debug.log
yarn-error.log
# vs code
.vscode
# temp
.tmp
# IDEA or android-studio
*.iml
.idea
dist

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"semi": true,
"singleQuote": true,
"printWidth": 140
}

View File

@ -1 +1,12 @@
# casbin-editor
# Casbin-editor
Use the Casbin-editor to write your Casbin model and policy in your web browser.
It provides functionality such as syntax highlighting and code completion, just like an IDE for a programming language.
Try it at: http://casbin.org/editor/
## Getting started
```shell script
yarn install
yarn start
```

52
package.json Executable file
View File

@ -0,0 +1,52 @@
{
"name": "casbin-editor",
"version": "1.0.0",
"description": "",
"keywords": [],
"main": "src/index.js",
"dependencies": {
"@reach/router": "1.2.1",
"@types/react": "^16.9.11",
"@types/react-dom": "^16.9.4",
"casbin": "^3.0.6",
"codemirror": "5.48.4",
"normalize.css": "^8.0.1",
"react": "16.8.6",
"react-codemirror2": "6.0.0",
"react-dom": "16.8.6",
"styled-components": "4.3.2"
},
"devDependencies": {
"@types/codemirror": "^0.0.80",
"@types/reach__router": "^1.2.6",
"@types/styled-components": "^4.4.0",
"@typescript-eslint/eslint-plugin": "^2.7.0",
"@typescript-eslint/parser": "^2.7.0",
"eslint": "^6.6.0",
"eslint-config-prettier": "^6.5.0",
"eslint-plugin-prettier": "^3.1.1",
"husky": "^3.0.9",
"prettier": "^1.19.1",
"pretty-quick": "^2.0.1",
"react-scripts": "^3.2.0",
"typescript": "^3.7.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
"lint": "eslint . --ext .js,.ts,.tsx"
},
"husky": {
"hooks": {
"pre-commit": "yarn lint && pretty-quick --staged"
}
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}

50
public/index.html Executable file
View File

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<style>
.CodeMirror {
font-family: Arial, monospace;
font-size: 16px;
}
</style>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Casbin Online Editor</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -0,0 +1,163 @@
// @ts-nocheck
import CodeMirror from 'codemirror';
CodeMirror.defineMode('casbin-conf', function() {
function tokenBase(stream, state) {
const ch = stream.peek();
if (ch === '[') {
if (stream.match('[request_definition')) {
state.sec = 'r';
stream.skipTo(']');
stream.eat(']');
return 'header';
} else if (stream.match('[policy_definition')) {
state.sec = 'p';
stream.skipTo(']');
stream.eat(']');
return 'header';
} else if (stream.match('[role_definition')) {
state.sec = 'g';
stream.skipTo(']');
stream.eat(']');
return 'header';
} else if (stream.match('[policy_effect')) {
state.sec = 'e';
stream.skipTo(']');
stream.eat(']');
return 'header';
} else if (stream.match('[matchers')) {
state.sec = 'm';
stream.skipTo(']');
stream.eat(']');
return 'header';
} else {
state.sec = '';
stream.skipToEnd();
return '';
}
} else if (ch === '#') {
stream.skipToEnd();
return 'comment';
} else if (ch === '"') {
stream.skipToEnd();
stream.skipTo('"');
stream.eat('"');
return 'string';
} else if (ch === '=') {
// eslint-disable-next-line @typescript-eslint/camelcase
state.after_equal = true;
stream.eat('=');
}
if (stream.sol()) {
// eslint-disable-next-line @typescript-eslint/camelcase
state.after_equal = false;
}
if (state.sec === '') {
stream.skipToEnd();
return '';
}
if (stream.sol()) {
if (state.sec !== '') {
if ((state.sec === 'g' && stream.match(new RegExp('^g[2-9]?'))) || stream.match(state.sec)) {
if (stream.peek() === ' ' || stream.peek() === '=') {
return 'builtin';
} else {
state.sec = '';
stream.next();
return;
}
} else {
state.sec = '';
stream.next();
return;
}
} else {
stream.next();
return;
}
}
if (!state.after_equal) {
stream.next();
return;
}
if (state.sec === 'r' || state.sec === 'p') {
// Match: r = [sub], [obj], [act]
// p = [sub], [obj], [act]
if (state.comma) {
state.comma = false;
if (stream.match(new RegExp('^[_a-zA-Z][_a-zA-Z0-9]*'))) {
return 'property';
}
}
if (stream.eat(',') || stream.eat(' ')) {
state.comma = true;
return '';
}
} else if (state.sec === 'e') {
// Match: e = some(where (p.[eft] == allow))
if (state.dot) {
state.dot = false;
if (stream.match(new RegExp('^[_a-zA-Z][_a-zA-Z0-9]*'))) {
return 'property';
}
}
if (stream.eat('.')) {
state.dot = true;
return '';
}
// Match: e = some(where ([p].eft == allow))
if (stream.match('p') && stream.peek() === '.') {
return 'builtin';
}
// Match: e = [some]([where] (p.eft == allow))
if (stream.match('some') || stream.match('where') || stream.match('priority')) {
return 'keyword';
}
// Match: e = some(where (p.eft == [allow]))
if (stream.match('allow') || stream.match('deny')) {
return 'string';
}
} else if (state.sec === 'm') {
// Match: m = r.[sub] == p.[sub] && r.[obj] == p.[obj] && r.[act] == p.[act]
if (state.dot) {
state.dot = false;
if (stream.match(new RegExp('^[_a-zA-Z][_a-zA-Z0-9]*'))) {
return 'property';
}
}
if (stream.eat('.')) {
state.dot = true;
return '';
}
// Match: m = [r].sub == [p].sub && [r].obj == [p].obj && [r].act == [p].act
if ((stream.match('r') || stream.match('p')) && stream.peek() === '.') {
return 'builtin';
}
// Match: m = [g](r.sub, p.sub) && r.obj == p.obj && r.act == p.act
if (stream.match(new RegExp('^[_a-zA-Z][_a-zA-Z0-9]*')) && stream.peek() === '(') {
return 'def';
}
}
stream.next();
}
return {
startState: function() {
return { tokenize: tokenBase };
},
token: function(stream, state) {
return state.tokenize(stream, state);
}
};
});

View File

@ -0,0 +1,41 @@
// @ts-nocheck
import CodeMirror from 'codemirror';
CodeMirror.defineMode('casbin-csv', function() {
function tokenBase(stream, state) {
const ch = stream.peek();
if (ch === '#') {
stream.skipToEnd();
return 'comment';
} else if (ch === ',') {
stream.eat(',');
return '';
}
if (stream.sol() && stream.match('p')) {
return 'def';
}
if (stream.sol() && (stream.match('g2') || stream.match('g'))) {
return 'keyword';
}
if (stream.skipTo(',')) {
return 'string';
}
stream.skipToEnd();
return 'property';
// stream.next();
}
return {
startState: function() {
return { tokenize: tokenBase };
},
token: function(stream, state) {
return state.tokenize(stream, state);
}
};
});

View File

@ -0,0 +1,192 @@
/* eslint-disable */
const exampleModel = {
/////////////////////////////////////////////////////////////////////////
basic:
'[request_definition]\n' +
'r = sub, obj, act\n' +
'\n' +
'[policy_definition]\n' +
'p = sub, obj, act\n' +
'\n' +
'[policy_effect]\n' +
'e = some(where (p.eft == allow))\n' +
'\n' +
'[matchers]\n' +
'm = r.sub == p.sub && r.obj == p.obj && r.act == p.act',
/////////////////////////////////////////////////////////////////////////
basic_with_root:
'[request_definition]\n' +
'r = sub, obj, act\n' +
'\n' +
'[policy_definition]\n' +
'p = sub, obj, act\n' +
'\n' +
'[policy_effect]\n' +
'e = some(where (p.eft == allow))\n' +
'\n' +
'[matchers]\n' +
'm = r.sub == p.sub && r.obj == p.obj && r.act == p.act || r.sub == "root"',
/////////////////////////////////////////////////////////////////////////
basic_without_resources:
'[request_definition]\n' +
'r = sub, act\n' +
'\n' +
'[policy_definition]\n' +
'p = sub, act\n' +
'\n' +
'[policy_effect]\n' +
'e = some(where (p.eft == allow))\n' +
'\n' +
'[matchers]\n' +
'm = r.sub == p.sub && r.act == p.act',
/////////////////////////////////////////////////////////////////////////
basic_without_users:
'[request_definition]\n' +
'r = obj, act\n' +
'\n' +
'[policy_definition]\n' +
'p = obj, act\n' +
'\n' +
'[policy_effect]\n' +
'e = some(where (p.eft == allow))\n' +
'\n' +
'[matchers]\n' +
'm = r.obj == p.obj && r.act == p.act',
/////////////////////////////////////////////////////////////////////////
rbac:
'[request_definition]\n' +
'r = sub, obj, act\n' +
'\n' +
'[policy_definition]\n' +
'p = sub, obj, act\n' +
'\n' +
'[role_definition]\n' +
'g = _, _\n' +
'\n' +
'[policy_effect]\n' +
'e = some(where (p.eft == allow))\n' +
'\n' +
'[matchers]\n' +
'm = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act',
/////////////////////////////////////////////////////////////////////////
rbac_with_resource_roles:
'[request_definition]\n' +
'r = sub, obj, act\n' +
'\n' +
'[policy_definition]\n' +
'p = sub, obj, act\n' +
'\n' +
'[role_definition]\n' +
'g = _, _\n' +
'g2 = _, _\n' +
'\n' +
'[policy_effect]\n' +
'e = some(where (p.eft == allow))\n' +
'\n' +
'[matchers]\n' +
'm = g(r.sub, p.sub) && g2(r.obj, p.obj) && r.act == p.act',
/////////////////////////////////////////////////////////////////////////
rbac_with_domains:
'[request_definition]\n' +
'r = sub, dom, obj, act\n' +
'\n' +
'[policy_definition]\n' +
'p = sub, dom, obj, act\n' +
'\n' +
'[role_definition]\n' +
'g = _, _, _\n' +
'\n' +
'[policy_effect]\n' +
'e = some(where (p.eft == allow))\n' +
'\n' +
'[matchers]\n' +
'm = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act',
/////////////////////////////////////////////////////////////////////////
rbac_with_deny:
'[request_definition]\n' +
'r = sub, obj, act\n' +
'\n' +
'[policy_definition]\n' +
'p = sub, obj, act, eft\n' +
'\n' +
'[role_definition]\n' +
'g = _, _\n' +
'\n' +
'[policy_effect]\n' +
'e = some(where (p.eft == allow)) && !some(where (p.eft == deny))\n' +
'\n' +
'[matchers]\n' +
'm = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act',
/////////////////////////////////////////////////////////////////////////
abac:
'[request_definition]\n' +
'r = sub, obj, act\n' +
'\n' +
'[policy_definition]\n' +
'p = sub, obj, act\n' +
'\n' +
'[policy_effect]\n' +
'e = some(where (p.eft == allow))\n' +
'\n' +
'[matchers]\n' +
'm = r.sub == r.obj.Owner',
/////////////////////////////////////////////////////////////////////////
keymatch:
'[request_definition]\n' +
'r = sub, obj, act\n' +
'\n' +
'[policy_definition]\n' +
'p = sub, obj, act\n' +
'\n' +
'[policy_effect]\n' +
'e = some(where (p.eft == allow))\n' +
'\n' +
'[matchers]\n' +
'm = r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)',
/////////////////////////////////////////////////////////////////////////
keymatch2:
'[request_definition]\n' +
'r = sub, obj, act\n' +
'\n' +
'[policy_definition]\n' +
'p = sub, obj, act\n' +
'\n' +
'[policy_effect]\n' +
'e = some(where (p.eft == allow))\n' +
'\n' +
'[matchers]\n' +
'm = r.sub == p.sub && keyMatch2(r.obj, p.obj) && regexMatch(r.act, p.act)',
/////////////////////////////////////////////////////////////////////////
ipmatch:
'[request_definition]\n' +
'r = sub, obj, act\n' +
'\n' +
'[policy_definition]\n' +
'p = sub, obj, act\n' +
'\n' +
'[policy_effect]\n' +
'e = some(where (p.eft == allow))\n' +
'\n' +
'[matchers]\n' +
'm = ipMatch(r.sub, p.sub) && r.obj == p.obj && r.act == p.act',
/////////////////////////////////////////////////////////////////////////
priority:
'[request_definition]\n' +
'r = sub, obj, act\n' +
'\n' +
'[policy_definition]\n' +
'p = sub, obj, act, eft\n' +
'\n' +
'[role_definition]\n' +
'g = _, _\n' +
'\n' +
'[policy_effect]\n' +
'e = priority(p.eft) || deny\n' +
'\n' +
'[matchers]\n' +
'm = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act'
/////////////////////////////////////////////////////////////////////////
};
export default exampleModel;

View File

@ -0,0 +1,79 @@
/* eslint-disable */
const examplePolicy = {
/////////////////////////////////////////////////////////////////////////
basic: 'p, alice, data1, read\n' + 'p, bob, data2, write',
/////////////////////////////////////////////////////////////////////////
basic_with_root: 'p, alice, data1, read\n' + 'p, bob, data2, write',
/////////////////////////////////////////////////////////////////////////
basic_without_resources: 'p, alice, read\n' + 'p, bob, write',
/////////////////////////////////////////////////////////////////////////
basic_without_users: 'p, data1, read\n' + 'p, data2, write',
/////////////////////////////////////////////////////////////////////////
rbac:
'p, alice, data1, read\n' +
'p, bob, data2, write\n' +
'p, data2_admin, data2, read\n' +
'p, data2_admin, data2, write\n' +
'\n' +
'g, alice, data2_admin',
/////////////////////////////////////////////////////////////////////////
rbac_with_resource_roles:
'p, alice, data1, read\n' +
'p, bob, data2, write\n' +
'p, data_group_admin, data_group, write\n' +
'\n' +
'g, alice, data_group_admin\n' +
'g2, data1, data_group\n' +
'g2, data2, data_group',
/////////////////////////////////////////////////////////////////////////
rbac_with_domains:
'p, admin, domain1, data1, read\n' +
'p, admin, domain1, data1, write\n' +
'p, admin, domain2, data2, read\n' +
'p, admin, domain2, data2, write\n' +
'\n' +
'g, alice, admin, domain1\n' +
'g, bob, admin, domain2',
/////////////////////////////////////////////////////////////////////////
rbac_with_deny:
'p, alice, data1, read, allow\n' +
'p, bob, data2, write, allow\n' +
'p, data2_admin, data2, read, allow\n' +
'p, data2_admin, data2, write, allow\n' +
'p, alice, data2, write, deny\n' +
'\n' +
'g, alice, data2_admin',
/////////////////////////////////////////////////////////////////////////
abac: '',
/////////////////////////////////////////////////////////////////////////
keymatch:
'p, alice, /alice_data/*, GET\n' +
'p, alice, /alice_data/resource1, POST\n' +
'\n' +
'p, bob, /alice_data/resource2, GET\n' +
'p, bob, /bob_data/*, POST\n' +
'\n' +
'p, cathy, /cathy_data, (GET)|(POST)',
/////////////////////////////////////////////////////////////////////////
keymatch2: 'p, alice, /alice_data/:resource, GET\n' + 'p, alice, /alice_data2/:id/using/:resId, GET',
/////////////////////////////////////////////////////////////////////////
ipmatch: 'p, 192.168.2.0/24, data1, read\n' + 'p, 10.0.0.0/16, data2, write',
/////////////////////////////////////////////////////////////////////////
priority:
'p, alice, data1, read, allow\n' +
'p, data1_deny_group, data1, read, deny\n' +
'p, data1_deny_group, data1, write, deny\n' +
'p, alice, data1, write, allow\n' +
'\n' +
'g, alice, data1_deny_group\n' +
'\n' +
'p, data2_allow_group, data2, read, allow\n' +
'p, bob, data2, read, deny\n' +
'p, bob, data2, write, deny\n' +
'\n' +
'g, bob, data2_allow_group'
/////////////////////////////////////////////////////////////////////////
};
export default examplePolicy;

View File

@ -0,0 +1,18 @@
/* eslint-disable */
const exampleRequest = {
basic: 'alice, data1, read',
basic_with_root: 'alice, data1, read',
basic_without_resources: 'alice, read',
basic_without_users: 'data1, read',
rbac: 'alice, data2, read',
rbac_with_resource_roles: 'alice, data1, read\n' + 'alice, data1, write\n' + 'alice, data2, read\n' + 'alice, data2, write ',
rbac_with_domains: 'alice, domain1, data1, read',
rbac_with_deny: 'alice, data1, read\n' + 'alice, data2, write',
abac: 'Not support',
keymatch: 'alice, /alice_data/hello, GET',
keymatch2: 'alice, /alice_data/hello, GET\n' + 'alice, /alice_data/hello, POST',
ipmatch: 'Not support',
priority: 'alice, data1, read'
};
export default exampleRequest;

154
src/editor/editor.tsx Normal file
View File

@ -0,0 +1,154 @@
import React, { CSSProperties, useEffect, useState } from 'react';
import { Controlled as CodeMirror } from 'react-codemirror2';
import { get, Persist, set } from './persist';
import * as codemirror from 'codemirror';
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/monokai.css';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/addon/selection/active-line';
import 'codemirror/addon/edit/matchbrackets';
import 'codemirror/addon/display/placeholder';
import './casbin-mode/casbin-conf';
import './casbin-mode/casbin-csv';
interface CasbinCodeMirror {
model: string;
options: codemirror.EditorConfiguration;
style?: CSSProperties;
onChange: (text: string) => void;
persist: Persist;
}
interface EditorProps {
model: string;
onChange?: (text: string) => void;
style?: CSSProperties;
}
const CasbinCodeMirror = (props: CasbinCodeMirror) => {
const [value, setValue] = useState(get(props.persist, props.model));
const { model, onChange, persist } = props;
useEffect(() => {
const modelText = get(persist, model);
setValue(modelText);
onChange(modelText);
}, [model, persist, onChange]);
return (
<div style={props.style}>
<CodeMirror
onBeforeChange={(editor, data, value) => {
setValue(value);
props.onChange(value);
set(props.persist, value);
}}
options={props.options}
value={value}
/>
</div>
);
};
CasbinCodeMirror.defaultProps = {
onChange: () => {}
};
export const CustomFunctionEditor = (props: EditorProps) => {
return (
<CasbinCodeMirror
persist={Persist.CUSTOM_FUNCTION}
options={{
lineNumbers: true,
indentUnit: 4,
styleActiveLine: true,
matchBrackets: true,
mode: 'javascript',
lineWrapping: true,
theme: 'monokai'
}}
{...props}
/>
);
};
export const ModelEditor = (props: EditorProps) => {
return (
<CasbinCodeMirror
persist={Persist.MODEL}
options={{
lineNumbers: true,
indentUnit: 4,
styleActiveLine: true,
matchBrackets: true,
mode: 'casbin-conf',
lineWrapping: true,
theme: 'monokai'
}}
{...props}
/>
);
};
export const PolicyEditor = (props: EditorProps) => {
return (
<CasbinCodeMirror
persist={Persist.POLICY}
options={{
lineNumbers: true,
indentUnit: 4,
styleActiveLine: true,
matchBrackets: true,
mode: 'casbin-csv',
lineWrapping: true,
theme: 'monokai'
}}
{...props}
/>
);
};
export const RequestEditor = (props: EditorProps) => {
return (
<CasbinCodeMirror
persist={Persist.REQUEST}
options={{
lineNumbers: true,
indentUnit: 4,
styleActiveLine: true,
matchBrackets: true,
mode: 'casbin-csv',
lineWrapping: true,
theme: 'monokai'
}}
{...props}
/>
);
};
interface RequestResultEditorProps {
value: string;
style?: CSSProperties;
}
export const RequestResultEditor = (props: RequestResultEditorProps) => {
return (
<div style={props.style}>
<CodeMirror
onBeforeChange={() => {}}
value={props.value}
options={{
readOnly: true,
indentUnit: 4,
styleActiveLine: true,
matchBrackets: true,
mode: 'javascript',
lineWrapping: true,
theme: 'monokai'
}}
/>
</div>
);
};

94
src/editor/index.tsx Executable file
View File

@ -0,0 +1,94 @@
import React, { isValidElement, useState } from 'react';
import SelectModel from './select-model';
import { Button, EditorContainer, FlexRow, HeaderTitle } from '../ui';
import { getSelectedModel, reset } from './persist';
import { CustomFunctionEditor, ModelEditor, PolicyEditor, RequestEditor, RequestResultEditor } from './editor';
import { RouteComponentProps } from '@reach/router';
import Syntax from './syntax';
import RunTest from './run-test';
interface Props extends RouteComponentProps {}
export const EditorScreen = (props: Props) => {
const [model, setModel] = useState(getSelectedModel());
const [modelText, setModelText] = useState('');
const [policy, setPolicy] = useState('');
const [fn, setFn] = useState('');
const [request, setRequest] = useState('');
const [echo, setEcho] = useState<JSX.Element>(<></>);
const [requestResult, setRequestResult] = useState('');
const [visible, setVisible] = useState(false);
return (
<>
<FlexRow>
<EditorContainer>
<FlexRow>
<HeaderTitle>Model</HeaderTitle>
<SelectModel
onChange={value => {
setModel(value);
}}
/>
<Button
onClick={() => {
const ok = window.confirm('Confirm Reset?');
if (ok) {
reset(model);
window.location.reload();
}
}}
style={{ marginLeft: 8 }}
>
Reset
</Button>
</FlexRow>
<ModelEditor model={model} onChange={setModelText} />
</EditorContainer>
<EditorContainer>
<HeaderTitle>Policy</HeaderTitle>
<PolicyEditor model={model} onChange={setPolicy} />
</EditorContainer>
</FlexRow>
<FlexRow>
<EditorContainer>
<HeaderTitle>Request</HeaderTitle>
<RequestEditor model={model} onChange={setRequest} />
</EditorContainer>
<EditorContainer>
<HeaderTitle>Enforcement Result</HeaderTitle>
<RequestResultEditor value={requestResult} />
</EditorContainer>
</FlexRow>
<FlexRow>
<EditorContainer>
<FlexRow>
<HeaderTitle>Custom Function</HeaderTitle>
<Button onClick={() => setVisible(!visible)}>TOGGLE</Button>
</FlexRow>
{visible && <CustomFunctionEditor model={model} onChange={setFn} />}
</EditorContainer>
</FlexRow>
<div style={{ padding: 8 }}>
<Syntax model={modelText} onResponse={component => setEcho(component)} />
<RunTest
model={modelText}
policy={policy}
fn={fn}
request={request}
onResponse={v => {
if (isValidElement(v)) {
setEcho(v);
} else if (Array.isArray(v)) {
setRequestResult(v.join('\n'));
}
}}
/>
<div style={{ display: 'inline-block' }}>{echo}</div>
</div>
</>
);
};

64
src/editor/persist.ts Executable file
View File

@ -0,0 +1,64 @@
import exampleModel from './casbin-mode/example-model';
import examplePolicy from './casbin-mode/example-policy';
import exampleRequest from './casbin-mode/example-request';
export const DEFAULT_MODEL = 'basic';
export enum Persist {
MODEL,
POLICY,
REQUEST,
CUSTOM_FUNCTION
}
function getKey(persist: Persist, modelName: string) {
return `${modelName.toUpperCase()}_${Persist[persist]}`;
}
export function getSelectedModel() {
const v = window.localStorage.getItem(Persist.MODEL.toString());
return v ? v : DEFAULT_MODEL;
}
export function setSelectedModel(value: string) {
window.localStorage.setItem(Persist[Persist.MODEL], value);
}
export function get(persist: Persist, modelName = DEFAULT_MODEL) {
const data = window.localStorage.getItem(getKey(persist, modelName));
if (data) {
return data;
}
switch (persist) {
case Persist.MODEL:
// @ts-ignore
return exampleModel[modelName];
case Persist.POLICY:
// @ts-ignore
return examplePolicy[modelName];
case Persist.REQUEST:
// @ts-ignore
return exampleRequest[modelName];
case Persist.CUSTOM_FUNCTION:
return `var fns = {}`;
}
}
export function set(persist: Persist, text: string) {
const modelName = getSelectedModel() || DEFAULT_MODEL;
window.localStorage.setItem(getKey(persist, modelName), text);
}
export function reset(modelName: string) {
for (const m in Persist) {
if (!Persist.hasOwnProperty(m)) {
continue;
}
const index = parseInt(m, 10);
if (!isNaN(index)) {
continue;
}
window.localStorage.removeItem(getKey(index, modelName));
}
}

66
src/editor/run-test.tsx Executable file
View File

@ -0,0 +1,66 @@
import React from 'react';
import { Button, Echo } from '../ui';
import { newEnforcer, newModel, StringAdapter } from 'casbin';
interface RunTestProps {
model: string;
policy: string;
fn: string;
request: string;
onResponse: (com: JSX.Element | boolean[]) => void;
}
const RunTest = (props: RunTestProps) => {
return (
<Button
style={{ marginRight: 8 }}
onClick={async () => {
const startTime = performance.now();
const result = [];
try {
const e = await newEnforcer(newModel(props.model), new StringAdapter(props.policy));
const fnString = props.fn;
if (fnString) {
try {
const fns: any = {};
// eslint-disable-next-line
eval(`${fnString}`);
if (fns) {
Object.keys(fns).forEach(key => e.addFunction(key, fns[key]));
}
} catch (e) {
props.onResponse(<Echo>Please check syntax in Custom Function Editor.</Echo>);
return;
}
}
for (const n of props.request.split('\n')) {
const p = n
.split(',')
.map(n => n.trim())
.filter(n => n);
if (!p || p.length === 0) {
return;
}
result.push(await e.enforce(...p));
}
const stopTime = performance.now();
props.onResponse(<Echo>{'Done in ' + ((stopTime - startTime) / 1000.0).toFixed(2) + 's'}</Echo>);
props.onResponse(result);
} catch (e) {
props.onResponse(<Echo type={'error'}>{e.message}</Echo>);
props.onResponse([]);
}
}}
>
RUN THE TEST
</Button>
);
};
export default RunTest;

42
src/editor/select-model.tsx Executable file
View File

@ -0,0 +1,42 @@
import React from 'react';
import { getSelectedModel, setSelectedModel } from './persist';
interface SelectModelProps {
onChange: (value: string) => void;
}
const SelectModel = (props: SelectModelProps) => {
return (
<select
defaultValue={getSelectedModel()}
onChange={e => {
const model = e.target.value;
setSelectedModel(model);
props.onChange(model);
}}
>
<option value="" disabled>
Select your model
</option>
<option value="basic">ACL</option>
<option value="basic_with_root">ACL with superuser</option>
<option value="basic_without_resources">ACL without resources</option>
<option value="basic_without_users">ACL without users</option>
<option value="rbac">RBAC</option>
<option value="rbac_with_resource_roles">RBAC with resource roles</option>
<option value="rbac_with_domains">RBAC with domains/tenants</option>
<option value="rbac_with_deny">RBAC with deny-override</option>
<option value="abac">ABAC</option>
<option value="keymatch">RESTful (KeyMatch)</option>
<option value="keymatch2">RESTful (KeyMatch2)</option>
<option value="ipmatch">IP match</option>
<option value="priority">Priority</option>
</select>
);
};
SelectModel.defaultProps = {
onChange: console.log,
defaultValue: 'basic'
};
export default SelectModel;

28
src/editor/syntax.tsx Executable file
View File

@ -0,0 +1,28 @@
import React from 'react';
import { Button, Echo } from '../ui';
import { Config } from 'casbin/lib/config';
interface SyntaxProps {
model: string;
onResponse: (com: JSX.Element) => void;
}
const Syntax = (props: SyntaxProps) => {
return (
<Button
style={{ marginRight: 8 }}
onClick={() => {
try {
Config.newConfigFromText(props.model);
props.onResponse(<Echo>passed</Echo>);
} catch (e) {
props.onResponse(<Echo type={'error'}>{e.message}</Echo>);
}
}}
>
SYNTAX VALIDATE
</Button>
);
};
export default Syntax;

30
src/index.tsx Executable file
View File

@ -0,0 +1,30 @@
import React from 'react';
import { Router } from '@reach/router';
import ReactDOM from 'react-dom';
import { EditorScreen } from './editor';
import { Footer } from './ui';
import 'normalize.css/normalize.css';
const App = () => (
<>
<Router>
<EditorScreen path="/" />
</Router>
<Footer>
<a
style={{ color: '#FFFFFF', textDecoration: 'none' }}
title="casbin-editor on GitHub"
rel="noopener noreferrer"
target="_blank"
href="https://github.com/nodece/casbin-editor"
>
Github
</a>
<span style={{ color: '#FFFFFF', float: 'right' }}>Copyright © {new Date().getFullYear()} Casbin contributors.</span>
</Footer>
</>
);
const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

1
src/react-app-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

63
src/ui.tsx Executable file
View File

@ -0,0 +1,63 @@
import styled from 'styled-components';
export const Button = styled.button`
border: 1px solid #443d80;
border-radius: 3px;
color: #443d80;
display: inline-block;
font-size: 14px;
font-weight: 400;
line-height: 1.2em;
padding: 10px;
text-decoration: none !important;
text-transform: uppercase;
transition: background 0.3s, color 0.3s;
:hover {
background: #443d80;
color: #fff;
}
`;
export const HeaderTitle = styled.h4`
padding: 8px;
`;
export const FlexRow = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`;
export const EditorContainer = styled.div`
flex: 1;
`;
interface EchoProps {
type?: 'pass' | 'error';
}
const error = '#db4545';
const pass = '#39aa56';
export const Echo = styled.span<EchoProps>`
color: ${(props: EchoProps) => {
switch (props.type) {
case 'error':
return error;
case 'pass':
return pass;
}
}};
font-weight: 600;
font-size: 14px;
`;
Echo.defaultProps = {
type: 'pass'
};
export const Footer = styled.div`
padding: 1em;
background: #222;
`;

21
tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "esnext",
"target": "es5",
"jsx": "react",
"moduleResolution": "node",
"experimentalDecorators": true,
"esModuleInterop": true,
"strict": true,
"resolveJsonModule": true,
"strictNullChecks": true,
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["src"]
}

10862
yarn.lock Normal file

File diff suppressed because it is too large Load Diff