Skip to content

Commit

Permalink
fix: Remove deprecated APIs and fix adapter when filtering policy
Browse files Browse the repository at this point in the history
  • Loading branch information
jkalberer committed Aug 1, 2022
1 parent 91339c2 commit 65bd719
Show file tree
Hide file tree
Showing 8 changed files with 2,146 additions and 2,603 deletions.
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,65 @@ async function myFunction() {
});


const e = await newEnforcer('examples/rbac_model.conf', a);

// Load the filtered policy from DB.
await e.loadFilteredPolicy({
'ptype': 'p',
'v0': 'alice'
});

// Check the permission.
await e.enforce('alice', 'data1', 'read');

// Modify the policy.
// await e.addPolicy(...);
// await e.removePolicy(...);

// Save the policy back to DB.
await e.savePolicy();
}
```

## Custom Entity Example
Use a custom entity that matches the CasbinRule or MongoCasbinRule in order to add additional fields or metadata to the entity.

```typescript
import { newEnforcer } from 'casbin';
import {
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import TypeORMAdapter from 'typeorm-adapter';

@Entity('custom_rule')
class CustomCasbinRule extends CasbinRule {
@CreateDateColumn()
createdDate: Date;

@UpdateDateColumn()
updatedDate: Date;
}

async function myFunction() {
// Initialize a TypeORM adapter and use it in a Node-Casbin enforcer:
// The adapter can not automatically create database.
// But the adapter will automatically and use the table named "casbin_rule".
// I think ORM should not automatically create databases.
const a = await TypeORMAdapter.newAdapter(
{
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '',
database: 'casbin',
},
{
customCasbinRuleEntity: CustomCasbinRule,
},
);

const e = await newEnforcer('examples/rbac_model.conf', a);

// Load the filtered policy from DB.
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,18 @@
"@types/node": "^10.11.7",
"coveralls": "^3.0.2",
"husky": "^1.1.2",
"jest": "^23.6.0",
"jest": "^28.1.3",
"lint-staged": "^7.3.0",
"mysql2": "^2.1.0",
"pg": "^8.4.2",
"rimraf": "^2.6.2",
"ts-jest": "22.4.6",
"ts-jest": "28.0.7",
"tslint": "^5.11.0",
"typescript": "^4.7.3"
},
"dependencies": {
"casbin": "^5.11.5",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.3.6"
},
"files": [
Expand Down
123 changes: 74 additions & 49 deletions src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,45 @@
import { Helper, Model, FilteredAdapter } from 'casbin';
import { CasbinRule } from './casbinRule';
import {
Connection,
ConnectionOptions,
createConnection,
getRepository,
DataSource,
DataSourceOptions,
FindOptionsWhere,
Repository,
} from 'typeorm';
import { CasbinMongoRule } from './casbinMongoRule';

type GenericCasbinRule = CasbinRule | CasbinMongoRule;
type CasbinRuleConstructor = new (...args: any[]) => GenericCasbinRule;

interface ExistentConnection {
connection: Connection;
connection: DataSource;
}
export type TypeORMAdapterOptions = ExistentConnection | DataSourceOptions;

export interface TypeORMAdapterConfig {
customCasbinRuleEntity?: CasbinRuleConstructor;
}
export type TypeORMAdapterOptions = ExistentConnection | ConnectionOptions;

/**
* TypeORMAdapter represents the TypeORM filtered adapter for policy storage.
*/
export default class TypeORMAdapter implements FilteredAdapter {
private option: ConnectionOptions;
private typeorm: Connection;
private adapterConfig?: TypeORMAdapterConfig;
private option: DataSourceOptions;
private typeorm: DataSource;
private filtered = false;

private constructor(option: TypeORMAdapterOptions) {
private constructor(
option: TypeORMAdapterOptions,
adapterConfig?: TypeORMAdapterConfig,
) {
this.adapterConfig = adapterConfig;

if ((option as ExistentConnection).connection) {
this.typeorm = (option as ExistentConnection).connection;
this.option = this.typeorm.options;
} else {
this.option = option as ConnectionOptions;
this.option = option as DataSourceOptions;
}
}

Expand All @@ -54,47 +64,55 @@ export default class TypeORMAdapter implements FilteredAdapter {
/**
* newAdapter is the constructor.
* @param option typeorm connection option
* @param adapterConfig additional configuration options for the adapter
*/
public static async newAdapter(option: TypeORMAdapterOptions) {
public static async newAdapter(
option: TypeORMAdapterOptions,
adapterConfig?: TypeORMAdapterConfig,
) {
let a: TypeORMAdapter;

const defaults = {
synchronize: true,
name: 'node-casbin-official',
};
if ((option as ExistentConnection).connection) {
a = new TypeORMAdapter(option);
a = new TypeORMAdapter(option, adapterConfig);
} else {
const options = option as ConnectionOptions;
const entities = { entities: [this.getCasbinRuleType(options.type)] };
const options = option as DataSourceOptions;
const entities = {
entities: [
TypeORMAdapter.getCasbinRuleType(options.type, adapterConfig),
],
};
const configuration = Object.assign(defaults, options);
a = new TypeORMAdapter(Object.assign(configuration, entities));
a = new TypeORMAdapter(
Object.assign(configuration, entities),
adapterConfig,
);
}
await a.open();
return a;
}

private async open() {
if (!this.typeorm) {
this.typeorm = await createConnection(this.option);
this.typeorm = new DataSource(this.option);
}

if (!this.typeorm.isConnected) {
await this.typeorm.connect();
if (!this.typeorm.isInitialized) {
await this.typeorm.initialize();
}
}

public async close() {
if (this.typeorm.isConnected) {
await this.typeorm.close();
if (this.typeorm.isInitialized) {
await this.typeorm.destroy();
}
}

private async clearTable() {
await getRepository(
this.getCasbinRuleConstructor(),
this.option.name,
).clear();
await this.getRepository().clear();
}

private loadPolicyLine(line: GenericCasbinRule, model: Model) {
Expand All @@ -112,22 +130,19 @@ export default class TypeORMAdapter implements FilteredAdapter {
* loadPolicy loads all policy rules from the storage.
*/
public async loadPolicy(model: Model) {
const lines = await getRepository(
this.getCasbinRuleConstructor(),
this.option.name,
).find();
const lines = await this.getRepository().find();

for (const line of lines) {
this.loadPolicyLine(line, model);
}
}

// Loading policies based on filter condition
public async loadFilteredPolicy(model: Model, filter: object) {
const filteredLines = await getRepository(
this.getCasbinRuleConstructor(),
this.option.name,
).find(filter);
public async loadFilteredPolicy(
model: Model,
filter: FindOptionsWhere<GenericCasbinRule>,
) {
const filteredLines = await this.getRepository().find({ where: filter });
for (const line of filteredLines) {
this.loadPolicyLine(line, model);
}
Expand Down Expand Up @@ -211,9 +226,7 @@ export default class TypeORMAdapter implements FilteredAdapter {
*/
public async addPolicy(sec: string, ptype: string, rule: string[]) {
const line = this.savePolicyLine(ptype, rule);
await getRepository(this.getCasbinRuleConstructor(), this.option.name).save(
line,
);
await this.getRepository().save(line);
}

/**
Expand Down Expand Up @@ -247,10 +260,7 @@ export default class TypeORMAdapter implements FilteredAdapter {
*/
public async removePolicy(sec: string, ptype: string, rule: string[]) {
const line = this.savePolicyLine(ptype, rule);
await getRepository(
this.getCasbinRuleConstructor(),
this.option.name,
).delete({
await this.getRepository().delete({
...line,
});
}
Expand All @@ -260,7 +270,10 @@ export default class TypeORMAdapter implements FilteredAdapter {
*/
public async removePolicies(sec: string, ptype: string, rules: string[][]) {
const queryRunner = this.typeorm.createQueryRunner();
const type = TypeORMAdapter.getCasbinRuleType(this.option.type);
const type = TypeORMAdapter.getCasbinRuleType(
this.option.type,
this.adapterConfig,
);

await queryRunner.connect();
await queryRunner.startTransaction();
Expand Down Expand Up @@ -313,27 +326,39 @@ export default class TypeORMAdapter implements FilteredAdapter {
if (fieldIndex <= 6 && 6 < fieldIndex + fieldValues.length) {
line.v6 = fieldValues[6 - fieldIndex];
}
await getRepository(
this.getCasbinRuleConstructor(),
this.option.name,
).delete({

await this.getRepository().delete({
...line,
});
}

private getCasbinRuleConstructor(): CasbinRuleConstructor {
return TypeORMAdapter.getCasbinRuleType(this.option.type);
return TypeORMAdapter.getCasbinRuleType(
this.option.type,
this.adapterConfig,
);
}

/**
* Returns either a {@link CasbinRule} or a {@link CasbinMongoRule}, depending on the type. This switch is required as the normal
* {@link CasbinRule} does not work when using MongoDB as a backend (due to a missing ObjectID field).
* Returns either a {@link CasbinRule} or a {@link CasbinMongoRule}, depending on the type. If passed a custom entity through the adapter config it will use that entity type.
* This switch is required as the normal {@link CasbinRule} does not work when using MongoDB as a backend (due to a missing ObjectID field).
* @param type
*/
private static getCasbinRuleType(type: string): CasbinRuleConstructor {
private static getCasbinRuleType(
type: string,
adapterConfig?: TypeORMAdapterConfig,
): CasbinRuleConstructor {
if (adapterConfig?.customCasbinRuleEntity) {
return adapterConfig.customCasbinRuleEntity;
}

if (type === 'mongodb') {
return CasbinMongoRule;
}
return CasbinRule;
}

private getRepository(): Repository<GenericCasbinRule> {
return this.typeorm.getRepository(this.getCasbinRuleConstructor());
}
}
72 changes: 72 additions & 0 deletions test/adapter-config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2018 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.

import { Enforcer } from 'casbin';
import {
CreateDateColumn,
DataSource,
Entity,
UpdateDateColumn,
} from 'typeorm';
import TypeORMAdapter, { CasbinRule } from '../src/index';
import { connectionConfig } from './config';

@Entity('custom_rule')
class CustomCasbinRule extends CasbinRule {
@CreateDateColumn()
public createdDate: Date;

@UpdateDateColumn()
public updatedDate: Date;
}

test(
'TestAdapter',
async () => {
const datasource = new DataSource({
...connectionConfig,
entities: [CustomCasbinRule],
synchronize: true,
});

const a = await TypeORMAdapter.newAdapter(
{ connection: datasource },
{
customCasbinRuleEntity: CustomCasbinRule,
},
);
try {
// Because the DB is empty at first,
// so we need to load the policy from the file adapter (.CSV) first.
const e = new Enforcer();

await e.initWithFile(
'examples/rbac_model.conf',
'examples/rbac_policy.csv',
);

// This is a trick to save the current policy to the DB.
// We can't call e.savePolicy() because the adapter in the enforcer is still the file adapter.
// The current policy means the policy in the Node-Casbin enforcer (aka in memory).
await a.savePolicy(e.getModel());

const rules = await datasource.getRepository(CustomCasbinRule).find();
expect(rules[0].createdDate).not.toBeFalsy();
expect(rules[0].updatedDate).not.toBeFalsy();
} finally {
a.close();
}
},
60 * 1000,
);

0 comments on commit 65bd719

Please sign in to comment.