Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions docs/hardis/project/metadata/create-admin-permset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
title: Create Admin Permission Set
description: Generate a Permission Set with full CRUD and Modify All permissions on all custom objects and fields from local source metadata
---

## Description

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:

- **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

## 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/<name>.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
<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">
<label>ObjectRightsModifyAll</label>
<hasActivationRequired>false</hasActivationRequired>
<objectPermissions>
<allowCreate>true</allowCreate>
<allowDelete>true</allowDelete>
<allowEdit>true</allowEdit>
<allowRead>true</allowRead>
<modifyAllRecords>true</modifyAllRecords>
<object>Account__c</object>
<viewAllRecords>true</viewAllRecords>
</objectPermissions>
<fieldPermissions>
<editable>true</editable>
<field>Account__c.Description__c</field>
<readable>true</readable>
</fieldPermissions>
</PermissionSet>
```

## 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
11 changes: 11 additions & 0 deletions messages/create-admin-permset.md
Original file line number Diff line number Diff line change
@@ -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)
175 changes: 175 additions & 0 deletions src/commands/hardis/project/metadata/create-admin-permset.ts
Original file line number Diff line number Diff line change
@@ -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<AnyJson> {
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

<details markdown="1">
<summary>Technical explanations</summary>

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/<name>.permissionset-meta.xml\`

</details>
`;
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<AnyJson> {
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<string, { name: string; editable: boolean }[]> = {};

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 {
// 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;
}
}
Loading