diff --git a/PUBLISHING.md b/PUBLISHING.md
index 5f37527..51e14a2 100644
--- a/PUBLISHING.md
+++ b/PUBLISHING.md
@@ -4,7 +4,7 @@
- Ensure the version in `manifest.json` and `package.json` is updated.
## Chrome
-1. Build production version by running `npm run build` in `src` folder.
+1. Build production version by running `npm run prod` in `src` folder.
2. Copy `report.html`, `icons`, `background.js`, `content-script.js`, `manifest.json` and (optionally) `build.js.map` from the `src` folder into the new `dist` folder.
3. Side load the `dist` folder into Chrome and test it works correctly.
4. Zip up all the files inside the `dist` folder and name it `salesforce-user-perm-report-VERSION-chrome.zip`
@@ -12,7 +12,7 @@
## Edge
-1. Build production version by running `npm run build` in `src` folder.
+1. Build production version by running `npm run prod` in `src` folder.
2. Copy `report.html`, `icons`, `background.js`, `content-script.js`, `manifest.json` and (optionally) `build.js.map` from the `src` folder into the new `dist` folder.
3. Add the following line to `manifest.json`
```json
@@ -20,20 +20,4 @@
```
4. Side load the `dist` folder into Edge and test it works correctly.
5. Zip up all the files inside the `dist` folder and name it `salesforce-user-perm-report-VERSION-edge.zip`
-6. Upload new version to Microsoft Partner Center
-
-## Opera (incl OperaGX)
-
-1. Build production version by running `npm run build` in `src` folder.
-2. Copy `report.html`, `icons`, `background.js`, `content-script.js`, `manifest.json` and (optionally) `build.js.map` from the `src` folder into the new `dist` folder.
-3. Replace `minimum_chrome_version` with the following in `manifest.json`
-```json
- "minimum_opera_version": "74",
-```
-4. Side load the `dist` folder into Opera and OperaGX and test it works correctly.
-5. Zip up all the files inside the `dist` folder and name it `salesforce-user-perm-report-VERSION-opera.zip`
-6. Upload new version to TODO
-
-## Firefox
-
-At the time of writing, Firefox has not yet implemented support for manifest v3 and therefore this extension is not compatible with Firefox.
\ No newline at end of file
+6. Upload new version to Microsoft Partner Center
\ No newline at end of file
diff --git a/README.md b/README.md
index 017f562..0ea0e0d 100644
--- a/README.md
+++ b/README.md
@@ -16,9 +16,8 @@ Features
Roadmap
- ✅ Edge support
- - 🔄 Opera support
- - 🔄 Firefox support
- - 🔄 Various QoL & UI improvements
+ - ✅ Various QoL & UI improvements
+ - 🔄 Firefox support (likely next year when Firefox adds manifest v3 support)
- Modifying & saving permissions
- Toggle between labels and full names
- Toggle showing only granted permissions
@@ -87,9 +86,9 @@ Navigate to any user detail record in Classic or Lightning and click the "Open P
-**Q**: Is this extension available on Edge/Firefox/Safari?
+**Q**: Is this extension available on Firefox/Safari?
-**A**: I plan on making the extension available on the latest Edge and Firefox versions. I do not have a Mac so I'm not planning for Safari support.
+**A**: I plan on making the extension available on Firefox. I do not have a Mac so I'm not planning for Safari support.
## Support
diff --git a/src/front-end/App.vue b/src/front-end/App.vue
index fde9c60..282bf78 100644
--- a/src/front-end/App.vue
+++ b/src/front-end/App.vue
@@ -1,55 +1,60 @@
-
-
- {{ page.alert }}
-
-
-
-
-
- Open user
-
-
-
-
- {{ page.state === 'loading' ? page.progress : user.name }}
-
-
-
-
-
-
+
+
+
+
+ Your session has timed out. Login to Salesforce and refresh.
+ {{ alertMessageLookup[page.fault] }}
+
+
+
+
+
+
+ Open user
-
-
+
+
+
+ {{ page.state === 'loading' ? page.progress : user.name }}
-
-
-
-
- {{ tableOptions.managed ? 'Hide' : 'Show' }} managed metadata
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ tableOptions.managed ? 'Hide' : 'Show' }} managed metadata
+
+
+
+
+
+
+
+
+
@@ -69,16 +74,22 @@
},
data() {
return {
+ alertMessageLookup: {
+ 'supr:MISSING_SERVER_HOST': 'Missing server host, please launch the report from a user record.',
+ 'supr:MISSING_USER_ID': 'Missing user ID, please launch the report from a user record.',
+ 'supr:MISSING_PERMS': 'Missing system permissions to generate report, make sure you have at least the Download AppExchange Packages permission. This is required to query the managed packages you have installed to support toggling between unmanaged and managed metadata.',
+ 'supr:FAILED_MERGE': 'Failed to merge, see console for details.'
+ },
page: {
state: 'loading',
progress: '',
- alert: ''
+ fault: ''
},
serverHost: '',
+ openUserInLex: false,
user: {
id: '',
- name: '',
- usesLex: true
+ name: ''
},
tableOptions: {
search: '',
@@ -93,6 +104,9 @@
canRefreshReport: function() {
return this.page.state === 'ready';
},
+ canToggleManagedMetadata: function() {
+ return this.canRefreshReport && this.tableOptions.managedPrefixes.length > 0;
+ },
refreshIconAnimation: function() {
return this.page.state === 'loading' ? 'spin' : '';
}
@@ -106,13 +120,13 @@
const params = new URLSearchParams(window.location.search);
this.serverHost = params.get('host');
if (!this.serverHost) {
- this.page.alert = 'Missing server host, please launch the report from a user record.';
+ this.page.fault = 'supr:MISSING_SERVER_HOST';
return;
}
this.user.id = params.get('user');
if (!this.user.id) {
- this.page.alert = 'Missing user ID, please launch the report from a user record.';
+ this.page.fault = 'supr:MISSING_USER_ID';
return;
}
@@ -120,47 +134,89 @@
const self = this;
chrome.runtime.sendMessage({ operation: 'get-session', host: this.serverHost }, async function(session) {
if (!session.id) {
- self.alert = 'Missing session ID, your sesion may have expired or you\'ve been logged out. Log back in and refresh.';
+ self.page.fault = 'sf:INVALID_SESSION_ID';
return;
}
// Initialise Salesforce service
Vue.prototype.$salesforceService = new SalesforcePermissionsService(self.serverHost, session.id);
- // Run the report
- await self.runReport();
+ if (await self.sessionUserHasPermissions()) {
+ await self.runReport();
+ } else {
+ self.page.fault = 'supr:MISSING_PERMS';
+ }
});
},
+ sessionUserHasPermissions: async function() {
+ this.page.progress = 'Getting session user info...';
+
+ // Get user info from session
+ const getSessionUserInfoResult = await this.$salesforceService.getUserInfo();
+ if (!getSessionUserInfoResult.success) {
+ this.alertErrorResult(getSessionUserInfoResult);
+ return false;
+ }
+
+ this.page.progress = 'Checking session user permissions...';
+
+ // Get User LEX/Classic preference and Download AppExchange Packages system permission.
+ const sessionUserId = getSessionUserInfoResult.userInfo.userId;
+ const sessionUserQuery = `SELECT UserPreferencesLightningExperiencePreferred, Profile.PermissionsInstallMultiforce FROM User WHERE Id = '${sessionUserId}'`;
+ const sessionUserQueryResult = await this.$salesforceService.query(sessionUserQuery);
+ if (!sessionUserQueryResult.success) {
+ this.alertErrorResult(sessionUserQueryResult);
+ return false;
+ }
+
+ const userRecord = sessionUserQueryResult.records[0];
+
+ this.openUserInLex = userRecord['UserPreferencesLightningExperiencePreferred'];
+
+ // If the profile allow's the user to install AppExchange packages we don't need to check permission sets.
+ const canInstallExchangePackages = userRecord['Profile']['PermissionsInstallMultiforce'];
+ if (canInstallExchangePackages) {
+ return true;
+ } else {
+ // Check if any of the user's permission sets allow the user to install AppExchange packages.
+ const sessionUserPermissionSetQuery = `SELECT PermissionSet.PermissionsInstallPackaging FROM PermissionSetAssignment WHERE AssigneeId = '${sessionUserId}'`;
+ const sessionUserPermissionSetQueryResult = await this.$salesforceService.query(sessionUserPermissionSetQuery);
+ if (!sessionUserPermissionSetQueryResult.success) {
+ this.alertErrorResult(sessionUserPermissionSetQueryResult);
+ return false;
+ }
+
+ return sessionUserPermissionSetQueryResult.records.filter(record => record['PermissionSet']['PermissionsInstallPackaging'] === true).length > 0;
+ }
+ },
runReport: async function() {
this.page.state = 'loading';
this.page.progress = 'Querying user info...';
// Get the users name and profile ID
- const userQuery = `SELECT Username, ProfileId, UserPreferencesLightningExperiencePreferred FROM User WHERE Id = '${this.user.id}'`;
+ const userQuery = `SELECT Username, ProfileId FROM User WHERE Id = '${this.user.id}'`;
const userQueryResult = await this.$salesforceService.query(userQuery);
if (!userQueryResult.success) {
- this.page.alert = userQueryResult.error;
+ this.alertErrorResult(userQueryResult);
return;
}
const userRecord = userQueryResult.records[0];
this.user.name = userRecord['Username'];
- document.title = this.user.name;
-
- this.user.usesLex = userRecord['UserPreferencesLightningExperiencePreferred'];
+ document.title = `Loading: ${this.user.name}`;
// Get the profile full name
const profileId = userRecord['ProfileId'];
const profileResult = await this.$salesforceService.getProfileName(profileId);
if (!profileResult.success) {
- this.page.alert = profileResult.error;
+ this.alertErrorResult(profileResult);
return;
}
// Get the permission set full names
const permissionSetResult = await this.$salesforceService.getPermissionSetNames(this.user.id);
if (!permissionSetResult.success) {
- this.page.alert = permissionSetResult.error;
+ this.alertErrorResult(permissionSetResult);
return;
}
@@ -169,7 +225,7 @@
// Get managed package namespace prefixes
const namespacePrefixResult = await this.$salesforceService.getManagedPrefixes();
if (!namespacePrefixResult.success) {
- this.page.alert = namespacePrefixResult.error;
+ this.alertErrorResult(namespacePrefixResult);
return;
}
@@ -180,7 +236,7 @@
// Read profile and permission set metadata
const profileReadResult = await this.$salesforceService.readMetadata('Profile', [profileResult.name]);
if (!profileReadResult.success) {
- this.page.alert = profileReadResult.error;
+ this.alertErrorResult(profileReadResult);
return;
}
@@ -188,7 +244,7 @@
this.page.progress = `Reading permission sets... (${metadataRead}/${permissionSetResult.names.length})`;
});
if (!permissionSetsReadResult.success) {
- this.page.alert = permissionSetsReadResult.error;
+ this.alertErrorResult(permissionSetsReadResult);
return;
}
@@ -199,15 +255,19 @@
this.tree = this.$salesforceService.merge(profileReadResult.records, permissionSetsReadResult.records);
this.page.state = 'ready';
+ document.title = `Ready: ${this.user.name}`;
} catch (error) {
- this.page.alert = `${error.message} See console for details.`;
+ this.page.fault = 'supr:FAILED_MERGE';
console.error(error);
}
},
+ alertErrorResult: function(result) {
+ this.page.fault = result.faultCode;
+ },
onOpenUserClick: function() {
let userRelUrl = `/${this.user.id}?noredirect=1&isUserEntityOverride=1`;
- if (this.user.usesLex) {
+ if (this.openUserInLex) {
userRelUrl = '/lightning/setup/ManageUsers/page?address=' + encodeURIComponent(userRelUrl);
}
@@ -235,4 +295,10 @@
}
}
};
-
\ No newline at end of file
+
+
+
\ No newline at end of file
diff --git a/src/front-end/components/MetadataTypeCard.vue b/src/front-end/components/MetadataTypeCard.vue
index 1535709..d2ee068 100644
--- a/src/front-end/components/MetadataTypeCard.vue
+++ b/src/front-end/components/MetadataTypeCard.vue
@@ -30,8 +30,7 @@
'flowAccesses': 'Flow Accesses',
'pageAccesses': 'Visualforce Accesses',
'recordTypeVisibilities': 'Record Type Visibilities',
- 'categoryGroupVisibilities': 'Category Group Visibilities',
- 'loginFlows': 'Login Flows'
+ 'categoryGroupVisibilities': 'Category Group Visibilities'
};
export default {
diff --git a/src/front-end/components/Table.vue b/src/front-end/components/Table.vue
index 19f1f34..a9a7b5b 100644
--- a/src/front-end/components/Table.vue
+++ b/src/front-end/components/Table.vue
@@ -23,7 +23,6 @@
watch: {
tree: function() {
this.metadataTypes = [];
- this.permissionSetNames = [];
for (const typeName of Object.keys(this.tree)) {
const metadataTypeRow = {
diff --git a/src/front-end/services/SalesforceService.js b/src/front-end/services/SalesforceService.js
index 8dad0e3..0913ee3 100644
--- a/src/front-end/services/SalesforceService.js
+++ b/src/front-end/services/SalesforceService.js
@@ -1,4 +1,5 @@
const METADATA_ENDPOINT = '/services/Soap/m/54.0';
+const PARTNER_ENDPOINT = '/services/Soap/u/54.0';
const QUERY_ENDPOINT = '/services/data/v54.0/query';
const TOOLING_QUERY_ENDPOINT = '/services/data/v54.0/tooling/query';
@@ -67,7 +68,7 @@ export default class SalesforceService {
const responseXml = new window.DOMParser().parseFromString(responseXmlRaw, 'text/xml');
if (!response.ok) {
- return this._constructResultFromMetadataFault(responseXml);
+ return this._constructResultFromSoapFault(responseXml);
}
const recordNodes = responseXml.querySelectorAll('Envelope Body readMetadataResponse result records');
@@ -85,6 +86,38 @@ export default class SalesforceService {
};
}
+ async getUserInfo() {
+ const message = this._constructGetUserInfoMessage();
+
+ const requestUrl = new URL(PARTNER_ENDPOINT, this.serverBaseUrl);
+ const response = await fetch(requestUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'text/xml',
+ 'SOAPAction': '""'
+ },
+ body: message
+ });
+
+ const responseXmlRaw = await response.text();
+
+ const responseXml = new window.DOMParser().parseFromString(responseXmlRaw, 'text/xml');
+
+ if (!response.ok) {
+ return this._constructResultFromSoapFault(responseXml);
+ }
+
+ const resultNode = responseXml.querySelectorAll('Envelope Body getUserInfoResponse result')[0];
+
+ return {
+ success: true,
+ userInfo: Array.from(resultNode.childNodes).reduce((prev, curr) => {
+ prev[curr.nodeName] = curr.textContent;
+ return prev;
+ }, {})
+ }
+ }
+
async getManagedPrefixes() {
const namespacePrefixQuery = 'SELECT SubscriberPackage.NamespacePrefix FROM InstalledSubscriberPackage';
const namespacePrefixQueryResult = await this.query(namespacePrefixQuery, true);
@@ -109,8 +142,7 @@ export default class SalesforceService {
${this.sessionId}
-
+
${type}`;
@@ -126,6 +158,25 @@ export default class SalesforceService {
return message.trim();
}
+ _constructGetUserInfoMessage() {
+ let message = `
+
+
+
+
+ ${this.sessionId}
+
+
+
+
+
+ `;
+
+ return message.trim();
+ }
+
_authFetch(requestUrl) {
const actualRequestUrl = requestUrl.toString().replace('+', '%20');
@@ -136,7 +187,7 @@ export default class SalesforceService {
});
}
- _constructResultFromMetadataFault(responseXml) {
+ _constructResultFromSoapFault(responseXml) {
const faultNodes = responseXml.querySelectorAll('Envelope Body Fault faultcode');
let faultCode = 'unknown';
@@ -146,7 +197,8 @@ export default class SalesforceService {
return {
success: false,
- error: `Metadata operation failed with fault ${faultCode}`
+ error: `SOAP operation failed`,
+ faultCode: faultCode
};
}
}
\ No newline at end of file
diff --git a/src/manifest.json b/src/manifest.json
index 355660a..abfcf57 100644
--- a/src/manifest.json
+++ b/src/manifest.json
@@ -2,7 +2,7 @@
"name": "SUPR - Salesforce User Permission Report",
"short_name": "SUPR",
"description": "Salesforce User Permission Report allows you to see a report of all the permissions a user has and where they are set.",
- "version": "1.2.2",
+ "version": "1.2.3",
"author": "Aurel Hudec",
"homepage_url": "https://github.com/hudec117/sf-user-perm-report",
"incognito": "split",
diff --git a/src/package.json b/src/package.json
index 7dfc583..bd3766c 100644
--- a/src/package.json
+++ b/src/package.json
@@ -1,6 +1,6 @@
{
"name": "salesforce-user-perm-report",
- "version": "1.2.2",
+ "version": "1.2.3",
"description": "Salesforce User Permission Report allows you to see a report of all the permissions a user has and where they are set.",
"scripts": {
"dev": "webpack --watch --config webpack.dev.js",
diff --git a/src/report.html b/src/report.html
index 299ce50..0855392 100644
--- a/src/report.html
+++ b/src/report.html
@@ -8,9 +8,9 @@
-