6

Working source first, permission sets and profiles have a low "signal to noise" ratio where the signal is the content of the XML elements and the noise is the element layout and the elements themselves. And many of us have multiple permission sets to manage that each can be easily 10,000+ lines long each as they include hundreds of fields, dozens of objects and dozens of classes.

I've found dropping line breaks helpful, so instead of the conventional formatting:

<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">

    <classAccesses>
        <apexClass>TransactionController</editable>
        <enabled>true</field>
    </classAccesses>
    ... many more

    <fieldPermissions>
        <editable>true</editable>
        <field>Transaction__c.Amount__c</field>
        <readable>true</readable>
    </fieldPermissions>
    <fieldPermissions>
        <editable>true</editable>
        <field>Transaction__c.Date__c</field>
        <readable>true</readable>
    </fieldPermissions>
    ... many more

    <objectPermissions>
        <allowCreate>true</allowCreate>
        <allowDelete>true</allowDelete>
        <allowEdit>true</allowEdit>
        <allowRead>true</allowRead>
        <object>Transaction__c</object>
    </objectPermissions>
    ... many more
    
</PermissionSet>

using a line per first child element makes reconciling Git diffs easier and is also a little easier for humans to scan:

<?xml version="1.0" encoding="UTF-8"?>
<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">

    <classAccesses><apexClass>TransactionController</editable><enabled>true</field></classAccesses>
    ... many more

    <fieldPermissions><editable>true</editable><field>Transaction__c.Amount__c</field><readable>true</readable></fieldPermissions>
    <fieldPermissions><editable>true</editable><field>Transaction__c.Date__c</field><readable>true</readable></fieldPermissions>
    ... many more

    <objectPermissions><allowCreate>true</allowCreate><allowDelete>true</allowDelete><allowEdit>true</allowEdit><allowRead>true</allowRead><object>Transaction__c</object></objectPermissions>
    ... many more

</PermissionSet>

but the long lines are a drawback (with the line length dominated by the XML tags).

So a better signal to noise ratio would be a format like this (inspired by the Robot Framework Data-driven style):

PermissionSet

    classAccesses
        apexClass              enabled
        ---------              -------
        TransactionController  true
        ... many more

    fieldPermissions
        field                      editable  readable
        -----                      --------  --------
        Transaction__c.Amount__c   true      true
        Transaction__c.Date__c     true      true
        ... many more

    objectPermissions
        object          allowCreate  allowDelete  allowEdit  allowRead
        ------          -----------  -----------  ---------  ---------
        Transaction__c  true         true         true       true
        ... many more

that can be transformed to/from the XML format when required.

Is anyone doing this? Pros/cons?

Or is there a good XML <-> some-other-format tool you would recommend that handles repeated structures well i.e. does not repeat the element names?

PS

Consider voting for this idea flagged in the comments SFDX Breakdown Profile and PermissionSet Metadata Into Subdirectories where the approach is to break up the permission sets and profiles into many files to help with versioning.

9
  • Could whoever just voted to close this add a comment explaining why they think it should be closed? Many of us have multiple permission sets that are 10,000+ lines long and effectively managing that data is a challenge. So if there are better approaches out there I'd like to leverage those.
    – Keith C
    Mar 31, 2023 at 9:47
  • Upvoted the question. This is a pain point for me as well.
    – Rohit
    Mar 31, 2023 at 9:58
  • 1
    Doesn't answer your question "that handles repeated structures well i.e. does not repeat the element names?" but I find YAML much easier to read when looking at diffs, although it can be confusing too but there's much less noise given no open/close tags etc. Mar 31, 2023 at 10:27
  • 1
    @KeithC: I have also found this tool that could be used for inspiration. github.com/lodossDev/sfdx-profiles-splitter Mar 31, 2023 at 15:45
  • 1
    Yeah @JefersonChaves, I can see the merits of the file splitting approach for merges.
    – Keith C
    Mar 31, 2023 at 15:55

3 Answers 3

2

XML is verbose. This is why we have YML, JSON, and other lightweight formats. In 2014, I created a workbook in Excel that had macros to generate the relevant XML before deployment. This means I'd have, for example, an Admin.xlsx that the macro would then generate an Admin.profile-meta.xml file based on the contents. Each type of permission (e.g. apexClasses) would be its own tab, with one tab for all the general permissions. This worked out well for what I needed, with the caveat that xlsx doesn't play nice with Git, being an inherently binary file type.

Based on that experience, plus the current experience of sfdx objects like objects, with nested "magic folders" to represent the metadata, I'd be tempted to offer a similar solution. A small NodeJS could read and parse a file system and spit out an XML file that you can use for deployments. This format would require effectively zero space on disk, and would very efficiently compress in Git.

Here's how I'd probably lay out this kind of framework.

