casbin-mongoose-adapter
Version:
Mongoose adapter for Casbin
663 lines (593 loc) • 22.3 kB
text/typescript
// Copyright 2019 The elastic.io team (http://elastic.io). 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 { BatchAdapter, FilteredAdapter, Helper, logPrint, Model, UpdatableAdapter } from 'casbin';
import {
ClientSession,
Connection,
ConnectOptions,
createConnection,
FilterQuery,
Model as MongooseModel
} from 'mongoose';
import { AdapterError, InvalidAdapterTypeError } from './errors';
import { collectionName, IModel, modelName, schema } from './model';
export interface MongooseAdapterOptions {
filtered?: boolean;
synced?: boolean;
autoAbort?: boolean;
autoCommit?: boolean;
timestamps?: boolean;
}
export interface policyLine {
ptype?: string;
v0?: string;
v1?: string;
v2?: string;
v3?: string;
v4?: string;
v5?: string;
}
export interface sessionOption {
session?: ClientSession;
}
/**
* Implements a policy adapter for casbin with MongoDB support.
*
* @class
*/
export class MongooseAdapter implements BatchAdapter, FilteredAdapter, UpdatableAdapter {
public connection?: Connection;
private filtered: boolean;
private isSynced: boolean;
private uri: string;
private options?: ConnectOptions;
private autoAbort: boolean;
private autoCommit: boolean;
private session: ClientSession;
private casbinRule: MongooseModel<IModel>;
/**
* Creates a new instance of mongoose adapter for casbin.
* It does not wait for successfull connection to MongoDB.
* So, if you want to have a possibility to wait until connection successful, use newAdapter instead.
*
* @constructor
* @param {String} uri Mongo URI where casbin rules must be persisted
* @param {Object} [options={}] Additional options to pass on to mongoose client
* @param {Object} [adapterOptions={}] adapterOptions additional adapter options
* @example
* const adapter = new MongooseAdapter('MONGO_URI');
* const adapter = new MongooseAdapter('MONGO_URI', { mongoose_options: 'here' })
*/
constructor(uri: string, options?: ConnectOptions, adapterOptions?: MongooseAdapterOptions) {
if (!uri) {
throw new AdapterError('You must provide Mongo URI to connect to!');
}
// by default, adapter is not filtered
this.filtered = false;
this.isSynced = false;
this.autoAbort = false;
this.uri = uri;
this.options = options;
this.connection = createConnection(this.uri, this.options);
this.casbinRule = this.connection.model<IModel>(
modelName,
schema(adapterOptions?.timestamps),
collectionName
);
}
/**
* Creates a new instance of mongoose adapter for casbin.
* Instead of constructor, it does wait for successfull connection to MongoDB.
* Preferable way to construct an adapter instance, is to use this static method.
*
* @static
* @param {String} uri Mongo URI where casbin rules must be persisted
* @param {Object} [options={}] Additional options to pass on to mongoose client
* @param {Object} [adapterOptions={}] Additional options to pass on to adapter
* @example
* const adapter = await MongooseAdapter.newAdapter('MONGO_URI');
* const adapter = await MongooseAdapter.newAdapter('MONGO_URI', { mongoose_options: 'here' });
*/
static async newAdapter(
uri: string,
options?: ConnectOptions,
adapterOptions: MongooseAdapterOptions = {}
) {
const adapter = new MongooseAdapter(uri, options, adapterOptions);
const {
filtered = false,
synced = false,
autoAbort = false,
autoCommit = false
} = adapterOptions;
adapter.setFiltered(filtered);
adapter.setSynced(synced);
adapter.setAutoAbort(autoAbort);
adapter.setAutoCommit(autoCommit);
return adapter;
}
/**
* Creates a new instance of mongoose adapter for casbin.
* It does the same as newAdapter, but it also sets a flag that this adapter is in filtered state.
* That way, casbin will not call loadPolicy() automatically.
*
* @static
* @param {String} uri Mongo URI where casbin rules must be persisted
* @param {Object} [options] Additional options to pass on to mongoose client
* @example
* const adapter = await MongooseAdapter.newFilteredAdapter('MONGO_URI');
* const adapter = await MongooseAdapter.newFilteredAdapter('MONGO_URI', { mongoose_options: 'here' });
*/
static async newFilteredAdapter(uri: string, options?: ConnectOptions) {
const adapter = await MongooseAdapter.newAdapter(uri, options, {
filtered: true
});
return adapter;
}
/**
* Creates a new instance of mongoose adapter for casbin.
* It does the same as newAdapter, but it checks wether database is a replica set. If it is, it enables
* transactions for the adapter.
* Transactions are never commited automatically. You have to use commitTransaction to add pending changes.
*
* @static
* @param {String} uri Mongo URI where casbin rules must be persisted
* @param {Object} [options={}] Additional options to pass on to mongoose client
* @param {Boolean} autoAbort Whether to abort transactions on Error automatically
* @param autoCommit
* @example
* const adapter = await MongooseAdapter.newFilteredAdapter('MONGO_URI');
* const adapter = await MongooseAdapter.newFilteredAdapter('MONGO_URI', { mongoose_options: 'here' });
*/
static async newSyncedAdapter(
uri: string,
options?: ConnectOptions,
autoAbort: boolean = true,
autoCommit = true
) {
return await MongooseAdapter.newAdapter(uri, options, {
synced: true,
autoAbort,
autoCommit
});
}
/**
* Switch adapter to (non)filtered state.
* Casbin uses this flag to determine if it should load the whole policy from DB or not.
*
* @param {Boolean} [enable=true] Flag that represents the current state of adapter (filtered or not)
*/
setFiltered(enable: boolean = true) {
this.filtered = enable;
}
/**
* isFiltered determines whether the filtered model is enabled for the adapter.
* @returns {boolean}
*/
isFiltered(): boolean {
return this.filtered;
}
/**
* SyncedAdapter: Switch adapter to (non)synced state.
* This enables mongoDB transactions when loading and saving policies to DB.
*
* @param {Boolean} [synced=true] Flag that represents the current state of adapter (filtered or not)
*/
setSynced(synced: boolean = true) {
this.isSynced = synced;
}
/**
* SyncedAdapter: Automatically abort on Error.
* When enabled, functions will automatically abort on error
*
* @param {Boolean} [abort=true] Flag that represents if automatic abort should be enabled or not
*/
setAutoAbort(abort: boolean = true) {
if (this.isSynced) this.autoAbort = abort;
}
/**
* SyncedAdapter: Automatically commit after each addition.
* When enabled, functions will automatically commit after function has finished
*
* @param {Boolean} [commit=true] Flag that represents if automatic commit should be enabled or not
*/
setAutoCommit(commit: boolean = true) {
if (this.isSynced) this.autoCommit = commit;
}
/**
* SyncedAdapter: Gets active session or starts a new one. Sessions are used to handle transactions.
*/
async getSession(): Promise<ClientSession> {
if (this.isSynced) {
return this.session && this.session.inTransaction()
? this.session
: this.connection!.startSession();
} else
throw new InvalidAdapterTypeError(
'Transactions are only supported by SyncedAdapter. See newSyncedAdapter'
);
}
/**
* SyncedAdapter: Sets current session to specific one. Do not use this unless you know what you are doing.
*/
async setSession(session: ClientSession) {
if (this.isSynced) {
this.session = session;
} else {
throw new InvalidAdapterTypeError(
'Sessions are only supported by SyncedAdapter. See newSyncedAdapter'
);
}
}
/**
* SyncedAdapter: Gets active transaction or starts a new one. Transaction must be closed before changes are done
* to the database. See: commitTransaction, abortTransaction
* @returns {Promise<ClientSession>} Returns a session with active transaction
*/
async getTransaction(): Promise<ClientSession> {
if (this.isSynced) {
const session = await this.getSession();
if (!session.inTransaction()) {
await this.casbinRule.createCollection();
await session.startTransaction();
logPrint(
'Transaction started. To commit changes use adapter.commitTransaction() or to abort use adapter.abortTransaction()'
);
}
return session;
} else
throw new InvalidAdapterTypeError(
'Transactions are only supported by SyncedAdapter. See newSyncedAdapter'
);
}
/**
* SyncedAdapter: Commits active transaction. Documents are not saved before this function is used.
* Transaction closes after the use of this function.
* @returns {Promise<void>}
*/
async commitTransaction(): Promise<void> {
if (this.isSynced) {
const session = await this.getSession();
await session.commitTransaction();
} else
throw new InvalidAdapterTypeError(
'Transactions are only supported by SyncedAdapter. See newSyncedAdapter'
);
}
/**
* SyncedAdapter: Aborts active transaction. All Document changes within this transaction are reverted.
* Transaction closes after the use of this function.
* @returns {Promise<void>}
*/
async abortTransaction(): Promise<void> {
if (this.isSynced) {
const session = await this.getSession();
await session.abortTransaction();
logPrint('Transaction aborted');
} else
throw new InvalidAdapterTypeError(
'Transactions are only supported by SyncedAdapter. See newSyncedAdapter'
);
}
/**
* Loads one policy rule into casbin model.
* This method is used by casbin and should not be called by user.
*
* @param {Object} line Record with one policy rule from MongoDB
* @param {Object} model Casbin model to which policy rule must be loaded
*/
loadPolicyLine(line: policyLine, model: Model) {
let lineText = `${line.ptype!}`;
for (const word of [line.v0, line.v1, line.v2, line.v3, line.v4, line.v5]) {
if (word !== undefined) {
let wrappedWord = /^".*"$/.test(word) ? word : `"${word}"`;
lineText = `${lineText},${wrappedWord}`;
} else {
break;
}
}
if (lineText) {
Helper.loadPolicyLine(lineText, model);
}
}
/**
* Implements the process of loading policy from database into enforcer.
* This method is used by casbin and should not be called by user.
*
* @param {Model} model Model instance from enforcer
* @returns {Promise<void>}
*/
async loadPolicy(model: Model): Promise<void> {
return this.loadFilteredPolicy(model, null);
}
/**
* Loads partial policy based on filter criteria.
* This method is used by casbin and should not be called by user.
*
* @param {Model} model Enforcer model
* @param {Object} [filter] MongoDB filter to query
*/
async loadFilteredPolicy(model: Model, filter: FilterQuery<IModel> | null) {
if (filter) {
this.setFiltered(true);
} else {
this.setFiltered(false);
}
const options: sessionOption = {};
if (this.isSynced) options.session = await this.getTransaction();
const lines = await this.casbinRule.find(filter || {}, null, options).lean();
this.autoCommit && options.session && (await options.session.commitTransaction());
for (const line of lines) {
this.loadPolicyLine(line, model);
}
}
/**
* Generates one policy rule ready to be saved into MongoDB.
* This method is used by casbin to generate Mongoose Model Object for single policy
* and should not be called by user.
*
* @param {String} pType Policy type to save into MongoDB
* @param {Array<String>} rule An array which consists of policy rule elements to store
* @returns {IModel} Returns a created CasbinRule record for MongoDB
*/
savePolicyLine(pType: string, rule: string[]): IModel {
const model = new this.casbinRule({ ptype: pType });
if (rule.length > 0) {
model.v0 = rule[0];
}
if (rule.length > 1) {
model.v1 = rule[1];
}
if (rule.length > 2) {
model.v2 = rule[2];
}
if (rule.length > 3) {
model.v3 = rule[3];
}
if (rule.length > 4) {
model.v4 = rule[4];
}
if (rule.length > 5) {
model.v5 = rule[5];
}
return model;
}
/**
* Implements the process of saving policy from enforcer into database.
* If you are using replica sets with mongo, this function will use mongo
* transaction, so every line in the policy needs tosucceed for this to
* take effect.
* This method is used by casbin and should not be called by user.
*
* @param {Model} model Model instance from enforcer
* @returns {Promise<Boolean>}
*/
async savePolicy(model: Model) {
const options: sessionOption = {};
if (this.isSynced) options.session = await this.getTransaction();
try {
const lines: IModel[] = [];
const policyRuleAST = model.model.get('p') instanceof Map ? model.model.get('p')! : new Map();
const groupingPolicyAST =
model.model.get('g') instanceof Map ? model.model.get('g')! : new Map();
for (const [ptype, ast] of policyRuleAST) {
for (const rule of ast.policy) {
lines.push(this.savePolicyLine(ptype, rule));
}
}
for (const [ptype, ast] of groupingPolicyAST) {
for (const rule of ast.policy) {
lines.push(this.savePolicyLine(ptype, rule));
}
}
await this.casbinRule.collection.insertMany(lines, options);
this.autoCommit && options.session && (await options.session.commitTransaction());
} catch (err) {
this.autoAbort && options.session && (await options.session.abortTransaction());
console.error(err);
return false;
}
return true;
}
/**
* Implements the process of adding policy rule.
* This method is used by casbin and should not be called by user.
*
* @param {String} sec Section of the policy
* @param {String} pType Type of the policy (e.g. "p" or "g")
* @param {Array<String>} rule Policy rule to add into enforcer
* @returns {Promise<void>}
*/
async addPolicy(sec: string, pType: string, rule: string[]): Promise<void> {
const options: sessionOption = {};
try {
if (this.isSynced) options.session = await this.getTransaction();
const line = this.savePolicyLine(pType, rule);
await line.save(options);
this.autoCommit && options.session && (await options.session.commitTransaction());
} catch (err) {
this.autoAbort && options.session && (await options.session.abortTransaction());
throw err;
}
}
/**
* Implements the process of adding a list of policy rules.
* This method is used by casbin and should not be called by user.
*
* @param {String} sec Section of the policy
* @param {String} pType Type of the policy (e.g. "p" or "g")
* @param {Array<String>} rules Policy rule to add into enforcer
* @returns {Promise<void>}
*/
async addPolicies(sec: string, pType: string, rules: Array<string[]>): Promise<void> {
const options: sessionOption = {};
if (this.isSynced) options.session = await this.getTransaction();
else
throw new InvalidAdapterTypeError(
'addPolicies is only supported by SyncedAdapter. See newSyncedAdapter'
);
try {
const promises = rules.map(async (rule) => this.addPolicy(sec, pType, rule));
await Promise.all(promises);
this.autoCommit && options.session && (await options.session.commitTransaction());
} catch (err) {
this.autoAbort && options.session && (await options.session.abortTransaction());
throw err;
}
}
/**
* Implements the process of updating policy rule.
* This method is used by casbin and should not be called by user.
*
* @param {String} sec Section of the policy
* @param {String} pType Type of the policy (e.g. "p" or "g")
* @param {Array<String>} oldRule Policy rule to remove from enforcer
* @param {Array<String>} newRule Policy rule to add into enforcer
* @returns {Promise<void>}
*/
async updatePolicy(
sec: string,
pType: string,
oldRule: string[],
newRule: string[]
): Promise<void> {
const options: sessionOption = {};
try {
if (this.isSynced) options.session = await this.getTransaction();
const { ptype, v0, v1, v2, v3, v4, v5 } = this.savePolicyLine(pType, oldRule);
const newRuleLine = this.savePolicyLine(pType, newRule);
const newModel = {
ptype: newRuleLine.ptype,
v0: newRuleLine.v0,
v1: newRuleLine.v1,
v2: newRuleLine.v2,
v3: newRuleLine.v3,
v4: newRuleLine.v4,
v5: newRuleLine.v5
};
await this.casbinRule.updateOne({ ptype, v0, v1, v2, v3, v4, v5 }, newModel, options);
this.autoCommit && options.session && (await options.session.commitTransaction());
} catch (err) {
this.autoAbort && options.session && (await options.session.abortTransaction());
throw err;
}
}
/**
* Implements the process of removing a list of policy rules.
* This method is used by casbin and should not be called by user.
*
* @param {String} sec Section of the policy
* @param {String} pType Type of the policy (e.g. "p" or "g")
* @param {Array<String>} rule Policy rule to remove from enforcer
* @returns {Promise<void>}
*/
async removePolicy(sec: string, pType: string, rule: string[]): Promise<void> {
const options: sessionOption = {};
try {
if (this.isSynced) options.session = await this.getTransaction();
const { ptype, v0, v1, v2, v3, v4, v5 } = this.savePolicyLine(pType, rule);
await this.casbinRule.deleteMany({ ptype, v0, v1, v2, v3, v4, v5 }, options);
this.autoCommit && options.session && (await options.session.commitTransaction());
} catch (err) {
this.autoAbort && options.session && (await options.session.abortTransaction());
throw err;
}
}
/**
* Implements the process of removing a policyList rules.
* This method is used by casbin and should not be called by user.
*
* @param {String} sec Section of the policy
* @param {String} pType Type of the policy (e.g. "p" or "g")
* @param {Array<String>} rules Policy rule to remove from enforcer
* @returns {Promise<void>}
*/
async removePolicies(sec: string, pType: string, rules: Array<string[]>): Promise<void> {
const options: sessionOption = {};
try {
if (this.isSynced) options.session = await this.getTransaction();
else
throw new InvalidAdapterTypeError(
'removePolicies is only supported by SyncedAdapter. See newSyncedAdapter'
);
const promises = rules.map(async (rule) => this.removePolicy(sec, pType, rule));
await Promise.all(promises);
this.autoCommit && options.session && (await options.session.commitTransaction());
} catch (err) {
this.autoAbort && options.session && (await options.session.abortTransaction());
throw err;
}
}
/**
* Implements the process of removing policy rules.
* This method is used by casbin and should not be called by user.
*
* @param {String} sec Section of the policy
* @param {String} pType Type of the policy (e.g. "p" or "g")
* @param {Number} fieldIndex Index of the field to start filtering from
* @param {...String} fieldValues Policy rule to match when removing (starting from fieldIndex)
* @returns {Promise<void>}
*/
async removeFilteredPolicy(
sec: string,
pType: string,
fieldIndex: number,
...fieldValues: string[]
): Promise<void> {
const options: sessionOption = {};
try {
if (this.isSynced) options.session = await this.getTransaction();
const where: any = pType ? { ptype: pType } : {};
if (fieldIndex <= 0 && fieldIndex + fieldValues.length > 0 && fieldValues[0 - fieldIndex]) {
if (pType === 'g') {
where.$or = [{ v0: fieldValues[0 - fieldIndex] }, { v1: fieldValues[0 - fieldIndex] }];
} else {
where.v0 = fieldValues[0 - fieldIndex];
}
}
if (fieldIndex <= 1 && fieldIndex + fieldValues.length > 1 && fieldValues[1 - fieldIndex]) {
where.v1 = fieldValues[1 - fieldIndex];
}
if (fieldIndex <= 2 && fieldIndex + fieldValues.length > 2 && fieldValues[2 - fieldIndex]) {
where.v2 = fieldValues[2 - fieldIndex];
}
if (fieldIndex <= 3 && fieldIndex + fieldValues.length > 3 && fieldValues[3 - fieldIndex]) {
where.v3 = fieldValues[3 - fieldIndex];
}
if (fieldIndex <= 4 && fieldIndex + fieldValues.length > 4 && fieldValues[4 - fieldIndex]) {
where.v4 = fieldValues[4 - fieldIndex];
}
if (fieldIndex <= 5 && fieldIndex + fieldValues.length > 5 && fieldValues[5 - fieldIndex]) {
where.v5 = fieldValues[5 - fieldIndex];
}
await this.casbinRule.deleteMany(where, options);
this.autoCommit && options.session && (await options.session.commitTransaction());
} catch (err) {
this.autoAbort && options.session && (await options.session.abortTransaction());
throw err;
}
}
async close() {
if (this.connection) {
if (this.session) await this.session.endSession();
await this.connection.close();
}
}
/**
* Just for testing.
*/
getCasbinRule() {
return this.casbinRule;
}
}