@backstage-community/plugin-rbac-backend
Version:
333 lines (327 loc) • 11.2 kB
JavaScript
'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