From 2500dd44b50c3aac16f48aed1895e802dcc6ca5d Mon Sep 17 00:00:00 2001 From: Fabien Taillon Date: Fri, 4 Oct 2019 12:07:47 +0200 Subject: [PATCH] Added texei:profile:clean command --- README.md | 49 ++++++- messages/profile-clean.json | 3 + src/commands/texei/profile/clean.ts | 190 ++++++++++++++++++++++++++++ 3 files changed, 235 insertions(+), 7 deletions(-) create mode 100644 messages/profile-clean.json create mode 100644 src/commands/texei/profile/clean.ts diff --git a/README.md b/README.md index 1618e20..c22a9ae 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Link the plugin: sfdx plugins:link . * [`sfdx texei:data:export -o -d [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-texeidataexport--o-string--d-string--u-string---apiversion-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) * [`sfdx texei:data:import -d [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-texeidataimport--d-string--u-string---apiversion-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) * [`sfdx texei:package:dependencies:install [-k ] [-b ] [-p ] [-n ] [-w ] [-r] [-v ] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-texeipackagedependenciesinstall--k-string--b-string--p-string--n-string--w-number--r--v-string--u-string---apiversion-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) +* [`sfdx texei:profile:clean [-k ] [-p ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-texeiprofileclean--k-string--p-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) * [`sfdx texei:skinnyprofile:retrieve [-v ] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-texeiskinnyprofileretrieve--v-string--u-string---apiversion-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) * [`sfdx texei:source:customlabel:replace -l -v [-p ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-texeisourcecustomlabelreplace--l-string--v-string--p-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) * [`sfdx texei:source:layouts:cleanorg [-p ] [-v ] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-texeisourcelayoutscleanorg--p-string--v-string--u-string---apiversion-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) @@ -60,7 +61,7 @@ EXAMPLE Data exported! ``` -_See code: [src/commands/texei/data/export.ts](https://github.com/texei/texei-sfdx-plugin/blob/v1.0.0/src/commands/texei/data/export.ts)_ +_See code: [src/commands/texei/data/export.ts](https://github.com/texei/texei-sfdx-plugin/blob/v1.1.0/src/commands/texei/data/export.ts)_ ## `sfdx texei:data:import -d [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]` @@ -91,7 +92,7 @@ EXAMPLE Data imported! ``` -_See code: [src/commands/texei/data/import.ts](https://github.com/texei/texei-sfdx-plugin/blob/v1.0.0/src/commands/texei/data/import.ts)_ +_See code: [src/commands/texei/data/import.ts](https://github.com/texei/texei-sfdx-plugin/blob/v1.1.0/src/commands/texei/data/import.ts)_ ## `sfdx texei:package:dependencies:install [-k ] [-b ] [-p ] [-n ] [-w ] [-r] [-v ] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]` @@ -144,7 +145,41 @@ EXAMPLE $ texei:package:dependencies:install -u MyScratchOrg -v MyDevHub -k "1:MyPackage1Key 2: 3:MyPackage3Key" -b "DEV" ``` -_See code: [src/commands/texei/package/dependencies/install.ts](https://github.com/texei/texei-sfdx-plugin/blob/v1.0.0/src/commands/texei/package/dependencies/install.ts)_ +_See code: [src/commands/texei/package/dependencies/install.ts](https://github.com/texei/texei-sfdx-plugin/blob/v1.1.0/src/commands/texei/package/dependencies/install.ts)_ + +## `sfdx texei:profile:clean [-k ] [-p ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]` + +Clean Profile by removing permissions stored on Permission Set + +``` +USAGE + $ sfdx texei:profile:clean [-k ] [-p ] [--json] [--loglevel + trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL] + +OPTIONS + -k, --keep=keep comma-separated list of profile node + permissions that need to be kept. + Default: + layoutAssignments,loginHours,loginIp + Ranges,custom,userLicense + + -p, --path=path comma-separated list of profiles, or + path to profiles folder. Default: + default package directory + + --json format output as json + + --loglevel=(trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL) [default: warn] logging level for + this command invocation + +EXAMPLES + $ texei:profile:clean -k layoutAssignments,recordTypeVisibilities + $ texei:profile:clean -p custom-sfdx-source-folder/main/profiles + $ texei:profile:clean -p + custom-sfdx-source-folder/main/profiles,source-folder-2/main/profiles/myAdmin.profile-meta.xml +``` + +_See code: [src/commands/texei/profile/clean.ts](https://github.com/texei/texei-sfdx-plugin/blob/v1.1.0/src/commands/texei/profile/clean.ts)_ ## `sfdx texei:skinnyprofile:retrieve [-v ] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]` @@ -174,7 +209,7 @@ EXAMPLE $ texei:skinnyprofile:retrieve -u MyScratchOrg ``` -_See code: [src/commands/texei/skinnyprofile/retrieve.ts](https://github.com/texei/texei-sfdx-plugin/blob/v1.0.0/src/commands/texei/skinnyprofile/retrieve.ts)_ +_See code: [src/commands/texei/skinnyprofile/retrieve.ts](https://github.com/texei/texei-sfdx-plugin/blob/v1.1.0/src/commands/texei/skinnyprofile/retrieve.ts)_ ## `sfdx texei:source:customlabel:replace -l -v [-p ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]` @@ -198,7 +233,7 @@ EXAMPLE $ texei:source:customlabel:replace --label GreatSalesforceBlog --value https://blog.texei.com ``` -_See code: [src/commands/texei/source/customlabel/replace.ts](https://github.com/texei/texei-sfdx-plugin/blob/v1.0.0/src/commands/texei/source/customlabel/replace.ts)_ +_See code: [src/commands/texei/source/customlabel/replace.ts](https://github.com/texei/texei-sfdx-plugin/blob/v1.1.0/src/commands/texei/source/customlabel/replace.ts)_ ## `sfdx texei:source:layouts:cleanorg [-p ] [-v ] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]` @@ -231,7 +266,7 @@ EXAMPLES $ texei:source:layouts:cleanorg --targetusername myScratchOrg --targetdevhubusername myDevHub ``` -_See code: [src/commands/texei/source/layouts/cleanorg.ts](https://github.com/texei/texei-sfdx-plugin/blob/v1.0.0/src/commands/texei/source/layouts/cleanorg.ts)_ +_See code: [src/commands/texei/source/layouts/cleanorg.ts](https://github.com/texei/texei-sfdx-plugin/blob/v1.1.0/src/commands/texei/source/layouts/cleanorg.ts)_ ## `sfdx texei:user:update [-v ] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]` @@ -263,5 +298,5 @@ EXAMPLES $ sfdx texei:user:update --values "UserPermissionsKnowledgeUser=true --json" ``` -_See code: [src/commands/texei/user/update.ts](https://github.com/texei/texei-sfdx-plugin/blob/v1.0.0/src/commands/texei/user/update.ts)_ +_See code: [src/commands/texei/user/update.ts](https://github.com/texei/texei-sfdx-plugin/blob/v1.1.0/src/commands/texei/user/update.ts)_ diff --git a/messages/profile-clean.json b/messages/profile-clean.json new file mode 100644 index 0000000..33d808d --- /dev/null +++ b/messages/profile-clean.json @@ -0,0 +1,3 @@ +{ + "commandDescription": "Clean Profile by removing permissions stored on Permission Set" +} \ No newline at end of file diff --git a/src/commands/texei/profile/clean.ts b/src/commands/texei/profile/clean.ts new file mode 100644 index 0000000..2683c6e --- /dev/null +++ b/src/commands/texei/profile/clean.ts @@ -0,0 +1,190 @@ +import { flags, SfdxCommand } from '@salesforce/command'; +import { JsonArray, JsonMap } from '@salesforce/ts-types'; +import { Messages, SfdxProjectJson, SfdxError } from '@salesforce/core'; +import * as fs from 'fs'; +import * as path from 'path'; + +const util = require('util'); +const xml2js = require('xml2js'); + +// Initialize Messages with the current plugin directory +Messages.importMessagesDirectory(__dirname); + +// Load the specific messages for this file. Messages from @salesforce/command, @salesforce/core, +// or any library that is using the messages framework can also be loaded this way. +const messages = Messages.loadMessages('texei-sfdx-plugin', 'profile-clean'); + +const defaultProfileFolder = 'force-app/main/default/profiles'; + +export default class Clean extends SfdxCommand { + + public static description = messages.getMessage('commandDescription'); + + public static examples = [ + '$ texei:profile:clean -k layoutAssignments,recordTypeVisibilities', + '$ texei:profile:clean -p custom-sfdx-source-folder/main/profiles', + '$ texei:profile:clean -p custom-sfdx-source-folder/main/profiles,source-folder-2/main/profiles/myAdmin.profile-meta.xml' + ]; + + protected static flagsConfig = { + keep: flags.string({ char: 'k', required: false, description: 'comma-separated list of profile node permissions that need to be kept. Default: layoutAssignments,loginHours,loginIpRanges,custom,userLicense' }), + path: flags.string({ char: 'p', required: false, description: 'comma-separated list of profiles, or path to profiles folder. Default: default package directory' }) + }; + + // Comment this out if your command does not require an org username + protected static requiresUsername = false; + + // Comment this out if your command does not require a hub org username + protected static requiresDevhubUsername = false; + + // Set this to true if your command requires a project workspace; 'requiresProject' is false by default + protected static requiresProject = false; + + public async run(): Promise { + + let cleanResult = []; + + // TODO: Keep default recordTypeVisibilities & applicationVisibilities like in skinnyprofile:retrieve + const defaultKeep = ['layoutAssignments','loginHours','loginIpRanges','custom','userLicense']; + const nodesToKeep = this.flags.keep ? this.flags.keep : defaultKeep; + let profilesToClean = []; + + // Get profiles files path + if (this.flags.path) { + + // If path was provided as a flag use it/them + const paths = this.flags.path.split(','); + + for (const currentPath of paths) { + + if (currentPath.endsWith('.profile-meta.xml')) { + // Well, this should be a profile + // Otherwise you have a weird folder naming convention, you should probably stop this + profilesToClean.push(currentPath); + } + else { + // Flag provided value doesn't end like a Profile source metadata + // Expect it's a folder + profilesToClean = await this.getProfilesInPath(currentPath); + } + } + } + else { + // Else look in the default package directory + const defaultPackageDirectory = await this.getDefaultPath(); + profilesToClean = await this.getProfilesInPath(defaultPackageDirectory); + } + + if (profilesToClean.length == 0) { + this.ux.log('No Profile found :('); + } + + // Promisify functions + const readFile = util.promisify(fs.readFile); + + for (const profilePath of profilesToClean) { + + // Generate path + const filePath = path.join( + process.cwd(), + profilePath + ); + + // Read data file + const data = await readFile(filePath, 'utf8'); + + // Parsing file + // According to xml2js doc it's better to recreate a parser for each file + // https://www.npmjs.com/package/xml2js#user-content-parsing-multiple-files + var parser = new xml2js.Parser(); + const parseString = util.promisify(parser.parseString); + const profileJson = await parseString(data); + + // Removing unwanted nodes + for (const nodeKey in profileJson.Profile) { + if (profileJson.Profile.hasOwnProperty(nodeKey)) { + if (!nodesToKeep.includes(nodeKey)) { + delete profileJson.Profile[nodeKey]; + } + } + } + + // Building back as an xml + const builder = new xml2js.Builder(); + var xmlFile = builder.buildObject(profileJson); + + // Writing back to file + await fs.writeFile(filePath, xmlFile, 'utf8', function (err) { + if (err) { + throw new SfdxError(`Unable to write Products file at path ${filePath}: ${err}`); + } + }); + + this.ux.log(`Profile cleaned: ${profilePath}`); + cleanResult.push(profilePath); + } + + return { profilesCleaned: cleanResult }; + } + + private async getProfilesInPath(pathToRead: string) { + let profilesInPath = []; + + const readDirectory = util.promisify(fs.readdir); + const filesInDir = await readDirectory(pathToRead); + + for (const fileInDir of filesInDir) { + + const dirOrFilePath = path.join( + process.cwd(), + pathToRead, + fileInDir + ); + + // If it's a Profile file, add it + if (!fs.lstatSync(dirOrFilePath).isDirectory() && fileInDir.endsWith('.profile-meta.xml')) { + + const profileFoundPath = path.join( + pathToRead, + fileInDir + ); + + profilesInPath.push(profileFoundPath); + } + } + + return profilesInPath; + } + + // should probably be in a util class + private async getDefaultPath() { + + // Look for a default package directory + const options = SfdxProjectJson.getDefaultOptions(); + const project = await SfdxProjectJson.create(options); + const packageDirectories = project.get('packageDirectories') as JsonArray || []; + + let foundPath; + for (let packageDirectory of packageDirectories) { + packageDirectory = packageDirectory as JsonMap; + + if (packageDirectory.path && packageDirectory.default) { + + foundPath = path.join( + packageDirectory.path as string, + 'main', + 'default', + 'profiles' + ); + break; + } + + // If no default package directory is found, use the vanilla default DX folder + if (!foundPath) { + foundPath = defaultProfileFolder; + } + } + + return foundPath + } +} \ No newline at end of file