profiles/
  Admin/
    classAccesses/
      TransactionController/
        enabled/
          true
    fieldPermissions/
      Transaction__c.Amount__c/
        readable/
          true
        editable/
          true
    objectPermissions/
      Transaction__c/
        allowCreate/
          true
        allowDelete/
          true
        allowEdit/
          true
        allowRead/
          true

Each / above is a folder, and the remainder are empty files zero bytes long. This format would be directly translatable to and from XML with minimal rules about transformation. Each top-level concept (e.g. classAccesses) would understand that the first level is the primary key (e.g. for classAccesses, apexClass would be the property), and all of the other properties simply defined as files.

I feel this could probably be an efficient design, since it would compress very efficiently, and diffs would never have merge conflicts lower than at the file level. Even better, this would probably only take a few hundred lines of JavaScript to create conversion tools that run both ways, to and from this format, and you wouldn't need to store the bulky XML bits. Also, file systems don't allocate extra blocks for 0 byte files, so this entire design should store efficiently on disk as well.

We'd lose the ability to edit all the permissions at once in the file system (but again, we can convert to XML, edit, then back again), but gain the ability to script bulk updates, like updating field level security for all profiles for a given field might be something like:

for profile in */ ; do
  rm $profile/fieldPermissions/Transaction__c.Amount__c/editable/true
  touch $profile/fieldPermissions/Transaction__c.Amount__c/editable/false
done

And so on.

6
  • A clever approach. My only concern is hitting issues with case-insensitive file systems.
    – Phil W
    Mar 31, 2023 at 13:23
  • (Also, need to use a portable build system that has explicit access to file system.)
    – Phil W
    Mar 31, 2023 at 13:59
  • That is very clever indeed, separating sections into folders would be similar to how Objects are already stored in source-format. Mar 31, 2023 at 14:05
  • @PhilW I'm not sure any hardware still running an OS that has non-case-preserving file systems would be a pleasant developer experience, lol. Besides, I'd probably have a pre-commit hook to run the tools to verify the format and/or generate the XML files automatically.
    – sfdcfox
    Mar 31, 2023 at 14:37
  • If you make a case mistake in Windows folder or file names (and IIRC MacOS - maybe only old versions since it's a long time since I used MacOS) it's a (minor) pain to fix; you have to rename to some intermediate name then back to the case correct version. I'm not sure how well git then behaves (probably fine, but I'd need to test it). Adding staging hooks in git may help, if that's something you can do in your setup...
    – Phil W
    Mar 31, 2023 at 14:51
2

I feel that YAML could help. Similar to Phil W's solution, you could transform it into something like using existing converters (e.g. this online tool for quick-d-dirty try) or worst come worse go for XLST (on this example I moved the tag field to be first before converting.

PermissionSet:
  classAccesses:
    apexClass: TransactionController
    enabled: true
  fieldPermissions:
    - field: Transaction__c.Amount__c
      editable: true
      readable: true
    - field: Transaction__c.Date__c
      editable: true
      readable: true
  objectPermissions:
    object: Transaction__c
    allowCreate: true
    allowDelete: true
    allowEdit: true
    allowRead: true

The drawback with this is that you are moving from translating from one format to another, so, you may face limitations. While Phil's solution keeps under the same format but with a potentially better semantic.

2

Personally, I think any format that relies purely on indentation is a very bad idea. It is too easy to get indentation systemically wrong and thus I would never use the "robot framework" approach.

