moleculer-iam
Version:
Centralized IAM module for moleculer. Including a certified OIDC provider and an Identity provider for user profile, credentials, and custom claims management. Custom claims could be defined/updated by declarative schema which contains claims validation a
308 lines (307 loc) • 12.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.IDP_RDBMS_Adapter = void 0;
const tslib_1 = require("tslib");
const _ = tslib_1.__importStar(require("lodash"));
const path_1 = tslib_1.__importDefault(require("path"));
const bcrypt_1 = tslib_1.__importDefault(require("bcrypt"));
const dataloader_1 = tslib_1.__importDefault(require("dataloader"));
const moment_1 = tslib_1.__importDefault(require("moment"));
const rdbms_1 = require("../../../lib/rdbms");
const adapter_1 = require("../adapter");
const model_1 = require("./model");
/* Postgres, MySQL, MariaDB, SQLite and Microsoft SQL Server supported */
// tslint:disable-next-line:class-name
class IDP_RDBMS_Adapter extends adapter_1.IDPAdapter {
constructor(props, options) {
super(props);
this.props = props;
this.displayName = "RDBMS";
this.getVersionedClaimsLoader = new dataloader_1.default(async (entries) => {
const where = {
id: entries.map(entry => entry.id),
key: [...new Set(entries.reduce((keys, entry) => keys.concat(entry.claims.map(c => c.key)), []))],
};
const foundClaimsList = new Array(entries.length).fill(null).map(() => ({}));
const raws = await this.IdentityClaims.findAll({ where });
for (const raw of raws) {
const claim = raw.get({ plain: true });
const entryIndex = entries.findIndex(e => e.id === claim.id);
const entry = entries[entryIndex];
const foundClaims = foundClaimsList[entryIndex];
const foundClaim = entry.claims.find(c => c.key === claim.key);
const specificVersion = foundClaim && foundClaim.schemaVersion;
if (!specificVersion || specificVersion === claim.schemaVersion) {
foundClaims[claim.key] = claim.value;
}
}
return foundClaimsList;
}, {
cache: false,
maxBatchSize: 100,
});
// create manager
const { claimsMigrationLockTimeoutSeconds = 100, ...opts } = options || {};
this.claimsMigrationLockTimeoutSeconds = claimsMigrationLockTimeoutSeconds;
this.manager = new rdbms_1.RDBMSManager({
logger: props.logger,
migrationDirPath: path_1.default.join(__dirname, "./migrations"),
migrationTableName: "idpMigrations",
}, options);
}
/* define and migrate model schema */
async start() {
// await this.manager.rollback({ to: 0 }); // uncomment this line to develop migrations scripts
await this.manager.migrate();
// define models
await model_1.defineAdapterModels(this.manager);
await super.start();
}
async stop() {
await this.manager.dispose();
await super.stop();
}
/* fetch from synced cache */
get IdentityCache() {
return this.manager.getModel("IdentityCache");
}
// args will be like { claims:{}, metadata:{}, ... }
async find(args) {
return this.IdentityCache.findOne({ where: args, attributes: ["id"] })
.then(raw => raw ? raw.get("id") : undefined);
}
// args will be like { claims:{}, metadata:{}, ... }
async count(args) {
return this.IdentityCache.count({ where: args });
}
// args will be like { where: { claims:{}, metadata:{}, ...}, offset: 0, limit: 100, ... }
async get(args) {
args.attributes = ["id"];
return this.IdentityCache.findAll(args)
.then(raws => raws.map(raw => raw.get("id")));
}
/* delete */
async delete(id, transaction) {
let isolated = false;
if (!transaction) {
transaction = await this.transaction();
isolated = true;
}
try {
const where = { id };
let count = await this.IdentityMetadata.destroy({ where, transaction });
count += await this.IdentityClaims.destroy({ where, transaction });
count += await this.IdentityClaimsCache.destroy({ where, transaction });
count += await this.IdentityCredentials.destroy({ where, transaction });
if (isolated) {
await transaction.commit();
}
return count > 0;
}
catch (error) {
if (isolated) {
await transaction.rollback();
}
throw error;
}
}
/* metadata */
get IdentityMetadata() {
return this.manager.getModel("IdentityMetadata");
}
async createOrUpdateMetadata(id, metadata, transaction) {
const [model, created] = await this.IdentityMetadata.findOrCreate({
where: { id },
defaults: { data: metadata },
transaction,
});
if (!created) {
await model.update({
data: _.defaultsDeep(metadata, model.get({ plain: true }).data || {}),
}, {
transaction,
});
}
}
async getMetadata(id) {
return this.IdentityMetadata.findOne({ where: { id } })
.then(raw => raw ? raw.get("data") : undefined);
}
/* claims */
get IdentityClaims() {
return this.manager.getModel("IdentityClaims");
}
async createOrUpdateVersionedClaims(id, claims) {
await this.IdentityClaims.bulkCreate(claims.map(({ key, value, schemaVersion }) => ({ id, key, schemaVersion, value })), {
fields: ["id", "key", "schemaVersion", "value"],
updateOnDuplicate: ["value"],
});
}
async getVersionedClaims(id, claims) {
return this.getVersionedClaimsLoader.load({ id, claims });
}
/* cache */
get IdentityClaimsCache() {
return this.manager.getModel("IdentityClaimsCache");
}
async onClaimsUpdated(id, updatedClaims, transaction) {
const claims = await this.getClaims(id, []);
const mergedClaims = _.defaultsDeep(updatedClaims, claims);
// this.logger.debug("sync identity claims cache:", updatedClaims);
await this.IdentityClaimsCache.upsert({
id,
data: mergedClaims,
}, {
transaction,
});
}
/* credentials */
get IdentityCredentials() {
return this.manager.getModel("IdentityCredentials");
}
async createOrUpdateCredentials(id, credentials, transaction) {
const hashedCredentials = {};
// hash credentials
if (credentials.password) {
hashedCredentials.password = await bcrypt_1.default.hash(credentials.password, 10);
}
const [model, created] = await this.IdentityCredentials.findOrCreate({
where: { id },
defaults: hashedCredentials,
transaction,
});
if (!created) {
// not changed
if (await this.assertCredentials(id, credentials)) {
return false;
}
await model.update(hashedCredentials, { transaction });
}
return true;
}
async assertCredentials(id, credentials) {
const model = await this.IdentityCredentials.findOne({ where: { id } });
if (!model) {
return null;
}
const hashedCredentials = model.get({ plain: true });
// password
if (typeof credentials.password !== "undefined" && typeof hashedCredentials.password !== "undefined") {
return bcrypt_1.default.compare(credentials.password, hashedCredentials.password)
.catch(error => {
this.logger.error(error);
return false;
});
}
this.logger.error(`unimplemented credentials type: ${Object.keys(credentials)}`);
return null;
}
/* claims schema */
get IdentityClaimsSchema() {
return this.manager.getModel("IdentityClaimsSchema");
}
async createClaimsSchema(schema, transaction) {
await this.IdentityClaimsSchema.upsert(schema, { transaction });
}
/*
private serializeRegExpIncludedClaimsSchema(schema: IdentityClaimsSchema): IdentityClaimsSchema {
if (schema.validation && (schema.validation as any).regexp && (schema.validation as any).regexp instanceof RegExp) {
const schemaWithRegExp = _.cloneDeep(schema);
(schemaWithRegExp.validation as any).regexp = (schema.validation as any).regexp.source.toString();
return schemaWithRegExp;
}
return schema;
}
private unserializeRegExpIncludedClaimsSchema(schema: IdentityClaimsSchema): IdentityClaimsSchema {
if (schema.validation && (schema.validation as any).regexp && !((schema.validation as any).regexp instanceof RegExp)) {
const schemaWithRegExp = _.cloneDeep(schema);
(schemaWithRegExp.validation as any).regexp = new RegExp((schema.validation as any).regexp);
return schemaWithRegExp;
}
return schema;
}
*/
async forceDeleteClaimsSchema(key) {
await this.IdentityClaimsSchema.destroy({ where: { key } });
}
async getClaimsSchema(args) {
const { key, version, active } = args;
const where = { key };
if (typeof version !== "undefined") {
where.version = version;
}
if (typeof active !== "undefined") {
where.active = active;
}
return this.IdentityClaimsSchema
.findOne({ where })
.then(raw => raw ? raw.get({ plain: true }) : undefined);
}
async setActiveClaimsSchema(args, transaction) {
const { key, version } = args;
await this.IdentityClaimsSchema.update({ active: rdbms_1.Sequelize.literal(`version = '${version}'`) }, { where: { key }, fields: ["active"], transaction });
}
async getClaimsSchemata(args) {
const { scope, key, version, active } = args;
const where = {};
if (scope && scope.length !== 0) {
where.scope = scope;
}
if (typeof key !== "undefined") {
where.key = key;
}
if (typeof version !== "undefined") {
where.version = version;
}
if (typeof active !== "undefined") {
where.active = active;
}
return this.IdentityClaimsSchema
.findAll({ where })
.then(raws => raws.map(raw => raw.get({ plain: true })));
}
/* transaction and migration lock for distributed system */
async transaction() {
return this.manager.sequelize.transaction({
autocommit: false,
type: rdbms_1.Transaction.TYPES.DEFERRED,
isolationLevel: rdbms_1.Transaction.ISOLATION_LEVELS.READ_UNCOMMITTED,
});
}
get IdentityClaimsMigrationLock() {
return this.manager.getModel("IdentityClaimsMigrationLock");
}
async acquireMigrationLock(key) {
const lock = await this.IdentityClaimsMigrationLock.findOne();
if (lock) {
const now = moment_1.default();
const deadline = moment_1.default(lock.get("updatedAt")).add(this.claimsMigrationLockTimeoutSeconds, "s");
// force release lock
if (now.isAfter(deadline)) {
const deadLockKey = lock.get("key");
this.logger.info(`force release migration lock which is dead over ${this.claimsMigrationLockTimeoutSeconds} seconds:`, deadLockKey);
await this.releaseMigrationLock(deadLockKey);
}
// acquire lock again
this.logger.info(`retry to acquire migration lock after 5s: ${key}`);
await new Promise(resolve => setTimeout(resolve, 5 * 1000));
return this.acquireMigrationLock(key);
}
try {
await this.IdentityClaimsMigrationLock.create({ key });
}
catch (error) {
this.logger.warn(`failed to create migration lock, retry after 3s: ${key}`);
await new Promise(resolve => setTimeout(resolve, 3 * 1000));
return this.acquireMigrationLock(key);
}
}
async touchMigrationLock(key, migratedIdentitiesNumber) {
await this.IdentityClaimsMigrationLock.update({ number: migratedIdentitiesNumber }, { where: { key } });
}
async releaseMigrationLock(key) {
await this.IdentityClaimsMigrationLock.destroy({ where: { key } });
}
}
exports.IDP_RDBMS_Adapter = IDP_RDBMS_Adapter;
//# sourceMappingURL=adapter.js.map