UNPKG

@backstage-community/plugin-rbac-backend

Version:
333 lines (327 loc) 11.2 kB
'use strict'; var errors = require('@backstage/errors'); var yaml = require('js-yaml'); var lodash = require('lodash'); var fs = require('fs'); var auditor = require('../auditor/auditor.cjs.js'); var helper = require('../helper.cjs.js'); var conditionValidation = require('../validation/condition-validation.cjs.js'); var fileWatcher = require('./file-watcher.cjs.js'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; } var yaml__default = /*#__PURE__*/_interopDefaultCompat(yaml); var fs__default = /*#__PURE__*/_interopDefaultCompat(fs); const DEFAULT_CONDITIONAL_POLICIES_FILE_LIMITS = { maxBytes: 1024 * 1024, maxDocuments: 256 }; function assertPositiveIntegerConditionalPoliciesFileLimit(value, fieldRef) { if (!Number.isInteger(value) || value <= 0) { throw new errors.InputError( `'${fieldRef}' must be a positive integer for conditional policies file validation` ); } } function resolveConditionalPoliciesFileLimits(partial = {}, errorFieldRefs) { const resolved = { maxBytes: partial.maxBytes ?? DEFAULT_CONDITIONAL_POLICIES_FILE_LIMITS.maxBytes, maxDocuments: partial.maxDocuments ?? DEFAULT_CONDITIONAL_POLICIES_FILE_LIMITS.maxDocuments }; assertPositiveIntegerConditionalPoliciesFileLimit( resolved.maxBytes, errorFieldRefs?.maxBytes ?? "maxBytes" ); assertPositiveIntegerConditionalPoliciesFileLimit( resolved.maxDocuments, errorFieldRefs?.maxDocuments ?? "maxDocuments" ); return resolved; } function yamlConditionEquals(stored, desired) { const storedComparable = { ...lodash.omit(stored, ["id"]), permissionMapping: helper.permissionMappingToActions(stored.permissionMapping) }; return helper.deepSortEqual(storedComparable, lodash.omit(desired, ["id"])); } class YamlConditionalPoliciesFileWatcher extends fileWatcher.AbstractFileWatcher { constructor(filePath, allowReload, logger, conditionalStorage, auditor, auth, pluginMetadataCollector, roleMetadataStorage, roleEventEmitter, conditionValidationLimits, limits = {}) { super(filePath, allowReload, logger); this.conditionalStorage = conditionalStorage; this.auditor = auditor; this.auth = auth; this.pluginMetadataCollector = pluginMetadataCollector; this.roleMetadataStorage = roleMetadataStorage; this.roleEventEmitter = roleEventEmitter; const resolvedLimits = resolveConditionalPoliciesFileLimits(limits); this.maxFileBytes = resolvedLimits.maxBytes; this.maxFileDocuments = resolvedLimits.maxDocuments; this.conditionValidationLimits = conditionValidationLimits; } maxFileBytes; maxFileDocuments; conditionValidationLimits; async initialize() { if (!this.filePath) { return; } const fileExists = fs__default.default.existsSync(this.filePath); if (!fileExists) { const auditorEvent = await this.auditor.createEvent({ eventId: auditor.ConditionEvents.CONDITIONAL_POLICIES_FILE_NOT_FOUND, severityLevel: "medium" }); await auditorEvent.fail({ error: new Error(`File '${this.filePath}' was not found`) }); return; } this.roleEventEmitter.on("roleAdded", this.onChange.bind(this)); await this.onChange(); if (this.allowReload) { this.watchFile(); } } async onChange() { try { await this.syncYamlConditionalPoliciesFile(); } catch (error) { const auditorEvent = await this.auditor.createEvent({ eventId: auditor.ConditionEvents.CONDITIONAL_POLICIES_FILE_CHANGE, severityLevel: "medium" }); await auditorEvent.fail({ error }); } } /** * Reads the current contents of the file and parses it. * @returns parsed data. */ parse() { const fileContents = this.getCurrentContents(); const fileSizeInBytes = Buffer.byteLength(fileContents, "utf8"); if (fileSizeInBytes > this.maxFileBytes) { throw new errors.InputError( `conditional policies file exceeds maximum size of ${this.maxFileBytes} bytes` ); } const parsedDocuments = []; yaml__default.default.loadAll(fileContents, (doc) => { if (doc === null) { return; } parsedDocuments.push( doc ); if (parsedDocuments.length > this.maxFileDocuments) { throw new errors.InputError( `conditional policies file exceeds maximum of ${this.maxFileDocuments} YAML documents` ); } }); for (const condition of parsedDocuments) { conditionValidation.validateRoleCondition(condition, this.conditionValidationLimits); } return parsedDocuments; } async syncYamlConditionalPoliciesFile() { const parsed = this.parse(); const csvFileSourcedRoles = await this.roleMetadataStorage.filterRoleMetadata("csv-file"); const fileDesired = this.filterParsedToCsvFileSourcedRoles( parsed, csvFileSourcedRoles ); const stored = await this.loadStoredConditionsForRoles(csvFileSourcedRoles); const diff = helper.diffConditionalPolicies( stored, fileDesired, yamlConditionEquals ); if (diff.toAdd.length === 0 && diff.toRemove.length === 0) { return; } try { const stagedAdditions = []; for (const condition of diff.toAdd) { stagedAdditions.push( await helper.processConditionMapping( condition, this.pluginMetadataCollector, this.auth ) ); } const plan = helper.planConditionalReconcile( stagedAdditions, diff.toRemove, (item) => helper.permissionMappingToActions(item.permissionMapping) ); const pendingDeleteIds = helper.pendingDeleteIdsFromPlan(plan); for (const { stored: storedRow, desired } of plan.updates) { await this.persistConditionUpdate( storedRow.id, desired, pendingDeleteIds ); } for (const conditionToCreate of plan.creates) { await this.persistConditionCreate(conditionToCreate, pendingDeleteIds); } for (const condition of plan.deletes) { await this.persistConditionDelete(condition); } } catch (error) { await helper.abortConditionalPolicyReconcile({ logger: this.logger, auditor: this.auditor, source: "conditional-policies-file", abortEventId: auditor.ConditionEvents.CONDITIONAL_POLICIES_FILE_CHANGE, pendingAdds: diff.toAdd.length, pendingRemoves: diff.toRemove.length, pluginIds: [...new Set(diff.toAdd.map((c) => c.pluginId))], error: helper.toError(error) }); } } filterParsedToCsvFileSourcedRoles(parsed, csvFileSourcedRoles) { const fileDesired = []; for (const condition of parsed) { const roleMetadata = csvFileSourcedRoles.find( (role) => condition.roleEntityRef === role.roleEntityRef ); if (!roleMetadata) { this.logger.warn( `skip to add condition for role '${condition.roleEntityRef}'. The role either does not exist or was not created from a CSV file.` ); continue; } if (roleMetadata.source !== "csv-file") { this.logger.warn( `skip to add condition for role '${condition.roleEntityRef}'. Role is not from csv-file` ); continue; } fileDesired.push(condition); } return fileDesired; } async loadStoredConditionsForRoles(csvFileSourcedRoles) { return this.conditionalStorage.filterConditions( csvFileSourcedRoles.map((role) => role.roleEntityRef) ); } async persistConditionUpdate(id, conditionToUpdate, pendingDeleteIds) { const auditorEvent = await this.auditor.createEvent({ eventId: auditor.ConditionEvents.CONDITION_WRITE, severityLevel: "medium", meta: { actionType: auditor.ActionType.UPDATE } }); try { await this.conditionalStorage.updateCondition( id, conditionToUpdate, void 0, pendingDeleteIds ); await auditorEvent.success({ meta: { condition: { ...conditionToUpdate, permissionMapping: conditionToUpdate.permissionMapping.map( (pm) => pm.action ) } } }); } catch (error) { await auditorEvent.fail({ error, meta: { condition: { ...conditionToUpdate, permissionMapping: conditionToUpdate.permissionMapping.map( (pm) => pm.action ) } } }); throw error; } } async persistConditionCreate(conditionToCreate, pendingDeleteIds) { const auditorEvent = await this.auditor.createEvent({ eventId: auditor.ConditionEvents.CONDITION_WRITE, severityLevel: "medium", meta: { actionType: auditor.ActionType.CREATE } }); try { await this.conditionalStorage.createCondition( conditionToCreate, pendingDeleteIds ); await auditorEvent.success({ meta: { condition: { ...conditionToCreate, permissionMapping: conditionToCreate.permissionMapping.map( (pm) => pm.action ) } } }); } catch (error) { await auditorEvent.fail({ error, meta: { condition: { ...conditionToCreate, permissionMapping: conditionToCreate.permissionMapping.map( (pm) => pm.action ) } } }); throw error; } } async persistConditionDelete(condition) { const auditorEvent = await this.auditor.createEvent({ eventId: auditor.ConditionEvents.CONDITION_WRITE, severityLevel: "medium", meta: { actionType: auditor.ActionType.DELETE } }); const deleteMeta = { condition: { ...condition, permissionMapping: condition.permissionMapping.map((pm) => pm.action) } }; try { if (condition.id === void 0) { throw new errors.InputError( `Cannot delete conditional policy without stored id for role '${condition.roleEntityRef}'` ); } await this.conditionalStorage.deleteCondition(condition.id); await auditorEvent.success({ meta: deleteMeta }); } catch (error) { await auditorEvent.fail({ error, meta: deleteMeta }); throw error; } } async cleanUpConditionalPolicies() { const csvFileRoles = await this.roleMetadataStorage.filterRoleMetadata("csv-file"); const existedFileConds = await this.loadStoredConditionsForRoles(csvFileRoles); for (const condition of existedFileConds) { await this.persistConditionDelete(condition); } } } exports.DEFAULT_CONDITIONAL_POLICIES_FILE_LIMITS = DEFAULT_CONDITIONAL_POLICIES_FILE_LIMITS; exports.YamlConditionalPoliciesFileWatcher = YamlConditionalPoliciesFileWatcher; exports.resolveConditionalPoliciesFileLimits = resolveConditionalPoliciesFileLimits; //# sourceMappingURL=yaml-conditional-file-watcher.cjs.js.map