Skip navigation

Of all the killer Summer 12 improvements to the Force.com Platform, the one we at Skoodat were most excited about was Post-Install and Uninstall Apex scripts. In a nutshell, they allow developers of Force.com Managed Packages (more posts coming on what these are and how to use them!) to execute arbitrary Apex Code every time that a User installs or uninstalls a version of the developer’s package. This feature is directly targeted at developers’ needs to perform various “setup” and “tear-down” tasks to help prepare customers’ orgs to use their new packages.

What sorts of tasks fall under this category? Here are a few of the most common:

Setup / On-Install Tasks

  • Create/update records of “configuration” objects
  • Create/update records of List Custom Settings or Organization-Default Hierarchy Custom Settings
  • Create sample data (for Trials)
  • Send “Welcome” Emails to the User who installed the package
  • Notify an external system (using a callout or email service)
  • Kick off a batch process (for initializing fields, modifying fields to fit new API’s, etc.)

Teardown / On-Uninstall Tasks

  • Delete sample data
  • Send “Thanks for using our Package” Emails to the User who uninstalls the package
  • Notify an external system (using a callout or email service)

Prior to this feature, developers would use various “hack” strategies for achieving this sort of behavior, including:

  • Setup Page
    • Strategy: The default tab in your app is a “setup” page that runs needed routines either on page load or asynchronously in response to user button clicks
    • Problems:
      • Users may not go to this page
      • DML is not permitted in page constructors, forcing asynchronous post-load behavior
      • Button click behavior often requires lots of custom JavaScript Remoting calls or VF rerenders
      • Wastes users’ time getting to the real meat of your app
  • Runtime Detection
    • Strategy: When custom triggers / VF pages in your app are run, your app looks to see if needed setup tasks have been performed, and runs setup routines as appropriate
    • Problems:
      • Makes your app run slower — this logic has to be run every time your trigger / page is run
      • DML is not permitted in VF page constructors, so any insertions of Custom Setting or custom configuration object records will have to occur post-load / asynchronously, which can really throw off page functionality

With the problems of the hack approaches in mind, Apex Install Scripts appeared to be a total godsend! And, largely, they are indeed.

So how does one use this tool? Well, the first step is to have a Managed Package. Once you have a Managed Package, you need to create a new class that implements the Install Handler interface. The docs have a basic example of how to do this. Then, edit your package:

You will then see two fields, one for specifying a Post Install Script, and another for specifying a script to run when your package is Uninstalled by a User. In the lookup, you will be prompted to select a class which implements InstallHandler. Be sure your class has COMPILED before trying this — otherwise it may not show up in the list of available classes.

Now, release a Managed – Beta version of your package (so that your InstallHandler  implementation class does not get locked down in to your package until you are ready). Once you have the install link, install your package into a Sandbox or Partner DE or Test Org (the only places where Managed – Beta packages can be installed). As soon as you are finished with the 2-3 step install instructions, your Post Install Script will be run. To run your Uninstall Script, uninstall this package.

Pretty cool stuff!

The Phantom of the Install Opera

As we at Skoodat dived  in to using Install Scripts, we encountered some mysterious, strange, sometimes phantom behaviors, which aroused some pretty basic questions in our minds as to the nature and functionality of Install Scripts — none of which are discussed in the Install Handler interface documentation:

  • Who do Install Scripts execute as?
    • What User?
    • What Profile?
    • Based on Source (DE) Org, or Destination Org?
  • What code/resources in the Destination Org do Install Scripts have access to?
    • Global methods in Apex Classes in Managed Apps that the Install Script’s app extends?
    • What packages is
  • Which Namespaces / Managed Apps do Install Scripts have access to?
    • All apps? Just the installer’s app? Or any Site Licensed apps?

