Skip to content

Commit

Permalink
feat: add built-in FileSystem (#430)
Browse files Browse the repository at this point in the history
Signed-off-by: Zixuan Liu <nodeces@gmail.com>
  • Loading branch information
nodece committed Feb 17, 2023
1 parent c5af76a commit 999c34c
Show file tree
Hide file tree
Showing 12 changed files with 196 additions and 38 deletions.
28 changes: 22 additions & 6 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { readFileSync } from 'fs';

import { FileSystem, mustGetDefaultFileSystem } from './persist';

// ConfigInterface defines the behavior of a Config implementation
export interface ConfigInterface {
Expand All @@ -36,19 +37,34 @@ export class Config implements ConfigInterface {

private data: Map<string, Map<string, string>>;

private constructor() {
private readonly fs?: FileSystem;

private constructor(fs?: FileSystem) {
this.data = new Map<string, Map<string, string>>();
if (fs) {
this.fs = fs;
}
}

/**
* newConfig create an empty configuration representation from file.
*
* @param confName the path of the model file.
* @return the constructor of Config.
* @deprecated use {@link newConfigFromFile} instead.
*/
public static newConfig(confName: string): Config {
const config = new Config();
config.parse(confName);
return this.newConfigFromFile(confName);
}

/**
* newConfigFromFile create an empty configuration representation from file.
* @param path the path of the model file.
* @param fs {@link FileSystem}
*/
public static newConfigFromFile(path: string, fs?: FileSystem): Config {
const config = new Config(fs);
config.parse(path);
return config;
}

Expand Down Expand Up @@ -86,8 +102,8 @@ export class Config implements ConfigInterface {
}

private parse(path: string): void {
const buf = readFileSync(path);
this.parseBuffer(buf);
const body = (this.fs ? this.fs : mustGetDefaultFileSystem()).readFileSync(path);
this.parseBuffer(Buffer.isBuffer(body) ? body : Buffer.from(body));
}

private parseBuffer(buf: Buffer): void {
Expand Down
22 changes: 19 additions & 3 deletions src/coreEnforcer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import { compile, compileAsync, addBinaryOp } from 'expression-eval';

import { DefaultEffector, Effect, Effector } from './effect';
import { FunctionMap, Model, newModel, PolicyOp } from './model';
import { FunctionMap, Model, newModelFromFile, PolicyOp } from './model';
import { Adapter, FilteredAdapter, Watcher, BatchAdapter, UpdatableAdapter, WatcherEx } from './persist';
import { DefaultRoleManager, RoleManager } from './rbac';
import {
Expand All @@ -32,6 +32,7 @@ import {
} from './util';
import { getLogger, logPrint } from './log';
import { MatchingFunc } from './rbac';
import { FileSystem, getDefaultFileSystem } from './persist';

type Matcher = ((context: any) => Promise<any>) | ((context: any) => any);

Expand All @@ -56,6 +57,22 @@ export class CoreEnforcer {
protected autoSave = true;
protected autoBuildRoleLinks = true;
protected autoNotifyWatcher = true;
protected fs?: FileSystem;

/**
* setFileSystem sets a file system to read the model file or the policy file.
* @param fs {@link FileSystem}
*/
public setFileSystem(fs: FileSystem): void {
this.fs = fs;
}

/**
* getFileSystem gets the file system,
*/
public getFileSystem(): FileSystem | undefined {
return this.fs;
}

private getExpression(asyncCompile: boolean, exp: string): Matcher {
const matcherKey = `${asyncCompile ? 'ASYNC[' : 'SYNC['}${exp}]`;
Expand All @@ -77,8 +94,7 @@ export class CoreEnforcer {
* so the policy is invalidated and needs to be reloaded by calling LoadPolicy().
*/
public loadModel(): void {
this.model = newModel();
this.model.loadModel(this.modelPath);
this.model = newModelFromFile(this.modelPath, this.fs);
this.model.printModel();
}

Expand Down
6 changes: 3 additions & 3 deletions src/enforcer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// limitations under the License.

import { ManagementEnforcer } from './managementEnforcer';
import { Model, newModel } from './model';
import { Model, newModelFromFile } from './model';
import { Adapter, FileAdapter, StringAdapter } from './persist';
import { getLogger } from './log';
import { arrayRemoveDuplicates } from './util';
Expand All @@ -30,7 +30,7 @@ export class Enforcer extends ManagementEnforcer {
* @param lazyLoad lazyLoad whether to load policy at initial time
*/
public async initWithFile(modelPath: string, policyPath: string, lazyLoad = false): Promise<void> {
const a = new FileAdapter(policyPath);
const a = new FileAdapter(policyPath, this.fs);
await this.initWithAdapter(modelPath, a, lazyLoad);
}

Expand All @@ -52,7 +52,7 @@ export class Enforcer extends ManagementEnforcer {
* @param lazyLoad whether to load policy at initial time
*/
public async initWithAdapter(modelPath: string, adapter: Adapter, lazyLoad = false): Promise<void> {
const m = newModel(modelPath, '');
const m = newModelFromFile(modelPath, this.fs);
await this.initWithModelAndAdapter(m, adapter, lazyLoad);

this.modelPath = modelPath;
Expand Down
15 changes: 15 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2023 The Casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

declare const __non_webpack_require__: NodeRequireFunction;
15 changes: 15 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

import * as Util from './util';
import { setDefaultFileSystem } from './persist';

export * from './config';
export * from './enforcer';
Expand All @@ -25,3 +26,17 @@ export * from './rbac';
export * from './log';
export * from './frontend';
export { Util };

if (typeof process !== 'undefined' && process?.versions?.node) {
const requireFunc = typeof __non_webpack_require__ === 'function' ? __non_webpack_require__ : require;
const fs = requireFunc('fs');
const defaultFileSystem = {
readFileSync(path: string, encoding?: string) {
return fs.readFileSync(path, { encoding });
},
writeFileSync(path: string, text: string, encoding?: string) {
return fs.writeFileSync(path, text, encoding);
},
};
setDefaultFileSystem(defaultFileSystem);
}
29 changes: 23 additions & 6 deletions src/model/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Assertion } from './assertion';
import { getLogger, logPrint } from '../log';
import { DefaultRoleManager } from '../rbac';
import { EffectExpress, FieldIndex } from '../constants';
import { FileSystem } from '../persist/fileSystem';

const defaultDomain = '';
const defaultSeparator = '::';
Expand Down Expand Up @@ -125,9 +126,23 @@ export class Model {
return true;
}

// loadModel loads the model from model CONF file.
public loadModel(path: string): void {
const cfg = Config.newConfig(path);
/**
* loadModel loads the model from model CONF file.
* @param path the model file path
* @param fs {@link FileSystem}
* @deprecated {@link loadModelFromFile}
*/
public loadModel(path: string, fs?: FileSystem): void {
this.loadModelFromFile(path, fs);
}

/**
* loadModelFromFile loads the model from model CONF file.
* @param path the model file path
* @param fs {@link FileSystem}
*/
public loadModelFromFile(path: string, fs?: FileSystem): void {
const cfg = Config.newConfigFromFile(path, fs);

this.loadModelFromConfig(cfg);
}
Expand Down Expand Up @@ -576,7 +591,7 @@ export function newModel(...text: string[]): Model {

if (text.length === 2) {
if (text[0] !== '') {
m.loadModel(text[0]);
m.loadModelFromFile(text[0]);
}
} else if (text.length === 1) {
m.loadModelFromText(text[0]);
Expand All @@ -590,9 +605,11 @@ export function newModel(...text: string[]): Model {
/**
* newModelFromFile creates a model from a .CONF file.
*/
export function newModelFromFile(path: string): Model {
export function newModelFromFile(path: string, fs?: FileSystem): Model {
const m = new Model();
m.loadModel(path);
if (path) {
m.loadModelFromFile(path, fs);
}
return m;
}

Expand Down
15 changes: 10 additions & 5 deletions src/persist/fileAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { Adapter } from './adapter';
import { Model } from '../model';
import { Helper } from './helper';
import { arrayToString, readFile, writeFile } from '../util';
import { arrayToString } from '../util';
import { FileSystem, mustGetDefaultFileSystem } from './fileSystem';

/**
* FileAdapter is the file adapter for Casbin.
* It can load policy from file or save policy to file.
*/
export class FileAdapter implements Adapter {
public readonly filePath: string;
protected readonly fs?: FileSystem;

/**
* FileAdapter is the constructor for FileAdapter.
* @param {string} filePath filePath the path of the policy file.
*
* @param filePath filePath the path of the policy file.
* @param fs {@link FileSystem}
*/
constructor(filePath: string) {
constructor(filePath: string, fs?: FileSystem) {
this.filePath = filePath;
this.fs = fs;
}

public async loadPolicy(model: Model): Promise<void> {
Expand All @@ -27,7 +32,7 @@ export class FileAdapter implements Adapter {
}

private async loadPolicyFile(model: Model, handler: (line: string, model: Model) => void): Promise<void> {
const bodyBuf = await readFile(this.filePath);
const bodyBuf = await (this.fs ? this.fs : mustGetDefaultFileSystem()).readFileSync(this.filePath);
const lines = bodyBuf.toString().split('\n');
lines.forEach((n: string, index: number) => {
if (!n) {
Expand Down Expand Up @@ -76,7 +81,7 @@ export class FileAdapter implements Adapter {
}

private async savePolicyFile(text: string): Promise<void> {
await writeFile(this.filePath, text);
(this.fs ? this.fs : mustGetDefaultFileSystem()).writeFileSync(this.filePath, text);
}

/**
Expand Down
18 changes: 18 additions & 0 deletions src/persist/fileSystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Buffer } from 'buffer';
export interface FileSystem {
readFileSync(path: string, encoding?: string): Buffer | string;
writeFileSync(path: string, text: string, encoding?: string): void;
}

let defaultFileSystem: FileSystem | undefined = undefined;
const ErrorNoFileSystem = new Error('please set the default FileSystem by call the setDefaultFileSystem');
export const setDefaultFileSystem = (fs?: FileSystem): void => {
defaultFileSystem = fs;
};
export const getDefaultFileSystem = (): FileSystem | undefined => defaultFileSystem;
export const mustGetDefaultFileSystem = (): FileSystem => {
if (defaultFileSystem) {
return defaultFileSystem;
}
throw ErrorNoFileSystem;
};
1 change: 1 addition & 0 deletions src/persist/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './defaultFilteredAdapter';
export * from './batchAdapter';
export * from './batchFileAdapter';
export * from './updatableAdapter';
export * from './fileSystem';
30 changes: 16 additions & 14 deletions src/util/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import * as fs from 'fs';

// escapeAssertion escapes the dots in the assertion,
// because the expression evaluation doesn't support such variable names.
import { mustGetDefaultFileSystem } from '../persist';

function escapeAssertion(s: string): string {
s = s.replace(/r\./g, 'r_');
s = s.replace(/p\./g, 'p_');
Expand Down Expand Up @@ -82,25 +82,27 @@ function setEquals(a: string[], b: string[]): boolean {

// readFile return a promise for readFile.
function readFile(path: string, encoding?: string): any {
const fs = mustGetDefaultFileSystem();
return new Promise((resolve, reject) => {
fs.readFile(path, encoding || 'utf8', (error, data) => {
if (error) {
reject(error);
}
resolve(data);
});
try {
fs.readFileSync(path, encoding || 'utf8');
resolve();
} catch (e) {
reject(e);
}
});
}

// writeFile return a promise for writeFile.
function writeFile(path: string, file: string, encoding?: string): any {
const fs = mustGetDefaultFileSystem();
return new Promise((resolve, reject) => {
fs.writeFile(path, file, encoding || 'utf8', (error) => {
if (error) {
reject(error);
}
resolve(true);
});
try {
fs.writeFileSync(path, file, encoding || 'utf-8');
resolve();
} catch (e) {
reject(e);
}
});
}

Expand Down
21 changes: 20 additions & 1 deletion test/enforcer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { readFileSync } from 'fs';
import fs, { readFileSync } from 'fs';

import { newModel, newEnforcer, Enforcer, FileAdapter, StringAdapter, Util } from '../src';

Expand Down Expand Up @@ -713,3 +713,22 @@ test('TestSubjectPriorityWithDomain', async () => {
testEnforceEx(e, 'alice', 'data1', 'write', [true, ['alice', 'data1', 'domain1', 'write', 'allow']], 'domain1');
testEnforceEx(e, 'bob', 'data2', 'write', [true, ['bob', 'data2', 'domain2', 'write', 'allow']], 'domain2');
});

test('TestEnforcerWithScopeFileSystem', async () => {
const e = await newEnforcer();
const defaultFileSystem = {
readFileSync(path: string, encoding?: string) {
return fs.readFileSync(path, { encoding });
},
writeFileSync(path: string, text: string, encoding?: string) {
return fs.writeFileSync(path, text, encoding);
},
};
e.setFileSystem(defaultFileSystem);
expect(e.getFileSystem()).toEqual(defaultFileSystem);
await e.initWithFile('examples/basic_model.conf', 'examples/basic_policy.csv', false);
await testEnforce(e, 'alice', 'data1', 'read', true);
await testEnforce(e, 'alice', 'data1', 'write', false);
await testEnforce(e, 'bob', 'data2', 'write', true);
await testEnforce(e, 'bob', 'data2', 'read', false);
});

0 comments on commit 999c34c

Please sign in to comment.