Skip to content

Commit

Permalink
feat: Implement a unified Casbin engine interface and refactor the ex…
Browse files Browse the repository at this point in the history
…ecution logic. (#180)

* feat: implement CasbinEngine and refactor enforcement logic

* refactor: simplify ModelKind type usage and remove unnecessary type declaration
  • Loading branch information
HashCookie authored Dec 20, 2024
1 parent 7e9d771 commit 3f74ea3
Show file tree
Hide file tree
Showing 9 changed files with 365 additions and 217 deletions.
8 changes: 4 additions & 4 deletions app/components/ModelEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import { buttonPlugin } from '@/app/components/editor/ButtonPlugin';
import { extractPageContent } from '@/app/utils/contentExtractor';
import { useLang } from '@/app/context/LangContext';
import SidePanelChat from '@/app/components/SidePanelChat';
import { example, ModelKind } from '@/app/components/editor/casbin-mode/example';
import { example } from '@/app/components/editor/casbin-mode/example';
import { clsx } from 'clsx';

export const ModelEditor = () => {
const [modelText, setModelText] = useState('');
const [modelKind, setModelKind] = useState<ModelKind>('');
const [modelKind, setModelKind] = useState('');
const [initialText, setInitialText] = useState('');
const editorRef = useRef<EditorView | null>(null);
const cursorPosRef = useRef<{ from: number; to: number } | null>(null);
Expand Down Expand Up @@ -130,7 +130,7 @@ export const ModelEditor = () => {
<select
value={modelKind}
onChange={(e) => {
const selectedKind = e.target.value as ModelKind;
const selectedKind = e.target.value;
if (selectedKind && example[selectedKind]) {
setModelText(example[selectedKind].model);
setModelKind('');
Expand All @@ -151,7 +151,7 @@ export const ModelEditor = () => {
{Object.keys(example).map((n) => {
return (
<option key={n} value={n}>
{example[n as ModelKind].name}
{example[n].name}
</option>
);
})}
Expand Down
106 changes: 106 additions & 0 deletions app/components/editor/CasbinEngine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { newEnforcer, newModel, StringAdapter } from 'casbin';
import { remoteEnforcer } from './hooks/useRemoteEnforcer';
import { setupRoleManager, setupCustomConfig, processRequests } from '@/app/utils/casbinEnforcer';

interface EnforceResult {
allowed: boolean;
reason: string[];
error: string | null;
}

export interface ICasbinEngine {
enforce(params: {
model: string;
policy: string;
request: string;
customConfig?: string;
enforceContextData?: Map<string, string>;
}): Promise<EnforceResult>;

getVersion(): string;
getType(): 'node' | 'java' | 'go';
}

// Node.js
export class NodeCasbinEngine implements ICasbinEngine {
async enforce(params) {
try {
const e = await newEnforcer(
newModel(params.model),
params.policy ? new StringAdapter(params.policy) : undefined
);

setupRoleManager(e);

if (params.customConfig) {
await setupCustomConfig(e, params.customConfig);
}

const results = await processRequests(params.request, e, params.enforceContextData);

return {
allowed: results[0].okEx,
reason: results[0].reason,
error: null,
};
} catch (error) {
throw error;
}
}

getVersion(): string {
return process.env.CASBIN_VERSION || '';
}

getType(): 'node' {
return 'node';
}
}

// RemoteCasbinEngine
export class RemoteCasbinEngine implements ICasbinEngine {
constructor(private engine: 'java' | 'go') {}

async enforce(params) {
try {
const result = await remoteEnforcer({
model: params.model,
policy: params.policy,
request: params.request,
engine: this.engine,
});

if (result.error) {
throw new Error(result.error);
}

return {
allowed: result.allowed,
reason: result.reason,
error: null,
};
} catch (error) {
throw error;
}
}

getVersion(): string {
return '';
}

getType(): 'java' | 'go' {
return this.engine;
}
}

export function createCasbinEngine(type: 'node' | 'java' | 'go'): ICasbinEngine {
switch (type) {
case 'node':
return new NodeCasbinEngine();
case 'java':
case 'go':
return new RemoteCasbinEngine(type);
default:
throw new Error(`Unsupported engine type: ${type}`);
}
}
2 changes: 0 additions & 2 deletions app/components/editor/casbin-mode/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,5 +451,3 @@ export const defaultEnforceContext = `{
"e": "e",
"m": "m"
}`;

export type ModelKind = string;
6 changes: 3 additions & 3 deletions app/components/editor/hooks/useIndex.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, { isValidElement, ReactNode, useEffect, useRef, useState } from 'react';
import { defaultCustomConfig, defaultEnforceContext, example, ModelKind } from '@/app/components/editor/casbin-mode/example';
import { defaultCustomConfig, defaultEnforceContext, example } from '@/app/components/editor/casbin-mode/example';
import { ShareFormat } from '@/app/components/editor/hooks/useShareInfo';
import { defaultEnforceContextData } from '@/app/components/editor/hooks/useSetupEnforceContext';

export default function useIndex() {
const [modelKind, setModelKind] = useState<ModelKind>('basic');
const [modelKind, setModelKind] = useState('basic');
const [modelText, setModelText] = useState('');
const [policy, setPolicy] = useState('');
const [request, setRequest] = useState('');
Expand Down Expand Up @@ -53,7 +53,7 @@ export default function useIndex() {
.then((content) => {
const parsed = JSON.parse(content) as ShareFormat;
loadState.current.content = parsed;
const newModelKind = parsed?.modelKind && parsed.modelKind in example ? (parsed.modelKind as ModelKind) : 'basic';
const newModelKind = parsed?.modelKind && parsed.modelKind in example ? parsed.modelKind : 'basic';
setModelKind(newModelKind);
setTriggerUpdate((prev) => {
return prev + 1;
Expand Down
52 changes: 35 additions & 17 deletions app/components/editor/hooks/useRemoteEnforcer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,28 @@ interface RemoteEnforcerProps {
}

export async function remoteEnforcer(props: RemoteEnforcerProps) {
const { model, policy, request, engine } = props;

try {
const baseUrl = 'https://door.casdoor.com/api/run-casbin-command';

const args = [
"enforce",
"-m",
model,
"-p",
policy,
...request.split(',').map((item) => {return item.trim()})
'enforce',
'-m',
props.model,
'-p',
props.policy,
...props.request.split(',').map((item) => {
return item.trim();
}),
];

const url = new URL(baseUrl);
url.searchParams.set('language', engine);
url.searchParams.set('language', props.engine);
url.searchParams.set('args', JSON.stringify(args));

const response = await fetch(url.toString(), {
method: 'GET',
headers: {
'Accept': 'application/json',
}
Accept: 'application/json',
},
});

if (!response.ok) {
Expand All @@ -51,22 +50,41 @@ export async function remoteEnforcer(props: RemoteEnforcerProps) {

const result = await response.json();

if (result.status !== "ok") {
if (result.status !== 'ok') {
throw new Error(result.msg || 'API request failed');
}

const enforceResult = JSON.parse(result.data.trim());
let enforceResult;
const data = result.data.trim();

try {
enforceResult = JSON.parse(data);
} catch {
if (data.toLowerCase() === 'allowed') {
enforceResult = {
allow: true,
explain: 'Allowed by policy',
};
} else if (data.toLowerCase() === 'denied') {
enforceResult = {
allow: false,
explain: 'Denied by policy',
};
} else {
throw new Error(`Unexpected response format: ${data}`);
}
}

return {
allowed: enforceResult.allow,
reason: enforceResult.explain ? [enforceResult.explain] : [],
error: null
error: null,
};
} catch (error) {
return {
allowed: false,
reason: ["Error occurred during enforcement"],
error: error instanceof Error ? error.message : 'Unknown error occurred'
reason: ['Error occurred during enforcement'],
error: error instanceof Error ? error.message : 'Unknown error occurred',
};
}
}
Loading

0 comments on commit 3f74ea3

Please sign in to comment.