@cloud-copilot/iam-collect
Version:
Collect IAM information from AWS Accounts
326 lines • 15.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileSystemAwsIamStore = void 0;
const iam_utils_1 = require("@cloud-copilot/iam-utils");
const path_1 = require("path");
const json_js_1 = require("../../utils/json.js");
const util_js_1 = require("../util.js");
class FileSystemAwsIamStore {
baseFolder;
separator;
fsAdapter;
constructor(baseFolder, partition, separator, fsAdapter) {
this.baseFolder = baseFolder;
this.separator = separator;
this.baseFolder = (0, path_1.join)(baseFolder, 'aws', partition);
this.fsAdapter = fsAdapter;
}
organizationPath(organizationId) {
return (0, path_1.join)(this.baseFolder, 'organizations', organizationId).toLowerCase();
}
organizationMetadataPath(organizationId, metadataType) {
return (0, path_1.join)(this.organizationPath(organizationId), `${metadataType}.json`).toLowerCase();
}
organizationalUnitsPath(organizationId) {
return (0, path_1.join)(this.organizationPath(organizationId), 'ous').toLowerCase();
}
organizationalUnitPath(organizationId, ouId) {
return (0, path_1.join)(this.organizationalUnitsPath(organizationId), ouId).toLowerCase();
}
organizationPoliciesPath(organizationId, policyType) {
return (0, path_1.join)(this.organizationPath(organizationId), policyType).toLowerCase();
}
organizationPolicyPath(organizationId, policyType, policyId) {
return (0, path_1.join)(this.organizationPoliciesPath(organizationId, policyType), policyId).toLowerCase();
}
organizationPolicyMetadataPath(organizationId, policyType, policyId, metadataType) {
return (0, path_1.join)(this.organizationPolicyPath(organizationId, policyType, policyId), `${metadataType}.json`).toLowerCase();
}
organizationalUnitMetadataPath(organizationId, ouId, metadataType) {
return (0, path_1.join)(this.organizationalUnitPath(organizationId, ouId), `${metadataType}.json`).toLowerCase();
}
accountsPath() {
return (0, path_1.join)(this.baseFolder, 'accounts');
}
accountPath(accountId) {
return (0, path_1.join)(this.accountsPath(), accountId).toLowerCase();
}
accountMetadataPath(accountId, metadataType) {
return (0, path_1.join)(this.accountPath(accountId), `${metadataType}.json`).toLowerCase();
}
buildResourcePath(accountId, arn) {
return (0, util_js_1.resourcePrefix)(this.accountPath(accountId), arn, this.separator).toLowerCase();
}
buildMetadataPath(accountId, arn, metadataType) {
const prefix = this.buildResourcePath(accountId, arn);
return (0, path_1.join)(prefix, `${metadataType}.json`).toLowerCase();
}
/**
* Root RAM folder for a given account.
*/
ramRootPath(accountId) {
return (0, path_1.join)(this.accountPath(accountId), 'ram');
}
/**
* Folder under ramRootPath for a specific region (or 'global').
*/
ramRegionPath(accountId, region) {
// normalize region or use 'global'
const rg = region && region.trim() != '' ? region.toLowerCase() : 'global';
return (0, path_1.join)(this.ramRootPath(accountId), rg);
}
/**
* File name for a given resource ARN: replace ':' and '/' with '-'
*/
ramFileNameForArn(arn) {
return arn.replace(/[:/]/g, '-').toLowerCase() + '.json';
}
/**
* Full path to the RAM policy file for this ARN in region.
*/
ramPolicyFilePath(accountId, region, arn) {
return (0, path_1.join)(this.ramRegionPath(accountId, region), this.ramFileNameForArn(arn));
}
/**
* Get the path to the indexes directory.
*
* @returns The path to the indexes directory.
*/
indexesPath() {
return (0, path_1.join)(this.baseFolder, 'indexes');
}
/**
* The path to the index file for a given index name.
*
* @param indexName the name of the index
* @returns The path to the index file.
*/
indexPath(indexName) {
return (0, path_1.join)(this.indexesPath(), `${indexName}.json`).toLowerCase();
}
async saveResourceMetadata(accountId, arn, metadataType, data) {
const filePath = this.buildMetadataPath(accountId, arn, metadataType);
await this.saveOrDeleteFile(filePath, data);
}
async listResourceMetadata(accountId, arn) {
// List all files in the resource directory to find metadata types
const dirPath = this.buildResourcePath(accountId, arn);
const files = await this.fsAdapter.listDirectory(dirPath);
// Filter for files that match the pattern of *.json
const metadataTypes = files
.filter((file) => file.endsWith('.json'))
.map((file) => file.replace('.json', '')); // Remove the .json extension
return metadataTypes;
}
async getResourceMetadata(accountId, arn, metadataType, defaultValue) {
const filePath = this.buildMetadataPath(accountId, arn, metadataType);
return this.contentOrDefault(filePath, defaultValue);
}
async deleteResourceMetadata(accountId, arn, metadataType) {
const filePath = this.buildMetadataPath(accountId, arn, metadataType);
await this.fsAdapter.deleteFile(filePath);
}
async deleteResource(accountId, arn) {
const dirPath = this.buildResourcePath(accountId, arn);
await this.fsAdapter.deleteDirectory(dirPath);
}
async listResources(accountId, options) {
const dirPath = (0, util_js_1.resourceTypePrefix)(this.accountPath(accountId), { ...options }, this.separator);
return await this.fsAdapter.listDirectory(dirPath);
}
async findResourceMetadata(accountId, options) {
let searchBase = this.accountPath(accountId);
const pathParts = [options.service];
if (options.region) {
pathParts.push(options.region);
}
if (options.resourceType) {
pathParts.push(options.resourceType);
}
pathParts.push('*');
const strings = await this.fsAdapter.findWithPattern(searchBase, pathParts, 'metadata.json');
return strings.map((s) => JSON.parse(s));
}
async syncResourceList(accountId, options, desiredResources) {
const dirPath = (0, util_js_1.resourceTypePrefix)(this.accountPath(accountId), { ...options }, this.separator);
const existingSubDirs = (await this.fsAdapter.listDirectory(dirPath)).map((subDir) => (0, path_1.join)(dirPath, subDir));
let filteredSubDirs = existingSubDirs;
if (options.metadata && Object.keys(options.metadata).length > 0) {
filteredSubDirs = [];
// If metadata is provided, filter existing sub dirs based on metadata
const metadataFilter = (metadataString) => {
if (!metadataString) {
return false;
}
const metadata = JSON.parse(metadataString);
return Object.entries(options.metadata).every(([key, value]) => metadata[key] === value);
};
for (const subDir of existingSubDirs) {
const metadata = await this.fsAdapter.readFile((0, path_1.join)(subDir, `metadata.json`));
if (metadataFilter(metadata)) {
filteredSubDirs.push(subDir);
}
}
}
const desiredDirs = new Set(desiredResources.map((desiredArn) => {
const resourceDir = this.buildResourcePath(accountId, desiredArn);
return resourceDir;
}));
// Identify resources that exist in storage but not in desiredResources.
const resourcesToDelete = filteredSubDirs.filter((s) => !desiredDirs.has(s));
for (const resource of resourcesToDelete) {
await this.fsAdapter.deleteDirectory(resource);
}
}
async deleteAccountMetadata(accountId, metadataType) {
const filePath = this.accountMetadataPath(accountId, metadataType);
await this.fsAdapter.deleteFile(filePath);
}
async saveAccountMetadata(accountId, metadataType, data) {
const filePath = this.accountMetadataPath(accountId, metadataType);
await this.saveOrDeleteFile(filePath, data);
}
async getAccountMetadata(accountId, metadataType, defaultValue) {
const filePath = this.accountMetadataPath(accountId, metadataType);
return this.contentOrDefault(filePath, defaultValue);
}
async getOrganizationMetadata(organizationId, metadataType, defaultValue) {
const filePath = this.organizationMetadataPath(organizationId, metadataType);
return this.contentOrDefault(filePath, defaultValue);
}
async saveOrganizationMetadata(organizationId, metadataType, data) {
const filePath = this.organizationMetadataPath(organizationId, metadataType);
await this.saveOrDeleteFile(filePath, data);
}
async deleteOrganizationMetadata(organizationId, metadataType) {
const filePath = this.organizationMetadataPath(organizationId, metadataType);
await this.fsAdapter.deleteFile(filePath);
}
async listOrganizationalUnits(organizationId) {
const dirPath = this.organizationalUnitsPath(organizationId);
return await this.fsAdapter.listDirectory(dirPath);
}
async deleteOrganizationalUnitMetadata(organizationId, ouId, metadataType) {
const filePath = this.organizationalUnitMetadataPath(organizationId, ouId, metadataType);
await this.fsAdapter.deleteFile(filePath);
}
async saveOrganizationalUnitMetadata(organizationId, ouId, metadataType, data) {
const filePath = this.organizationalUnitMetadataPath(organizationId, ouId, metadataType);
await this.saveOrDeleteFile(filePath, data);
}
async getOrganizationalUnitMetadata(organizationId, ouId, metadataType, defaultValue) {
const filePath = this.organizationalUnitMetadataPath(organizationId, ouId, metadataType);
return this.contentOrDefault(filePath, defaultValue);
}
async deleteOrganizationalUnit(organizationId, ouId) {
const dirPath = this.organizationalUnitPath(organizationId, ouId);
await this.fsAdapter.deleteDirectory(dirPath);
}
async deleteOrganizationPolicyMetadata(organizationId, policyType, policyId, metadataType) {
const filePath = this.organizationPolicyMetadataPath(organizationId, policyType, policyId, metadataType);
await this.fsAdapter.deleteFile(filePath);
}
async saveOrganizationPolicyMetadata(organizationId, policyType, policyId, metadataType, data) {
const filePath = this.organizationPolicyMetadataPath(organizationId, policyType, policyId, metadataType);
await this.saveOrDeleteFile(filePath, data);
}
async getOrganizationPolicyMetadata(organizationId, policyType, policyId, metadataType, defaultValue) {
const filePath = this.organizationPolicyMetadataPath(organizationId, policyType, policyId, metadataType);
return this.contentOrDefault(filePath, defaultValue);
}
async deleteOrganizationPolicy(organizationId, policyType, policyId) {
const dirPath = this.organizationPolicyPath(organizationId, policyType, policyId);
await this.fsAdapter.deleteDirectory(dirPath);
}
async listOrganizationPolicies(organizationId, policyType) {
const dirPath = this.organizationPoliciesPath(organizationId, policyType);
return await this.fsAdapter.listDirectory(dirPath);
}
async syncRamResources(accountId, region, arns) {
const dirPath = this.ramRegionPath(accountId, region);
const files = await this.fsAdapter.listDirectory(dirPath);
const keepSet = new Set(arns.map((a) => this.ramFileNameForArn(a)));
for (const file of files) {
if (!keepSet.has(file.toLowerCase())) {
await this.fsAdapter.deleteFile((0, path_1.join)(dirPath, file));
}
}
}
async saveRamResource(accountId, arn, data) {
const region = (0, iam_utils_1.splitArnParts)(arn).region;
const filePath = this.ramPolicyFilePath(accountId, region, arn);
const content = typeof data === 'string' ? data : (0, json_js_1.consistentStringify)(data);
await this.fsAdapter.writeFile(filePath, content);
}
async getRamResource(accountId, arn, defaultValue) {
const region = (0, iam_utils_1.splitArnParts)(arn).region;
const filePath = this.ramPolicyFilePath(accountId, region, arn);
return this.contentOrDefault(filePath, defaultValue);
}
async listAccountIds() {
return this.fsAdapter.listDirectory(this.accountsPath());
}
async getIndex(indexName, defaultValue) {
const filePath = this.indexPath(indexName);
const contents = await this.fsAdapter.readFileWithHash(filePath);
if (contents) {
return { data: JSON.parse(contents.data), lockId: contents.hash };
}
return { data: defaultValue, lockId: '' };
}
async saveIndex(indexName, data, lockId) {
const filePath = this.indexPath(indexName);
return this.fsAdapter.writeWithOptimisticLock(filePath, (0, json_js_1.consistentStringify)(data), lockId);
}
async writeBatch(fn) {
await fn();
}
/**
* Checks if a given content value is empty.
*
* @param content The content to check.
* @returns true if the content is empty, false otherwise.
*/
isEmptyContent(content) {
return (content === undefined ||
content === null ||
content === '' ||
content === '{}' ||
content === '[]' ||
(Array.isArray(content) && content.length === 0) ||
(typeof content === 'object' && Object.keys(content).length === 0));
}
/**
* Read the content of a file or return a default value if the file does not exist.
*
* @param filePath the path to the file
* @param defaultValue the default value to return if the file does not exist
* @returns the content of the file or the default value
*/
async contentOrDefault(filePath, defaultValue) {
const contents = await this.fsAdapter.readFile(filePath);
if (!contents) {
return defaultValue;
}
return JSON.parse(contents);
}
/**
* Either saves the provided data to a file or deletes the file if the data is empty.
*
* @param filePath the path to the file
* @param data the data to save in the file
*/
async saveOrDeleteFile(filePath, data) {
if (typeof data === 'string') {
data = data.trim();
}
if (this.isEmptyContent(data)) {
await this.fsAdapter.deleteFile(filePath);
return;
}
const content = typeof data === 'string' ? data : (0, json_js_1.consistentStringify)(data);
await this.fsAdapter.writeFile(filePath, content);
}
}
exports.FileSystemAwsIamStore = FileSystemAwsIamStore;
//# sourceMappingURL=FileSystemAwsIamStore.js.map