From 34e5ade414fac9a3023abf55df5c43cb370d789f Mon Sep 17 00:00:00 2001 From: MGU Date: Sat, 24 Jan 2026 11:47:21 +0100 Subject: [PATCH 1/4] feature/create-admin-permset --- .../project/metadata/create-admin-permset.md | 12 ++ messages/create-admin-permset.md | 11 ++ .../project/metadata/create-admin-permset.ts | 175 ++++++++++++++++++ .../metadata/create-admin-permset.test.ts | 154 +++++++++++++++ 4 files changed, 352 insertions(+) create mode 100644 docs/hardis/project/metadata/create-admin-permset.md create mode 100644 messages/create-admin-permset.md create mode 100644 src/commands/hardis/project/metadata/create-admin-permset.ts create mode 100644 test/commands/hardis/project/metadata/create-admin-permset.test.ts diff --git a/docs/hardis/project/metadata/create-admin-permset.md b/docs/hardis/project/metadata/create-admin-permset.md new file mode 100644 index 0000000000..1d3b6bfee5 --- /dev/null +++ b/docs/hardis/project/metadata/create-admin-permset.md @@ -0,0 +1,12 @@ +--- +title: Create Admin Permission Set +description: Génère et (optionnel) déploie un Permission Set contenant droits sur tous les objets et champs. +--- + +Help: https://sfdx-hardis.cloudity.com/hardis/project/metadata/create-admin-permset/ + +Usage: + +`sf hardis:project:metadata:create-admin-permset` + +This command parses local source metadata and generates a permission set file named `ObjectRightsModifyAll.permissionset-meta.xml` saved to `force-app/main/default/permissionsets/`. diff --git a/messages/create-admin-permset.md b/messages/create-admin-permset.md new file mode 100644 index 0000000000..85256faf09 --- /dev/null +++ b/messages/create-admin-permset.md @@ -0,0 +1,11 @@ +# summary + +Generate an admin Permission Set with full object and field permissions + +# description + +Generates a Permission Set XML file containing full CRUD permissions on all custom objects and field access for all fields found in local source metadata. + +# flags.name.description + +Name of the permission set to generate (also used as the filename) diff --git a/src/commands/hardis/project/metadata/create-admin-permset.ts b/src/commands/hardis/project/metadata/create-admin-permset.ts new file mode 100644 index 0000000000..e7d77c0134 --- /dev/null +++ b/src/commands/hardis/project/metadata/create-admin-permset.ts @@ -0,0 +1,175 @@ +import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; +import { Messages } from '@salesforce/core'; +import { AnyJson } from '@salesforce/ts-types'; +import { uxLog } from '../../../../common/utils/index.js'; +import { parseXmlFile } from '../../../../common/utils/xmlUtils.js'; +import fs from 'fs-extra'; +import path from 'path'; +import xml2js from 'xml2js'; +import c from 'chalk'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('sfdx-hardis', 'create-admin-permset'); + +export default class CreateAdminPermset extends SfCommand { + public static summary = messages.getMessage('summary'); + public static description = ` +## Command Behavior + +**Generates a Permission Set file with full CRUD and Modify All permissions on all custom objects found in the local source.** + +This command scans the \`force-app/main/default/objects\` directory and creates a Permission Set XML file with: + +- **Object Permissions:** Full CRUD access (Create, Read, Update, Delete) plus View All and Modify All on each object +- **Field Permissions:** Read access on all fields, Edit access on editable fields (excludes Formula, AutoNumber, Summary, MasterDetail, and required fields) +- **Automatic Filtering:** Skips Platform Events (\`__e\`), Custom Metadata Types (\`__mdt\`), and PersonAccount + +Key use cases: +- **Admin Access Setup:** Quickly create an admin permission set for development or testing +- **Sandbox Preparation:** Generate baseline permissions for QA environments +- **Permission Set Templates:** Use as a starting point for role-based access + +
+Technical explanations + +The command performs the following operations: + +1. **Directory Scanning:** Reads all subdirectories in \`force-app/main/default/objects\` +2. **Field Parsing:** For each object, parses all \`.field-meta.xml\` files using \`parseXmlFile()\` +3. **Field Classification:** Determines editability based on field type and attributes: + - Non-editable types: Formula, Summary, AutoNumber + - Skipped fields: MasterDetail, required fields, OwnerId, Name +4. **XML Generation:** Uses \`xml2js.Builder\` to create the Permission Set XML structure +5. **File Output:** Writes to \`force-app/main/default/permissionsets/.permissionset-meta.xml\` + +
+`; + public static examples = [ + `$ sf hardis:project:metadata:create-admin-permset`, + `$ sf hardis:project:metadata:create-admin-permset --name MyCustomAdminPS`, + ]; + + public static flags = { + name: Flags.string({ + char: 'n', + description: messages.getMessage('flags.name.description'), + default: 'ObjectRightsModifyAll', + }), + }; + public static requiresProject = true; + + async run(): Promise { + const { flags } = await this.parse(CreateAdminPermset); + const permSetName = flags.name; + uxLog('action', this, c.cyan(`Start creating admin permission set "${permSetName}"...`)); + + const objectsDir = path.join('force-app', 'main', 'default', 'objects'); + const objectList: Record = {}; + + function normalizeXmlValue(v: any) { + if (v == null) return undefined; + if (Array.isArray(v)) return v[0]; + return v; + } + + function isFieldEditable(parsedField: any) { + const nonEditableFieldTypes = ["Formula", "Summary", "AutoNumber"]; + const fieldType = normalizeXmlValue(parsedField?.CustomField?.type) ?? "Unknown"; + const formulaVal = parsedField?.CustomField?.formula ?? parsedField?.CustomField?.formula; + const hasFormula = formulaVal != null && normalizeXmlValue(formulaVal) !== undefined; + return !nonEditableFieldTypes.includes(fieldType) && !hasFormula; + } + + function shouldSkipField(parsedField: any) { + const skipFieldNames = ["OwnerId", "Name"]; + + const type = normalizeXmlValue(parsedField?.CustomField?.type) ?? null; + const required = normalizeXmlValue(parsedField?.CustomField?.required); + const fullName = normalizeXmlValue(parsedField?.CustomField?.fullName) ?? null; + + if (!fullName) return true; // malformed field + + if (type === 'MasterDetail') return true; + if (required === 'true' || required === true) return true; + if (skipFieldNames.includes(fullName)) return true; + + return false; + } + + if (await fs.pathExists(objectsDir)) { + const objects = await fs.readdir(objectsDir); + for (const object of objects) { + if (object.endsWith('__mdt') || object.endsWith('__e')) { + continue; // ignore platform metadata and events objects + } + if (object === 'PersonAccount') { + continue; // ignore PersonAccount synthetic folder + } + const objectPath = path.join(objectsDir, object); + const fieldsPath = path.join(objectPath, 'fields'); + if (!(await fs.pathExists(fieldsPath))) continue; + const fields = await fs.readdir(fieldsPath); + const fieldInfos: { name: string; editable: boolean }[] = []; + for (const f of fields) { + try { + const parsed = await parseXmlFile(path.join(fieldsPath, f)); + const fullNameRaw = parsed?.CustomField?.fullName ?? null; + const fullName = normalizeXmlValue(fullNameRaw) ?? null; + if (!fullName) continue; + if (shouldSkipField(parsed)) continue; + fieldInfos.push({ name: fullName, editable: isFieldEditable(parsed) }); + } catch (e) { + // ignore parse errors per-field + } + } + objectList[object] = fieldInfos; + } + } else { + uxLog('warning', this, c.yellow('No local objects folder found at force-app/main/default/objects')); + } + + const permSet: any = { + PermissionSet: { + $: { xmlns: 'http://soap.sforce.com/2006/04/metadata' }, + label: permSetName, + hasActivationRequired: false, + }, + }; + + permSet.PermissionSet.fieldPermissions = []; + permSet.PermissionSet.objectPermissions = []; + + for (const [objectName, fields] of Object.entries(objectList)) { + permSet.PermissionSet.objectPermissions.push({ + allowCreate: true, + allowDelete: true, + allowEdit: true, + allowRead: true, + modifyAllRecords: true, + object: objectName, + viewAllRecords: true, + }); + for (const fieldInfo of fields) { + const fieldFull = fieldInfo.name; + const editable = !!fieldInfo.editable; + const fieldName = fieldFull.includes('.') ? fieldFull : `${objectName}.${fieldFull}`; + permSet.PermissionSet.fieldPermissions.push({ + editable, + field: fieldName, + readable: true, + }); + } + } + + const builder = new xml2js.Builder({ headless: false, renderOpts: { pretty: true } }); + const xml = builder.buildObject(permSet); + + const outDir = path.join('force-app', 'main', 'default', 'permissionsets'); + await fs.ensureDir(outDir); + const filename = path.join(outDir, `${permSetName}.permissionset-meta.xml`); + await fs.writeFile(filename, xml, 'utf8'); + uxLog('success', this, c.green(`Permission set generated at ${filename}`)); + + return permSet; + } +} diff --git a/test/commands/hardis/project/metadata/create-admin-permset.test.ts b/test/commands/hardis/project/metadata/create-admin-permset.test.ts new file mode 100644 index 0000000000..a0bd2ba6b4 --- /dev/null +++ b/test/commands/hardis/project/metadata/create-admin-permset.test.ts @@ -0,0 +1,154 @@ +import { TestContext } from '@salesforce/core/testSetup'; +import { expect } from 'chai'; +import { stubSfCommandUx } from '@salesforce/sf-plugins-core'; +import fs from 'fs-extra'; +import os from 'os'; +import path from 'path'; +import CreateAdminPermset from '../../../../../src/commands/hardis/project/metadata/create-admin-permset.js'; + +describe('hardis:project:metadata:create-admin-permset', () => { + const $$ = new TestContext(); + let tmpDir: string; + let originalCwd: string; + + beforeEach(async () => { + stubSfCommandUx($$.SANDBOX); + originalCwd = process.cwd(); + tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'create-admin-permset-')); + + // Setup minimal sfdx project + await fs.writeJson(path.join(tmpDir, 'sfdx-project.json'), { + packageDirectories: [{ path: 'force-app', default: true }], + }); + }); + + afterEach(async () => { + $$.restore(); + process.chdir(originalCwd); + await fs.remove(tmpDir); + }); + + it('generates permission set with default name', async () => { + // Setup test object with field + const objectDir = path.join(tmpDir, 'force-app/main/default/objects/TestObject__c/fields'); + await fs.ensureDir(objectDir); + await fs.writeFile( + path.join(objectDir, 'TestField__c.field-meta.xml'), + ` + + TestField__c + Text + 255 +` + ); + + process.chdir(tmpDir); + const result = await CreateAdminPermset.run([]); + + // Verify file created + const outFile = path.join(tmpDir, 'force-app/main/default/permissionsets/ObjectRightsModifyAll.permissionset-meta.xml'); + expect(await fs.pathExists(outFile)).to.be.true; + + // Verify structure + expect(result).to.have.property('PermissionSet'); + expect((result as any).PermissionSet.objectPermissions).to.have.lengthOf(1); + expect((result as any).PermissionSet.objectPermissions[0].object).to.equal('TestObject__c'); + }); + + it('uses custom name from --name flag', async () => { + const objectDir = path.join(tmpDir, 'force-app/main/default/objects/Account/fields'); + await fs.ensureDir(objectDir); + + process.chdir(tmpDir); + await CreateAdminPermset.run(['--name', 'CustomAdminPS']); + + const outFile = path.join(tmpDir, 'force-app/main/default/permissionsets/CustomAdminPS.permissionset-meta.xml'); + expect(await fs.pathExists(outFile)).to.be.true; + }); + + it('skips platform events and custom metadata types', async () => { + await fs.ensureDir(path.join(tmpDir, 'force-app/main/default/objects/MyEvent__e/fields')); + await fs.ensureDir(path.join(tmpDir, 'force-app/main/default/objects/MySetting__mdt/fields')); + await fs.ensureDir(path.join(tmpDir, 'force-app/main/default/objects/ValidObject__c/fields')); + await fs.writeFile( + path.join(tmpDir, 'force-app/main/default/objects/ValidObject__c/fields/Field__c.field-meta.xml'), + ` + + Field__c + Text + 255 +` + ); + + process.chdir(tmpDir); + const result = await CreateAdminPermset.run([]); + + const objectNames = (result as any).PermissionSet.objectPermissions.map((o: any) => o.object); + expect(objectNames).to.include('ValidObject__c'); + expect(objectNames).to.not.include('MyEvent__e'); + expect(objectNames).to.not.include('MySetting__mdt'); + }); + + it('marks formula fields as non-editable', async () => { + const objectDir = path.join(tmpDir, 'force-app/main/default/objects/TestObj__c/fields'); + await fs.ensureDir(objectDir); + await fs.writeFile( + path.join(objectDir, 'FormulaField__c.field-meta.xml'), + ` + + FormulaField__c + Text + Name & " Test" +` + ); + + process.chdir(tmpDir); + const result = await CreateAdminPermset.run([]); + + const formulaField = (result as any).PermissionSet.fieldPermissions.find( + (f: any) => f.field === 'TestObj__c.FormulaField__c' + ); + expect(formulaField.readable).to.be.true; + expect(formulaField.editable).to.be.false; + }); + + it('skips MasterDetail fields', async () => { + const objectDir = path.join(tmpDir, 'force-app/main/default/objects/ChildObj__c/fields'); + await fs.ensureDir(objectDir); + await fs.writeFile( + path.join(objectDir, 'ParentRef__c.field-meta.xml'), + ` + + ParentRef__c + MasterDetail + ParentObj__c +` + ); + await fs.writeFile( + path.join(objectDir, 'RegularField__c.field-meta.xml'), + ` + + RegularField__c + Text + 255 +` + ); + + process.chdir(tmpDir); + const result = await CreateAdminPermset.run([]); + + const fieldNames = (result as any).PermissionSet.fieldPermissions.map((f: any) => f.field); + expect(fieldNames).to.include('ChildObj__c.RegularField__c'); + expect(fieldNames).to.not.include('ChildObj__c.ParentRef__c'); + }); + + it('handles missing objects directory gracefully', async () => { + // No objects directory created + process.chdir(tmpDir); + const result = await CreateAdminPermset.run([]); + + expect(result).to.have.property('PermissionSet'); + expect((result as any).PermissionSet.objectPermissions).to.have.lengthOf(0); + expect((result as any).PermissionSet.fieldPermissions).to.have.lengthOf(0); + }); +}); From b261ce62a54a0da747252959c8f1ae47a44ab848 Mon Sep 17 00:00:00 2001 From: MGU Date: Sat, 24 Jan 2026 11:58:31 +0100 Subject: [PATCH 2/4] feat(docs): enhance create-admin-permset documentation with detailed descriptions and examples --- .../project/metadata/create-admin-permset.md | 113 +++++++++++++++++- 1 file changed, 108 insertions(+), 5 deletions(-) diff --git a/docs/hardis/project/metadata/create-admin-permset.md b/docs/hardis/project/metadata/create-admin-permset.md index 1d3b6bfee5..c77d67dd57 100644 --- a/docs/hardis/project/metadata/create-admin-permset.md +++ b/docs/hardis/project/metadata/create-admin-permset.md @@ -1,12 +1,115 @@ --- title: Create Admin Permission Set -description: Génère et (optionnel) déploie un Permission Set contenant droits sur tous les objets et champs. +description: Generate a Permission Set with full CRUD and Modify All permissions on all custom objects and fields from local source metadata --- -Help: https://sfdx-hardis.cloudity.com/hardis/project/metadata/create-admin-permset/ +## Description -Usage: +This command scans your local Salesforce source metadata and generates a comprehensive Permission Set XML file with maximum permissions on all custom objects and their fields. +It is particularly useful for: -`sf hardis:project:metadata:create-admin-permset` +- **Development & Testing:** Quickly create an admin-level permission set for scratch orgs or sandboxes +- **Data Migration:** Grant full access to data loader users during migration projects +- **Troubleshooting:** Eliminate permission-related issues when debugging by granting full access +- **Permission Set Templates:** Use the generated file as a starting point for creating role-specific permission sets -This command parses local source metadata and generates a permission set file named `ObjectRightsModifyAll.permissionset-meta.xml` saved to `force-app/main/default/permissionsets/`. +## Usage + +```shell +# Generate with default name (ObjectRightsModifyAll) +sf hardis:project:metadata:create-admin-permset + +# Generate with custom name +sf hardis:project:metadata:create-admin-permset --name MyAdminPermSet +``` + +## Parameters + +| Name | Type | Description | Default | +| --------------- | ------ | -------------------------------------------------------- | ----------------------- | +| `--name` / `-n` | string | Name of the permission set (used for label and filename) | `ObjectRightsModifyAll` | + +## Output + +The command generates a Permission Set XML file at: + +``` +force-app/main/default/permissionsets/.permissionset-meta.xml +``` + +## What's Included + +### Object Permissions + +For each custom object found in `force-app/main/default/objects/`, the permission set grants: + +| Permission | Value | +| ---------------- | ------ | +| allowCreate | `true` | +| allowRead | `true` | +| allowEdit | `true` | +| allowDelete | `true` | +| viewAllRecords | `true` | +| modifyAllRecords | `true` | + +### Field Permissions + +For each field on the objects: + +| Permission | Value | +| ---------- | --------------------------------------- | +| readable | `true` | +| editable | `true` (unless non-editable field type) | + +## What's Excluded + +The command intelligently filters out metadata that should not be included: + +### Excluded Objects + +| Object Type | Reason | +| ------------------------------- | ------------------------------------------------- | +| Platform Events (`__e`) | Cannot have CRUD permissions | +| Custom Metadata Types (`__mdt`) | Managed separately, no record-level access | +| PersonAccount | Synthetic object, permissions managed via Account | + +### Excluded Fields + +| Field Type | Reason | +| -------------------------- | ------------------------------------ | +| Formula fields | Read-only by definition | +| AutoNumber fields | System-generated, read-only | +| Roll-Up Summary fields | Calculated, read-only | +| MasterDetail relationships | Controlled by parent record | +| Required fields | Already accessible by default | +| OwnerId | System field | +| Name | Standard field with special handling | + +## Example Output + +```xml + + + + false + + true + true + true + true + true + Account__c + true + + + true + Account__c.Description__c + true + + +``` + +## See Also + +- [Salesforce Permission Sets Documentation](https://help.salesforce.com/s/articleView?id=sf.perm_sets_overview.htm) +- [hardis:project:convert:profilestopermsets](../convert/profilestopermsets.md) - Convert profiles to permission sets From cb71a2a565a16212ed9d7a2579793113b24b0c8a Mon Sep 17 00:00:00 2001 From: MGU Date: Sat, 24 Jan 2026 11:59:26 +0100 Subject: [PATCH 3/4] fix: simplify error handling in create-admin-permset command --- src/commands/hardis/project/metadata/create-admin-permset.ts | 2 +- .../hardis/project/metadata/create-admin-permset.test.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commands/hardis/project/metadata/create-admin-permset.ts b/src/commands/hardis/project/metadata/create-admin-permset.ts index e7d77c0134..60928fc852 100644 --- a/src/commands/hardis/project/metadata/create-admin-permset.ts +++ b/src/commands/hardis/project/metadata/create-admin-permset.ts @@ -118,7 +118,7 @@ The command performs the following operations: if (!fullName) continue; if (shouldSkipField(parsed)) continue; fieldInfos.push({ name: fullName, editable: isFieldEditable(parsed) }); - } catch (e) { + } catch { // ignore parse errors per-field } } diff --git a/test/commands/hardis/project/metadata/create-admin-permset.test.ts b/test/commands/hardis/project/metadata/create-admin-permset.test.ts index a0bd2ba6b4..4c468eec70 100644 --- a/test/commands/hardis/project/metadata/create-admin-permset.test.ts +++ b/test/commands/hardis/project/metadata/create-admin-permset.test.ts @@ -6,6 +6,8 @@ import os from 'os'; import path from 'path'; import CreateAdminPermset from '../../../../../src/commands/hardis/project/metadata/create-admin-permset.js'; +/* eslint-disable @typescript-eslint/no-unused-expressions */ + describe('hardis:project:metadata:create-admin-permset', () => { const $$ = new TestContext(); let tmpDir: string; From 8a64bcb5afca9313f118fe0a185bf2b573bf6606 Mon Sep 17 00:00:00 2001 From: maximeg44 <14998050+maximeg44@users.noreply.github.com> Date: Sat, 24 Jan 2026 11:04:07 +0000 Subject: [PATCH 4/4] [Mega-Linter] Apply linters fixes --- docs/hardis/project/metadata/create-admin-permset.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/hardis/project/metadata/create-admin-permset.md b/docs/hardis/project/metadata/create-admin-permset.md index c77d67dd57..f0ff844204 100644 --- a/docs/hardis/project/metadata/create-admin-permset.md +++ b/docs/hardis/project/metadata/create-admin-permset.md @@ -26,7 +26,7 @@ sf hardis:project:metadata:create-admin-permset --name MyAdminPermSet ## Parameters | Name | Type | Description | Default | -| --------------- | ------ | -------------------------------------------------------- | ----------------------- | +|-----------------|--------|----------------------------------------------------------|-------------------------| | `--name` / `-n` | string | Name of the permission set (used for label and filename) | `ObjectRightsModifyAll` | ## Output @@ -44,7 +44,7 @@ force-app/main/default/permissionsets/.permissionset-meta.xml For each custom object found in `force-app/main/default/objects/`, the permission set grants: | Permission | Value | -| ---------------- | ------ | +|------------------|--------| | allowCreate | `true` | | allowRead | `true` | | allowEdit | `true` | @@ -57,7 +57,7 @@ For each custom object found in `force-app/main/default/objects/`, the permissio For each field on the objects: | Permission | Value | -| ---------- | --------------------------------------- | +|------------|-----------------------------------------| | readable | `true` | | editable | `true` (unless non-editable field type) | @@ -68,7 +68,7 @@ The command intelligently filters out metadata that should not be included: ### Excluded Objects | Object Type | Reason | -| ------------------------------- | ------------------------------------------------- | +|---------------------------------|---------------------------------------------------| | Platform Events (`__e`) | Cannot have CRUD permissions | | Custom Metadata Types (`__mdt`) | Managed separately, no record-level access | | PersonAccount | Synthetic object, permissions managed via Account | @@ -76,7 +76,7 @@ The command intelligently filters out metadata that should not be included: ### Excluded Fields | Field Type | Reason | -| -------------------------- | ------------------------------------ | +|----------------------------|--------------------------------------| | Formula fields | Read-only by definition | | AutoNumber fields | System-generated, read-only | | Roll-Up Summary fields | Calculated, read-only |