Use of your own XML format makes a lot of sense to me (not that I'm doing this) because it should be easy to apply XSLT to transform it to the final structure you want. Recall that you could use attributes to represent the editable/readable/allowCreate/... type options, making this compact. You also don't really care what the value is, just that the attribute exists.

Also note that you should minimise unnecessary detail - for example, why have an "enabled" flag for a class? Just listing the class can mean it is enabled. Also, why repeat the object to which a field belongs, instead of structuring the XML to have that as a hierarchy relationship?

I think I'd go with something like:

<PermissionSet label="..." license="...">
  <ApexClasses>
    <ApexClass name="TransactionController"/>
    ...
  </ApexClasses>
  <Objects>
    <Object name="Transaction__c" create="x" delete="x" edit="x">
      <Field name="Amount__c" edit="x"/>
      <Field name="Date__c" edit="x"/>
      ...
    </Object>
    ...
  </Objects>
</PermissionSet>

Points to note:

  • There's no need to set read if you have create or edit for an object.
  • There's no need to set read if you have edit for a field.
  • There's no point repeating object information against separate fields - embed the fields in the object detail.
  • Using attributes can make it so you have compact, self-close element representation, saving some extra verbiage.
  • It would be very sensible to add an XML Schema for this so you can use a validating XML editor that is schema aware and can help you do auto-completion too.

It would, of course, also be an option to consider "Objects" and (what was) "Object" as supporting nested children with arbitrary names, to avoid repeating "Object", "Field", and similarly avoiding repeating "ApexClass" within "ApexClasses". The element names would be the Object/Field/Class name:

<PermissionSet name="..." license="...">
  <ApexClasses>
    <TransactionController/>
    ...
  </ApexClasses>
  <Objects>
    <Transaction__c create="x" delete="x" edit="x">
      <Amount__c edit="x"/>
      <Date__c edit="x"/>
      ...
    </Transaction__c>
    ...
  </Objects>
</PermissionSet>

I think you can generally get away with this since field names have restricted structure that is mostly compatible with the XML element name restrictions (not sure if a namespace prefix can start with a number - and if you have "xml" as the first few letters, this will break).

You might get away with this same approach for ApexClass names too since element names can apparently contain "." characters.

These caveats could be worked around by allowing a prefixing "_" (SObject and class names cannot start with "_"). This would need to be stripped off during transformation.

Here's an XLST that handles just the Apex classes, object and field permissions in this compact format (plus the label for the permission set):

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns="http://soap.sforce.com/2006/04/metadata">
    <xsl:output method="xml" indent="yes"/>

    <xsl:template match="PermissionSet">
        <xsl:text>&#xA;</xsl:text>
        <PermissionSet>
            <label>
                <xsl:value-of select="@label"/>
            </label>
            <xsl:apply-templates select="ApexClasses"/>
            <xsl:apply-templates select="Objects"/>
        </PermissionSet>
    </xsl:template>

    <!-- Match the ApexClasses element and convert the child elements to classAccesses -->
    <xsl:template match="ApexClasses">
        <xsl:for-each select="*">
            <classAccesses>
                <apexClass>
                    <xsl:choose>
                        <xsl:when test="starts-with(local-name(), '_')">
                            <xsl:value-of select="substring-after(local-name(), '_')"/>
                        </xsl:when>
                        <xsl:otherwise>
                            <xsl:value-of select="local-name()"/>
                        </xsl:otherwise>
                    </xsl:choose>
                </apexClass>
                <enabled>true</enabled>
            </classAccesses>
        </xsl:for-each>
    </xsl:template>

    <!-- Match the Objects element and convert the child elements to objectPermissions -->
    <xsl:template match="Objects">
        <xsl:for-each select="*">
            <objectPermissions>
                <xsl:if test="@create = 'x'">
                    <allowCreate>true</allowCreate>
                </xsl:if>
                <xsl:if test="@delete = 'x'">
                    <allowDelete>true</allowDelete>
                </xsl:if>
                <xsl:if test="@edit = 'x'">
                    <allowEdit>true</allowEdit>
                </xsl:if>
                <xsl:if test="@read = 'x' or @create = 'x' or @delete = 'x' or @edit = 'x'">
                    <allowRead>true</allowRead>
                </xsl:if>
                <object>
                    <xsl:choose>
                        <xsl:when test="starts-with(local-name(), '_')">
                            <xsl:value-of select="substring-after(local-name(), '_')"/>
                        </xsl:when>
                        <xsl:otherwise>
                            <xsl:value-of select="local-name()"/>
                        </xsl:otherwise>
                    </xsl:choose>
                </object>
            </objectPermissions>

            <!-- Process the child elements as field permission definitions -->
            <xsl:apply-templates select="*" mode="fields">
                <xsl:with-param name="object">
                    <xsl:choose>
                        <xsl:when test="starts-with(local-name(), '_')">
                            <xsl:value-of select="substring-after(local-name(), '_')"/>
                        </xsl:when>
                        <xsl:otherwise>
                            <xsl:value-of select="local-name()"/>
                        </xsl:otherwise>
                    </xsl:choose>
                </xsl:with-param>
            </xsl:apply-templates>
        </xsl:for-each>
    </xsl:template>

    <!-- Match the child elements of an Object element and convert them to fieldPermissions -->
    <xsl:template match="*" mode="fields">
        <xsl:param name="object"/>

        <fieldPermissions>
            <field><xsl:value-of select="$object"/>.<xsl:choose>
                    <xsl:when test="starts-with(local-name(), '_')">
                        <xsl:value-of select="substring-after(local-name(), '_')"/>
                    </xsl:when>
                    <xsl:otherwise>
                        <xsl:value-of select="local-name()"/>
                    </xsl:otherwise>
                </xsl:choose>
            </field>
            <xsl:if test="@edit = 'x'"><editable>true</editable></xsl:if>
            <xsl:if test="@read = 'x' or @edit = 'x'"><readable>true</readable></xsl:if>
        </fieldPermissions>
    </xsl:template>

</xsl:stylesheet>
2
  • This is getting there <Amount__c edit="x"/>. Thanks!
    – Keith C
    Mar 31, 2023 at 10:33
  • 1
    @KeithC I did a further update to add an even more compact format, with a couple of caveats.
    – Phil W
    Mar 31, 2023 at 10:34

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .