UNPKG

@cloud-copilot/iam-collect

Version:

Collect IAM information from AWS Accounts

317 lines 14.6 kB
import { splitArnParts } from '@cloud-copilot/iam-utils'; import { join } from 'path'; import { resourcePrefix, resourceTypePrefix } from '../util.js'; import { FileSystemAdapter } from './FileSystemAdapter.js'; export class FileSystemAwsIamStore { constructor(baseFolder, partition, separator, fsAdapter) { this.baseFolder = baseFolder; this.partition = partition; this.separator = separator; this.baseFolder = join(baseFolder, 'aws', partition); this.fsAdapter = fsAdapter || new FileSystemAdapter(); } organizationPath(organizationId) { return join(this.baseFolder, 'organizations', organizationId).toLowerCase(); } organizationMetadataPath(organizationId, metadataType) { return join(this.organizationPath(organizationId), `${metadataType}.json`).toLowerCase(); } organizationalUnitsPath(organizationId) { return join(this.organizationPath(organizationId), 'ous').toLowerCase(); } organizationalUnitPath(organizationId, ouId) { return join(this.organizationalUnitsPath(organizationId), ouId).toLowerCase(); } organizationPoliciesPath(organizationId, policyType) { return join(this.organizationPath(organizationId), policyType).toLowerCase(); } organizationPolicyPath(organizationId, policyType, policyId) { return join(this.organizationPoliciesPath(organizationId, policyType), policyId).toLowerCase(); } organizationPolicyMetadataPath(organizationId, policyType, policyId, metadataType) { return join(this.organizationPolicyPath(organizationId, policyType, policyId), `${metadataType}.json`).toLowerCase(); } organizationalUnitMetadataPath(organizationId, ouId, metadataType) { return join(this.organizationalUnitPath(organizationId, ouId), `${metadataType}.json`).toLowerCase(); } accountsPath() { return join(this.baseFolder, 'accounts'); } accountPath(accountId) { return join(this.accountsPath(), accountId).toLowerCase(); } accountMetadataPath(accountId, metadataType) { return join(this.accountPath(accountId), `${metadataType}.json`).toLowerCase(); } buildResourcePath(accountId, arn) { return resourcePrefix(this.accountPath(accountId), arn, this.separator).toLowerCase(); } buildMetadataPath(accountId, arn, metadataType) { const prefix = this.buildResourcePath(accountId, arn); return join(prefix, `${metadataType}.json`).toLowerCase(); } /** * Root RAM folder for a given account. */ ramRootPath(accountId) { return 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 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 join(this.ramRegionPath(accountId, region), this.ramFileNameForArn(arn)); } /** * Get the path to the indexes directory. * * @returns The path to the indexes directory. */ indexesPath() { return 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 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 = 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 = resourceTypePrefix(this.accountPath(accountId), { ...options }, this.separator); const existingSubDirs = (await this.fsAdapter.listDirectory(dirPath)).map((subDir) => 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(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(join(dirPath, file)); } } } async saveRamResource(accountId, arn, data) { const region = splitArnParts(arn).region; const filePath = this.ramPolicyFilePath(accountId, region, arn); const content = typeof data === 'string' ? data : JSON.stringify(data, null, 2); await this.fsAdapter.writeFile(filePath, content); } async getRamResource(accountId, arn, defaultValue) { const region = 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, JSON.stringify(data, null, 2), lockId); } /** * 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 : JSON.stringify(data, null, 2); await this.fsAdapter.writeFile(filePath, content); } } //# sourceMappingURL=FileSystemAwsIamStore.js.map