In the absence of documentation, the only way to find out was to either ask questions of the SFDC community, or find out through experimentation. I did both, and here are some things I discovered:

  • Install Scripts execute as a totally unique User and Profile
    • Neither the User nor Profile exist in either Source or Destination orgs.
    • This User / Profile has essentially ‘God’ / System privileges
    • This User / Profile can create / modify records of Standard objects as well as Custom Objects / Custom Settings that come with the package being installed.
  • Do NOT annotate your Install Script, or any classes it calls (such as Batch/Scheduled Apex) as “with sharing”
    • YES: public class MyInstallScript implements InstallHandler
    • YES: public without sharing class MyInstallScript implements InstallHandler
    • NO: public with sharing class MyInstallScript implements InstallHandler
  • DML operations can fail if they are not initiated from the class which implements the InstallHandler interface
    • A DML operation executed from a helper class will fail due to SObjectExceptions such as “Field Name is not accessible”
    • *Simply extracting this code back in to the class which implements InstallHandler can avoid this issue*
  • Describe / Schema / Permissions info is WRONG / MISLEADING from the InstallScript context
    • Schema permissions (e.g. Object or Field Accessibility) always returns FALSE
      • Example: Calls such as Contact.Name.getDescribe().isAccessible() will universally return FALSE regardless of Profile permissions in either Source or Destination org
    • Profile / PermissionSet data is WRONG
      • Example: PermissionsModifyAllData returns FALSE on the Install Script User’s Profile / PermissionSet, but the InstallScript CAN modify all data!
    • Consequences: Do NOT try to dynamically determine whether to permit DML based on permissions from within Install Scripts. You will get very frustrated.
    • *Timeline for Resolution*: Summer 13 release (Safe Harbor — got this from SFDC support)
      • UPDATE 2/25/2013 — looks like this was NOT resolved in Spring 13, so I’ve changed to Summer 13 😦

If you’re wondering how I verified this information, here’s a Post Install Script I used to determine this information. Basically it spits out some Describe information on some standard objects (Account and Contact) as well as a custom object in the source developer org (relax__Job__c). I also spit out the username and profileId of the running user, by which I discovered that the running user is in fact a “phantom” with all the powers of System. Here’s the complete InstallScript. After assembling a debug string, it calls a helper method that emails me the content.


public class InstallScript implements InstallHandler {

	public void onInstall(InstallContext ctx) {

		String username = UserInfo.getUserName();
		String profileId = UserInfo.getProfileId();
		String debugString =
			'Username: ' + ((username != null) ? username : 'null')
			+ 'ProfileId: ' + ((profileId != null) ? profileId : 'null')
			+ ', Contact.Accessible: ' + Contact.SObjectType.getDescribe().isAccessible()
			+ ', Contact.LastName.Accessible: ' + Contact.LastName.getDescribe().isAccessible()
			+ ', Account.Accessible: ' + Account.SObjectType.getDescribe().isAccessible()
			+ ', Account.Name.Accessible: ' + Account.Name.getDescribe().isAccessible()
			+ ', relax__Job__c.Accessible: ' + relax__Job__c..SObjectType.getDescribe().isAccessible()
			+ ', relax__Job__c.relax__Apex_Class__c.getDescribe().isAccessible(): ' + relax__Job__c.relax__Apex_Class__c.getDescribe().isAccessible();

		JobScheduler.SendDebugEmail(
			debugString,debugString,'Debug from Relax Install Script in org ' + ctx.organizationId(),'myname@mycompany.com'
		);
	}

	/////////////////
	// UNIT TESTS
	/////////////////

	private static testMethod void TestInstall() {
		InstallScript is = new InstallScript();
    	Test.testInstall(is, null);
    	Boolean b = false;
    	System.assertEquals(false,b);
	}
}

And… here is the debug output:

Username: 033e0000000h2ydias@00dd0000000bt2keaq
ProfileId: 00ed0000000SUxMAAW
Contact.Accessible: false
Contact.LastName.Accessible: false
Account.Accessible: false
Account.Name.Accessible: false
relax__Job__c.Accessible: false
relax__Job__c.relax__Apex_Class__c.Accessible: false

As you can see, Schema methods universally return false, and a quick trip to SOQLExplorer will reveal that neither the User or Profile executing this Script exists in either Source or Destination org.

Making the Switch

So, bugs and weirdness aside, Install Scripts can still do some rocking things. At Skoodat, we’ve used them to populate certain configuration objects from JSON/XML stored in Static Resources included with our package, as well as to insert default custom settings. Here’s an InstallScript that would tackle the second of these:


public class InstallScript implements InstallHandler {

   public void onInstall(InstallContext ctx) {

      // Create an Org-Default record
      // of a Hierarchy Custom Setting
      // included in our package
      insert new MySetting__c(
         // Makes an Org Default setting
         SetupOwnerId = UserInfo.getOrganizationId(),
         SettingField__c = 'JediWarrior';
      );

   }

