All files / src/context/yaml index.ts

92.22% Statements 83/90
81.81% Branches 45/55
100% Functions 14/14
92.94% Lines 79/85

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 2061x 1x 1x 1x             1x 1x 1x 1x   1x 1x   1x                   96x 96x 96x 96x 96x     96x   96x                 96x 96x   96x         20x 20x   20x   20x               43x 43x     43x 43x 43x 43x                 1x 1x       42x 95x 95x   93x           42x 93x   93x   89x             42x 42x     41x 1189x 1189x 1189x 1188x 1188x     1x 1x             5x 5x 5x 5x       5x   5x 1x 1x   1x   1x             4x                     5x     145x 145x     118x 118x 118x 118x 118x 118x 118x                         5x     4x     4x 4x       4x 4x 4x      
import fs from 'fs-extra';
import yaml from 'js-yaml';
import path from 'path';
import {
  loadFileAndReplaceKeywords,
  keywordReplace,
  wrapArrayReplaceMarkersInQuotes,
  Auth0,
} from '../../tools';
 
import log from '../../logger';
import { isFile, toConfigFn, stripIdentifiers, formatResults, recordsSorter } from '../../utils';
import handlers, { YAMLHandler } from './handlers';
import cleanAssets from '../../readonly';
import { Assets, Config, Auth0APIClient, AssetTypes, KeywordMappings } from '../../types';
import { filterOnlyIncludedResourceTypes } from '..';
import { preserveKeywords } from '../../keywordPreservation';
 
export default class YAMLContext {
  basePath: string;
  configFile: string;
  config: Config;
  mappings: KeywordMappings;
  mgmtClient: Auth0APIClient;
  assets: Assets;
  disableKeywordReplacement: boolean;
 
  constructor(config: Config, mgmtClient) {
    this.configFile = config.AUTH0_INPUT_FILE;
    this.config = config;
    this.mappings = config.AUTH0_KEYWORD_REPLACE_MAPPINGS || {};
    this.mgmtClient = mgmtClient;
    this.disableKeywordReplacement = false;
 
    //@ts-ignore because the assets property gets filled out throughout
    this.assets = {};
    // Get excluded rules
    this.assets.exclude = {
      rules: config.AUTH0_EXCLUDED_RULES || [],
      clients: config.AUTH0_EXCLUDED_CLIENTS || [],
      databases: config.AUTH0_EXCLUDED_DATABASES || [],
      connections: config.AUTH0_EXCLUDED_CONNECTIONS || [],
      resourceServers: config.AUTH0_EXCLUDED_RESOURCE_SERVERS || [],
      defaults: config.AUTH0_EXCLUDED_DEFAULTS || [],
    };
 
    this.basePath = (() => {
      Iif (!!config.AUTH0_BASE_PATH) return config.AUTH0_BASE_PATH;
      //@ts-ignore because this looks to be a bug, but do not want to introduce regression; more investigation needed
      return typeof configFile === 'object' ? process.cwd() : path.dirname(this.configFile);
    })();
  }
 
  loadFile(f) {
    let toLoad = path.join(this.basePath, f);
    Eif (!isFile(toLoad)) {
      // try load not relative to yaml file
      toLoad = f;
    }
    return loadFileAndReplaceKeywords(path.resolve(toLoad), {
      mappings: this.mappings,
      disableKeywordReplacement: this.disableKeywordReplacement,
    });
  }
 
