Python and Selenium for a no-permissions Salesforce profile
Tags: salesforce, tutorial, python, selenium
Published: 2019-08-23 21:03:50

---
title: Python and Selenium for a no-permissions Salesforce profile
description:  The Metadata API was just too darned tricky
published: true
tags: salesforce,tutorial,python,selenium
cover_image: https://katiekodes.com/images/header-selenium-salesforce-settings-20190823.png
canonical_url: https://katiekodes.com/salesforce-permissions-python-selenium/
---

I needed a fresh "no permissions" Salesforce profile in a mature org.

I couldn't get the Metadata API working fast enough to my liking, so I gave up and built a "robot version" of myself with Python and Selenium, hijacked my own browser, and watched fake-me click thousands of buttons at computer speed.

_(Does that make my computer itself the #AwesomeAdmin?)_

Here's how I did it.

## Background

I'm about to test a brand new [Pardot training environment](http://help.pardot.com/customer/portal/articles/2128159-training-environment-faq) I was issued, and I want to connect it to a Salesforce sandbox under a philosophy of [least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege).

That means creating a Salesforce Profile with _no_ object-level CRUD access and _no_ field-level Read/Write access whatsoever.

I'll flip things back _on_ if I need them.

Unfortunately, we don't have any such profiles to clone.

* I cloned the closest one I could find -- a "read only" one -- but it had accumulated a lot of read/edit _(yes, edit, despite its name)_ access it didn't need.
* To get it to the "no permissions" status I desired, I had a **lot** of edits to do.

Read on to see what I tried.

---

## Take 1:  Welkin Suite

I thought that The Welkin Suite's [Objects Permissions Editor](https://welkinsuite.com/blog/salesforce-objects-permissions-editor-in-the-welkin-suite-bloom-r25/) and [Field Level Security](https://docs.welkinsuite.com/?id=windows:how_does_it_work:declarative_development:sobjects_editor:fls_tab) tools could help me.

* Indeed, the Objects Permissions Editor made quick work of shutting off object-level CRUD access.
* However, it was too many clicks, and too much waiting, to pull up hundreds of objects one at a time in the Field Level Security tab of Welkin and shut things off object-by-object.
	* The editor is great for dropping the hammer and zeroing out permissions for a profile, or several, on 5 objects.  However, it was still too much work for 150 objects.

---

## Take 2:  Metadata API

I thought:

> "Welkin, as well as the Salesforce setup page authors, have both built tools that query Salesforce all security-settable fields for an object so as to present them to users.
> 
> "I'll just query whatever they're querying and loop over it, building a humongous `Pardot Data Sync.profile` in [XML](https://katiekodes.com/tags#xml), ZIP it up, and [deploy it to my sandbox in Workbench](https://workbench.developerforce.com/metadataDeploy.php)."

The contents of `Pardot Data Sync.profile` would look like this:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<Profile xmlns="http://soap.sforce.com/2006/04/metadata">
    <fieldPermissions>
        <allowCreate>false</allowCreate>
        <editable>false</editable>
        <field>SomeObjectAPIName.SomeFieldAPIName</field>
        <hidden>true</hidden>
        <readable>false</readable>
    </fieldPermissions>
	...
    <fieldPermissions>
        <allowCreate>false</allowCreate>
        <editable>false</editable>
        <field>SomeObjectAPIName.SomeFieldAPIName</field>
        <hidden>true</hidden>
        <readable>false</readable>
    </fieldPermissions>
</Profile>
```

That file would be inside a folder within the .ZIP called "profiles."

At the top level of the .ZIP would be a file called `package.xml` with the following contents:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<Package xmlns="http://soap.sforce.com/2006/04/metadata">
    <fullName>MiscLazyStuff</fullName>
    <types>
        <members>Pardot Data Sync</members>
        <name>Profile</name>
    </types>
    <version>46.0</version>
</Package>
```

I didn't know how to get a list of `SomeObjectAPIName.SomeFieldAPIName` values to build my `<fieldPermissions>...</fieldPermissions>` wrappers around, so I started by writing a Python script to loop over every `*.object` XML file I had downloaded to my computer with Welkin.

Unfortunately, I ran into no end of **metadata deployment** errors -- each of which I could only see one of at a time.

1. First I had to edit my script to ignore formula fields
2. Then I had to edit it to ignore Master-Detail fields
3. Then I had to edit it to ignore mandatory lookup fields.  A `required` property doesn't exist on standard fields of standard objects in the `.object` files, so to be safe, I just ignored all Lookup fields that didn't have a `required` flag either way.\
4. And, of course, I had to ignore fields with a `required` property of `true`.
5. But still, there were more standard fields of standard objects that were required without telling me so in their `*.object` XML definitions.<br/>I ran out of patience when the error messages told me that `AuthorizationFormConsent.ConsentCapturedDateTime` was required but its code in `AuthorizationFormConsent.object` looks like this:

```xml
<fields>
	<fullName>ConsentCapturedDateTime</fullName>
	<trackHistory>false</trackHistory>
</fields>
```

> Wait ... how was I supposed to know that about `ConsentCapturedDateTime`?!
> 
> _**Where is this documented / extractable??!!**_**

I spent another 3 hours banging my head against the wall playing with the Tooling API and Apex, trying to find a reliable way to get the same kind of "don't bother with this field" blacklist _(or a "do bother with these fields" whitelist)_ that the Welkin Suite and the authors of Salesforce's web-based Setup pages have obviously managed to come up with before I quit.

I mean, was I supposed to screen-scrape [documentation](https://developer.salesforce.com/docs/atlas.en-us.220.0.api.meta/api/sforce_api_objects_authorizationformconsent.htm) for the word "required" at the beginning of a "Description" paragraph or something?

At a certain point, I was **_done_**.

> **NOTE**:  _Will take input!_
> 
> Welkin folks, Salesforce employees, or other developers -- I'd love to hear by what algorithms you **figure out _exactly which_ fields to present to users** as permission-editable.

Until then ... instead of building my own car, I decided to "There I Fixed It" the wheel of a preexisting car.  Leading me to ...

![](https://i.chzbgr.com/full/3215767040/hA7AE392D/)

---

## Take 3:  Setup menu + Selenium _(and a bit of Data Loader)_

[Selenium](https://en.wikipedia.org/wiki/Selenium_%28software%29) is a programmable suite of software that acts as middleware between your web browser and a programming language of your choice.

Typically, developers use it to imitate a user clicking buttons on a web page so that they can ensure that their web page works as intended.

* Computerization saves hassle -- who really wants to click all the buttons that need to be clicked when it's the same thing every time?
* Computerization scales well -- this is a reliable way to simulate millions of humans clicking your web page at the same time _(like if you're a giant e-retailer during major shopping holiday seasons)_.

I installed it because I knew I needed to "un-click" somewhere between **several** and **hundreds of** checkboxes per screen on dozens to hundreds of Salesforce Setup pages that were all predicably like each other.

Sounds like a perfect use case for automating my work!

I could explore what my web browser was doing on my behalf as I clicked the first few buttons, then teach the computer to do it for me, and teach the computer what the subtle variations were to account for when switching from one Salesforce "object" to another.

### Step 1:  Log In

First, I ran the following Python code

```python
from selenium.webdriver import Chrome
browser = Chrome(executable_path='C:\\exampleprograms\\ChromeDriver\\chromedriver.exe')
browser.get('https://test.salesforce.com')
```

This caused a special instance of Chrome to pop up on my computer.

I logged into Salesforce with my username and password as usual.

### Step 2:  Get My Object Links

Then I commented out the code above and, without letting [my Python IDE](https://katiekodes.com/setup-python-windows-anaconda/#write-your-first-python-program) clear out the value of the `browser` variable _(which is normal behavior of my IDE from one "run" button click to the next)_, I ran the following code:

```python
editableLinksToVisit = []
browser.get('https://cs99.salesforce.com/00eX00000000XXX?s=ObjectsAndTabs')
objtable = browser.find_element_by_css_selector("[id$='table_objects_and_tabs:tb']")
rows = objtable.find_elements_by_xpath('.//*')
for row in rows:
    tds = row.find_elements_by_tag_name("td")
    if len(tds) > 2:
        td1 = tds[0]
        td2 = tds[1]
        td2span = td2.find_element_by_tag_name("span")
        if td2span is not None and td2span.text != '--':
            td1span = td1.find_element_by_tag_name("span")
            if td1span is not None:
                td1a = td1span.find_element_by_tag_name("a")
                if td1a is not None:
                    td1aText = td1a.text
                    td1aLink = td1a.get_attribute('href')
                    td1aEditableLink = td1aLink.replace('?s=ObjectsAndTabs','/e?s=ObjectsAndTabs')
                    editableLinksToVisit.append({'text':td1aText, 'editLink':td1aEditableLink})
```

Here's what the code above does:

1. The `browser.get()` command made me visit a new web site in Chrome -- as if I myself had typed that URL into my browser bar and hit "enter."
	* **It's pretty trippy** -- you literally watch your computer do things without you, like someone else has taken over control.
2. The URL I told `browser.get()` about is the URL of the "objects and tabs" screen of setup for the profile I was trying to edit _(which I've written here as having an object ID of `00eX00000000XXX`)_.
3. When I run `browser.get()`, the contents stored in my program's `browser` variable change.
4. Then I use various [methods](https://en.wikipedia.org/wiki/Method_(computer_programming)) built into the [data type](https://en.wikipedia.org/wiki/Data_type) I've stored in `browser` _(I forget exactly what it is)_ such as `.find_element_by_css_selector()` and `.find_element_by_tag_name()` to [scrape](https://en.wikipedia.org/wiki/Web_scraping) the contents of this particular web page.
	* Technically, I'm scraping the DOM, or the thing you see when you use the Firefox or Chrome developer console "inspect element" tool, not scraping the HTML source.  Their contents are ever-so-slightly different.  It used to be that you could tell what HTML was making up a page just by looking at its source.  These days, JavaScript can "inject" HTML into the "DOM" at the last minute, and your browser _actually_ renders a web page based on the DOM, so you have to know how to check what's really in the DOM if you want to parse the code behind a web page.
5. My big `for` loop and nest of `if` statements looks for links in a specific table on the page that are **_not_** next to the text "--."
	* "--" would indicate that a link goes to settings for a Tab, not an Object, so I want to skip those.
6. Then I grab the name of the object _(per the link's text)_ and its URL.
7. Finally, I edit the link by sticking an `/e` right before the `?s=` so I can go straight to the editable version of that object's profile-specific configuration page when I visit my link.

My big list of object names and their links is what I store as `editableLinksToVisit`.

### Step 3:  Click all the buttons

I again commented out the code above and, without letting my Python IDE clear out the value any variables _(which is normal behavior of my IDE from one "run" button click to the next)_, grabbed a cup of water and ran the following code _(it took about **15-25 minutes**)_:

```python
for linkDict in editableLinksToVisit:
    print(linkDict.get('text'))
    browser.get(linkDict.get('editLink'))
    
    submitbutton = browser.find_element_by_css_selector("input[type='submit'][value='Save'][id$='button_pc_save']")
    editinputs = browser.find_elements_by_css_selector("input:enabled:checked[id$=':fls_edit_ck']")
    readinputs = browser.find_elements_by_css_selector("input:enabled:checked[id$=':fls_read_ck']")
    
    if submitbutton is not None and (len(editinputs) + len(readinputs) > 0):
        print('submit ' + str(submitbutton is not None))
        print('edits ' + str(len(editinputs)))
        print('reads ' + str(len(readinputs)))
        for ipt in editinputs:
            ipt.send_keys(" ")
        for ipt in readinputs:
            ipt.send_keys(" ")
        submitbutton.send_keys("\n")
        print('done')
    else:
        print('oops')
        print('submit ' + str(submitbutton is not None))
        print('edits ' + str(len(editinputs)))
        print('reads ' + str(len(readinputs)))
```

This code is a big loop over all the description-URL link pairs _([tuples](https://www.w3schools.com/python/python_tuples.asp))_ I'd saved into `editableLinksToVisit`.

Here's what it does for each link in the list:

1. It writes down a note I can read telling me what object it's supposed to be about to hijack my browser to visit the URL of
2. It hijacks my browser and visits that URL.
3. It reads the code of the page's DOM to find all of the **field-level security** boxes that are currently checked and that are un-checkable.
	* It does this into separate "edit" and "read" checkbox lists.
4. It reads the code of the page's DOM to find the Save button.
4. If it found anything that "could use un-checking," it loops over the lists and un-checks the boxes.
	* I loop over "edits" first, then "reads," since un-checking "read" checkboxes can have a side effect of un-checking "edit" sideboxes and I wanted to keep things simple and not worry about side effects.
	* I use an imitation of hitting the spacebar after "tabbing" to a checkbox with the keyboard, rather than Selenium's `.click()` command, because `.click()` wasn't working reliably and someone on StackOverflow said this might and they were right. ðŸ¤·
5. It clicks the Save button _(again, I simulate this by virtually hitting "enter" on the keyboard rather than by clicking because for some reason that worked more reliably)_.

It's a bit mesmerizing to watch _(note -- animation below)_:

![](https://katiekodes.com/images/screenvid-selenium-uncheck.gif)

For a few minutes, anyway.

**Pro tip:**  Don't start it running when you're already staying late at the office and really just want to shut down your computer and enjoy a beautiful evening. ðŸŒ…

### Step 4:  Clean up with Data Loader

**CampaignMember** and a few other objects have editable permissions but didn't show up as editable through the web panel, so I found them by extracting this query with the Salesforce [Data Loader](https://login.salesforce.com/ui/setup/sforce/DataLoaderSetupPage?setupid=DataLoader):

```sql
SELECT Id, Field, SObjectType, PermissionsRead, PermissionsEdit
FROM FieldPermissions 
WHERE parentId IN ( SELECT id 
                    FROM permissionset 
                    WHERE PermissionSet.Profile.Name = 'Pardot Data Sync')
AND (PermissionsRead = true or PermissionsEdit = true)
```

I used Excel to flip "PermissionsRead" and "PermissionsEdit" to FALSE all the way down both columns.

Then, in two passes -- again, doing "edit" first and "read" second _(that is, I mapped "Id" both times but only mapped one column I wanted to edit at a time)_, with my settings to load just 1 record at a time _(so a single error wouldn't shut down a batch)_, I passed this spreadsheet into Update operations in Data Loader.

That also took a few minutes _(again, remember, 1 record at a time)_, but eventually it went through.

---

## Not A Production Solution

This is not sustainable as a long-term solution to creating a "zero" profile.

Loading through the Metadata API is so much more **kosher** and can probably be done all in one step, instead of having to muck around with the Data Loader for "cleanup."

Plus, I'm not even really sure I _did_ get every field-level security setting that I could've turned off.

I'd like to be more thorough in the future and truly bulk-edit permissions the way I'm **supposed to**.

* Q:  Anyone have any tips for building a _perfect_ list of objects and fields to write out permissions for into a `.profile` file?