   /////////////////
   // UNIT TESTS
   /////////////////

   private static testMethod void TestInstall() {

      // Delete any Org-Default Custom Settings
      delete [select Id from MySetting__c
         where SetupOwnerId = :UserInfo.getOrganizationId()];

      // Run our post-install script,
      // simulating a no-prior version installed scenario
      InstallScript is = new InstallScript();
      Test.testInstall(is, null);

      // Verify that an Org-Default setting was created
      MySetting__c c = MySetting__c.getOrgDefaults();

      System.assertNotEquals(null,c);
      System.assertEquals('JediWarrior',c.SettingField__c);

   }
}

So much easier than making a custom setup page, and it happens instantly!

We’re definitely looking forward to more improvements to this feature in Winter 13 and beyond, especially when we can have public implementations of global interfaces. Moreover, Winter 13 will include some sweet new methods for working with Static Resource data in Tests, so I may just be convinced to post an example of creating custom config object records in an InstallHandler from data stored in Static Resources as JSON/XML — so keep posted!

UPDATE 6/25/2014

I recently stumbled upon a post by Matt Bingham on Salesforce StackExchange where he documents that there is a huge difference between marking your Install Script as without sharing , e.g.

public class MyInstallScript implements InstallHandler {

}

is NOT the same as

public without sharing class MyInstallScript implements InstallHandler {

}

In fact, marking your class as without sharing grants it permission to

  • view all data
  • modify all data
  • interrogate system data (like CronTrigger and ApexClass)

I have also seen that there are some objects, such as the Chatter Group object (CollaborationGroup) that you can only interact with fully from Install Scripts in this “super-user” mode.

Regarding inaccurate PermissionSet / Profile data, I have also run into another frustrating wrinkle: the Install Script User does have a Profile, but this Profile is not queryable, and so any logic in your app that you have related to querying on the running user’s Profile or PermissionSetAssignments will NOT work, because (a) the User’s Profile record does not exist (you get the error “SObject has no rows for assignment”) and (b) the PermissionSetAssignments relationship on User is not visible from the InstallScript context (you get the error “Didn’t understand relationship PermissionSetAssignments”). Therefore, you’ll have to write special logic related to the scenario where the User’s Profile record does not exist, to avoid query exceptions.

 

Advertisements