  async loadAssetsFromLocal(opts = { disableKeywordReplacement: false }) {
    // Allow to send object/json directly
    this.disableKeywordReplacement = opts.disableKeywordReplacement;
    Iif (typeof this.configFile === 'object') {
      this.assets = this.configFile;
    } else {
      try {
        const fPath = path.resolve(this.configFile);
        log.debug(`Loading YAML from ${fPath}`);
        Object.assign(
          this.assets,
          yaml.load(
            opts.disableKeywordReplacement
              ? wrapArrayReplaceMarkersInQuotes(fs.readFileSync(fPath, 'utf8'), this.mappings)
              : keywordReplace(fs.readFileSync(fPath, 'utf8'), this.mappings)
          ) || {}
        );
      } catch (err) {
        log.debug(err.stack);
        throw new Error(`Problem loading ${this.configFile}\n${err}`);
      }
    }
 
    this.assets = Object.keys(this.assets).reduce((acc: Assets, key: AssetTypes) => {
      const excludedAssetTypes = this.config.AUTH0_EXCLUDED || [];
      if (excludedAssetTypes.includes(key)) return acc;
 
      return {
        ...acc,
        [key]: this.assets[key],
      };
    }, {});
 
    this.assets = Object.keys(this.assets).reduce((acc: Assets, key: AssetTypes) => {
      const includedAssetTypes = this.config.AUTH0_INCLUDED_ONLY;
 
      if (includedAssetTypes !== undefined && !includedAssetTypes.includes(key)) return acc;
 
      return {
        ...acc,
        [key]: this.assets[key],
      };
    }, {});
 
    // Run initial schema check to ensure valid YAML
    const auth0 = new Auth0(this.mgmtClient, this.assets, toConfigFn(this.config));
    if (!opts.disableKeywordReplacement) await auth0.validate(); //The schema validation needs to be disabled during keyword-preserved export because a field may be enforced as an array but will be expressed with an array replace marker (string).
 
    // Allow handlers to process the assets such as loading files etc
    await Promise.all(
      Object.entries(handlers).map(async ([name, handler]) => {
        try {
          const parsed = await handler.parse(this);
          Object.entries(parsed).forEach(([k, v]) => {
            this.assets[k] = v;
          });
        } catch (err) {
          log.debug(err.stack);
          throw new Error(`Problem deploying ${name}, ${err}`);
        }
      })
    );
  }
 
  async dump(): Promise<void> {
    const auth0 = new Auth0(this.mgmtClient, this.assets, toConfigFn(this.config));
    log.info('Loading Auth0 Tenant Data');
    try {
      await auth0.loadAssetsFromAuth0();
 
      const shouldPreserveKeywords =
        //@ts-ignore because the string=>boolean conversion may not have happened if passed-in as env var
        this.config.AUTH0_PRESERVE_KEYWORDS === 'true' ||
        this.config.AUTH0_PRESERVE_KEYWORDS === true;
      if (shouldPreserveKeywords) {
        await this.loadAssetsFromLocal({ disableKeywordReplacement: true }); //Need to disable keyword replacement to retrieve the raw keyword markers (ex: ##KEYWORD##)
        const localAssets = { ...this.assets };
        //@ts-ignore
        delete this['assets'];
 
        this.assets = preserveKeywords({
          localAssets,
          remoteAssets: auth0.assets,
          keywordMappings: this.config.AUTH0_KEYWORD_REPLACE_MAPPINGS || {},
          auth0Handlers: auth0.handlers,
        });
      } else {
        this.assets = auth0.assets;
      }
    } catch (err) {
      const docUrl =
        'https://auth0.com/docs/deploy/deploy-cli-tool/create-and-configure-the-deploy-cli-application#modify-deploy-cli-application-scopes';
      const extraMessage = err.message.startsWith('Insufficient scope')
        ? `\nSee ${docUrl} for more information`
        : '';
      throw new Error(`Problem loading tenant data from Auth0 ${err}${extraMessage}`);
    }
 
    await Promise.all(
      Object.entries(handlers)
        .filter(([handlerName]: [AssetTypes, YAMLHandler<any>]) => {
          const excludedAssetTypes = this.config.AUTH0_EXCLUDED || [];
          return !excludedAssetTypes.includes(handlerName);
        })
        .filter(filterOnlyIncludedResourceTypes(this.config.AUTH0_INCLUDED_ONLY))
        .map(async ([name, handler]) => {
          try {
            const data = await handler.dump(this);
            Eif (data) {
              Eif (data[name] !== null) log.info(`Exporting ${name}`);
              Object.entries(data).forEach(([k, v]) => {
                this.assets[k] = Array.isArray(v)
                  ? v.map(formatResults).sort(recordsSorter)
                  : formatResults(v);
              });
            }
          } catch (err) {
            log.debug(err.stack);
            throw new Error(`Problem exporting ${name}`);
          }
        })
    );
 
    // Clean known read only fields
    let cleaned = cleanAssets(this.assets, this.config);
 
    // Delete exclude as it's not part of the auth0 tenant config
    delete cleaned.exclude;
 
    // Optionally Strip identifiers
    Eif (!this.config.AUTH0_EXPORT_IDENTIFIERS) {
      cleaned = stripIdentifiers(auth0, cleaned);
    }
 
    // Write YAML File
    const raw = yaml.dump(cleaned);
    log.info(`Writing ${this.configFile}`);
    fs.writeFileSync(this.configFile, raw);
  }
}