19 Comments

  1. Interesting article! I’m curious to know if you have dealt with scheduling jobs in an Install Script. We’re currently trying to address the “Field Name is not accessible” error for one of our scheduled jobs that we schedule through our install script. The jobs get scheduled as the “phantom user” – a few of them execute fine, but one of them modifies Account fields and fails.

    Our potential solution right now is to create the scheduled jobs from a setup or configure page instead – they execute fine when scheduled by a system administrator.

    • Bobby, I’d be very interested to know if scheduling jobs works from an install script — haven’t tried it myself, but considering that scheduled jobs are always run as the user who initiated them, I have a hard time imagining that you could initiate it from an Install Script, as the jobs would be scheduled as the “Phantom User”, who doesn’t actually exist… if it works, that’d be pretty incredible! But in the meantime, I think initiating Scheduled Jobs from a Config/Setup page is definitely the best option.

  2. Thanks for writing up this blog post, helped in quickly implementing an installscript and its unit test.

  3. From what I’m experiencing, this phantom user’s profile does not have the PermissionsModifyAllData. So it is not really “god”. I have a trigger, that only executes if the running user’s profile has PermissionsModifyAllData = true. The trigger is not executed when the install script runs (though it should) which makes me believe that this phantom user does not have PermissionsModifyAllData.

    Any ideas how I can modify my trigger, so it executes if the current user’s profile has PermissionsModifyAllData OR it is the phantom user?

    • Alex, from my experience, the “Phantom User” DOES, in fact, have “god” permissions — meaning that it technically CAN do anything, to any data. However, the bizarre thing is that this capability is NOT reflected in any metadata settings! For instance, I promise you that your install script can insert a new Account record. However, if you check Account.SObjectType.getDescribe().isCreateable(), or Account.Name.getDescribe().isCreateable() these will (since I last checked) always return FALSE. This is a documented “inconsistency”, from what I’ve heard from Salesforce Support, that may be resolved in Summer 13.

      I’m sure that the same “inconsistency” applies with ALL Permissions, including the Profile “PermissionsModifyAllData” Permission. So, in order to get around this in your trigger, here’s what I’d do.

      1. Create a static flag in your InstallScript class to keep track of whether your InstallScript is running, e.g. “public static boolean IsRunning”, which is initially false.
      2. In your InstallScript, set this Static Flag to TRUE.
      3. IN your trigger, your conditional should be set to (pseudocode) if (userProfile.PermissionsModifyAllData = true || InstallScript.IsRunning) // Do my logic

      • That worked like a charm. Great trick – thank you so much.

  4. Nice post. I’m having issues inserting a user in the install script. Have you ever attempted this?

    Looks like since summer 2013 test methods can only be in test classes.

    • Hi Steve,

      There are multiple possible issues that I can imagine you might be having related to inserting users with an install script. I have never had to do this as part of an install script, so my first question is, what is your use case? Secondly, what else are you doing in the install script? If you are trying to do DML on “regular”,non-Setup objects — e.g. Accounts, Contacts, custom objects — AND do DML on Setup-objects (e.g. Users, PermissionSetAssignments), then you will encounter the “MIXED_DML_OPERATION” restriction — you can’t do DML on both setup and non-setup objects in the same transaction. To get around this, you’ll need to initiate one or more separate transactions. For instance, you can do DML on one type in your Install Script, but then have your Install Script call a @future method, or launch a Batch Apex process, that perform DML on the other type. So, for instance, your Install Script inserts / modifies User records, and inserts/deletes PermissionSetAssignments, and inserts an Org-Default Custom Setting. THEN, at the very end of your Install Script, you call an @future method that will create new records of a Custom Object, modify some Cases, etc.

      Other issues you might run into with your install script are: Profile/Licensing limits. For instance, you might be trying to create a new User with a Profile associated with the “Salesforce Platform” User Licenses … but the given Org may already have used up all of its Salesforce Platform User Licenses. THis will make your insert operation on a new User record fail. And the worst part is that there’s no “nice” way to check how many licenses an Org has allocated of a given type — however, you CAN check this by wrapping your “insert newUser;” in a try/catch block — if the insert fails due to a System.LicenseException, that’s a good sign that the given org doesn’t have any more licenses of the type you’re trying.

  5. Another thing that I noticed was, the install script special user do not have permission to do chatter posts. I was trying to post into a chatter group from install script, however it didn’t made any posts. Do you know how to enable debug logs for this user?

    • Hi Shantinath, try marking your InstallScript class as without sharing, like this:


      public without sharing class MyInstallScript implements InstallHandler {

      }

  6. Although CollaborationGroup is accessible but its returing 0 rows even though record exists in same org.

    • Did you mark your class as without sharing?

        • Ashish Narang
        • Posted May 13, 2015 at 2:13 am
        • Permalink

        yes, my class is without sharing. Is it working for you ?

  7. Interesting write up, thanks for that!
    We are experimenting with an InstallHandler that should do a callout to an external application.

    What was your solution to authenticate the request that was originated in the install handler in the external application?
    The documentation states there is no access to an session id (phantom user? ;)) in the install handler, so using the api to authenticate the user/request can not be used.

    I have also created a question in stackexchange regarding to this topic:
    http://salesforce.stackexchange.com/questions/89839/verify-org-identity-for-an-external-request-originated-in-an-installhandler

    It would be nice if you can share your approach/thoughts.

    • Luca, since you’re calling out to an external application, I don’t think you’ll need a Salesforce Session Id, you’ll just need to authenticate to your external application according to its standard authentication mechanism.

  8. Can we create new Permission Set in Post Install Script ?

    Please help on this ?

    • I think that you should be able to create a new unmanaged Permission Set in a customer’s org via a Post-Install Script. You will NOT be able to create or modify Permission Sets within your managed package, but you could assign these to a customer.

  9. Can we add Fields permissions in the Permissions set via post install script ?

    I tried It by using FieldPermissions object but It gives me the following error : Can not modify managed object : FieldPermissions.

    Can you please help us Its very urgent .

    • No you cannot modify a Permission Set that is included in your Managed Package during a post-install script — you cannot modify any managed package metadata during a post-install script. You could however create a new un-managed / local permission set in the customer’s org during your post-install script, and then assign that PermissionSet to user(s) in the customer’s org. Just make sure that your Post-Install Script is marked as “without sharing”.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: