.NET, SharePoint

Custom Code Access Security for Web Parts

We’ve been trying to figure out a way to do some custom searching in MOSS here for a couple weeks.  The first plan of attack was a custom Content Query Web Part using CommonViewFields hooked up to site columns based on a lookup.  Turns out there’s a bug doing that which doesn’t allow it to work as we wanted (maybe I’ll post about that later.)  That bug put a stop to the Content Query solution, so we moved on to another idea.

My friend and co-worker Brad Younge wrote a cool web part that does custom searching.  Two of them, actually.  One for custom queries and another for browsing content.  It’s a bit much for what we were trying to do, but once we show it to the customer I think we can modify it to their needs.

Once I got it up and running on my machine, I began getting security errors.  Like:

Request for the permission of type ‘Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c’ failed.

Took me a bit of searching to find the culprit.  The trust level on my site is set to WSS_Minimal.  Changing this to WSS_Medium or Full eliminated the errors, but changing the trust level in production wasn’t an option so I needed to go down the road of creating some custom code access security settings.  It’s not that difficult, but takes some time to track down what entries you need.  I ended up putting the following into my manifest.xml file:

    <CodeAccessSecurity>
        <
PolicyItem>
            <
PermissionSet class=NamedPermissionSet version=1 Description=Allow access to Statera BrowseForIt>
                <
IPermission class=AspNetHostingPermission version=1 Level=Minimal/>
                <
IPermission class=SecurityPermission version=1 Flags=Execution />
                <
IPermission class=System.Security.Permissions.RegistryPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 version=1 Unrestricted=True />
                <
IPermission class=Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c version=1 Unrestricted=True />
            </
PermissionSet>
            <
Assemblies>
                    <
Assembly Name=Statera.Moss.Webparts.BrowseForIt Version=1.0.0.0 PublicKeyBlob=0x002400000480000094000000060200000024000052534131000400000100010031B4F1C0A8692A7AFA369B31D858618DBB26497585F791DD5B8BE9FD2AC7C9554B348BD97E37984CE645AECBE104E14AC99821C94CE0BF726459339D21A4EA9A1220BD7E4E858D9375C4D3B5EE98F3DA5D2D99C9628A03047E81CB409CB04A98633C6E2396E6289FF37A8781E6691697D48528C4B146CE631156F08A87309797 />
            </
Assemblies>
        </
PolicyItem>
        <
PolicyItem>
            <
PermissionSet class=NamedPermissionSet version=1 Description=Allow access to Statera Common>
                <
IPermission class=AspNetHostingPermission version=1 Level=Minimal/>
                <
IPermission class=SecurityPermission version=1 Flags=Execution />
                <
IPermission class=System.Security.Permissions.EnvironmentPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 version=1 Read=USERNAME />
                <
IPermission class=Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c version=1 Unrestricted=True />
            </
PermissionSet>
            <
Assemblies>
                <
Assembly Name=Statera.Moss.Webparts.Common Version=1.0.0.0 PublicKeyBlob=0x00240000048000009400000006020000002400005253413100040000010001006B801BE53E218D8C0A003278A44D51BB14EDCD573981DD1288A7A492EF389D4BDC4D657F3F3CBF99222C34088AC56CC27FC7CC63821B67F0C70E1D1D7990DD80BCCCA0F455BB97908FFE41A7635172C7B5CAE4E923CE39A65FA5DED4498C750525CF997387F9C89DB3CFB644B97EC7EFACEBD5E081B237511CAADC8104380CDB />
            </
Assemblies>
        </
PolicyItem>
    </
CodeAccessSecurity>

When built into the .wsp and deployed with the -allowcaspolicies flag, it does a couple things.  First, it creates a custom trust config file called wss_custom_wss_minimaltrust.config.  That’s assuming your site is set to wss_minimal when you deploy.  If it’s set at wss_medium, the filename will be wss_custom_wss_mediumtrust.config.  It then sets the trust in web.config to WSS_Custom, changing it from the previous setting. 

When it creates the custom trust file, it takes the wss_minimaltrust.config file and adds content based on what’s in the manifest.  For this example, it adds two new PermissionSet entries with the listed IPermission mapping and settings.  

            <PermissionSet class=NamedPermissionSet version=1 Description=Allow access to Statera Common Name=Statera.Moss.Webparts.BrowseForIt.wsp-9d32b4ed-ddec-4ba8-804f-d69a3347dfe0-2>
              <
IPermission class=AspNetHostingPermission version=1 Level=Minimal />
              <
IPermission class=SecurityPermission version=1 Flags=Execution />
              <
IPermission class=System.Security.Permissions.EnvironmentPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 version=1 Read=USERNAME />
              <
IPermission class=Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c version=1 Unrestricted=True />
            </
PermissionSet>
            <
PermissionSet class=NamedPermissionSet version=1 Description=Allow access to Statera BrowseForIt Name=Statera.Moss.Webparts.BrowseForIt.wsp-9d32b4ed-ddec-4ba8-804f-d69a3347dfe0-1>
              <
IPermission class=AspNetHostingPermission version=1 Level=Minimal />
              <
IPermission class=SecurityPermission version=1 Flags=Execution />
              <
IPermission class=System.Security.Permissions.RegistryPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 version=1 Unrestricted=True />
              <
IPermission class=Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c version=1 Unrestricted=True />
            </
PermissionSet>

The other change to the file takes place down in the CodeGroup section, where it adds one UnionCodeGroup class CodeGroup for each PermissionSet.  These CodeGroups point back to the PermissionSet so WSS knows which policies to apply when code in the listed assembly is run.  

            <CodeGroup class=UnionCodeGroup version=1 PermissionSetName=Statera.Moss.Webparts.BrowseForIt.wsp-9d32b4ed-ddec-4ba8-804f-d69a3347dfe0-2>
              <
IMembershipCondition version=1 Name=Statera.Moss.Webparts.Common class=StrongNameMembershipCondition PublicKeyBlob=0x00240000048000009400000006020000002400005253413100040000010001006B801BE53E218D8C0A003278A44D51BB14EDCD573981DD1288A7A492EF389D4BDC4D657F3F3CBF99222C34088AC56CC27FC7CC63821B67F0C70E1D1D7990DD80BCCCA0F455BB97908FFE41A7635172C7B5CAE4E923CE39A65FA5DED4498C750525CF997387F9C89DB3CFB644B97EC7EFACEBD5E081B237511CAADC8104380CDB AssemblyVersion=1.0.0.0 />
            </
CodeGroup>
            <
CodeGroup class=UnionCodeGroup version=1 PermissionSetName=Statera.Moss.Webparts.BrowseForIt.wsp-9d32b4ed-ddec-4ba8-804f-d69a3347dfe0-1>
              <
IMembershipCondition version=1 Name=Statera.Moss.Webparts.BrowseForIt class=StrongNameMembershipCondition PublicKeyBlob=0x002400000480000094000000060200000024000052534131000400000100010031B4F1C0A8692A7AFA369B31D858618DBB26497585F791DD5B8BE9FD2AC7C9554B348BD97E37984CE645AECBE104E14AC99821C94CE0BF726459339D21A4EA9A1220BD7E4E858D9375C4D3B5EE98F3DA5D2D99C9628A03047E81CB409CB04A98633C6E2396E6289FF37A8781E6691697D48528C4B146CE631156F08A87309797 AssemblyVersion=1.0.0.0 />
            </
CodeGroup>

I think that about covers it.  In one of the articles I read covering code access security the author mentioned every web part developer runs into this at some point, and once you figure it out you don’t forget.  I hope he’s right!  Oh, and here’s a good MSDN article covering the topic.

.NET, SharePoint, Windows Workflow

How to Deploy a Custom Activity as a Feature

Henry asked me to figure out a way to easily deploy the custom activity I wrote, so with a suggestion and a pointer from him I was on my way.  The end result is deployment of the activity as a feature.  Here’s how it’s done.

The concept is simple.  Use the normal Feature deployment framework to do the three things needed to deploy the activity.  Those are:

  1. Deploy the dll to the GAC
  2. Add the .ACTIONS file to the 12 hive
  3. Update the authorizedTypes in the web.config

The first two are pretty simple, but I’ll show the manifest so you get an idea if you’re not sure how that works.

<Solution SolutionId="153F5D62-5662-4288-B915-903F5B950E39" xmlns="http://schemas.microsoft.com/sharepoint/" ResetWebServer="TRUE">

<
FeatureManifests>
<
FeatureManifest Location="CustomActivitiesFeature.xml"/>
</
FeatureManifests>

<
RootFiles>
<
RootFile Location="TEMPLATE1033WorkflowFindManager.ACTIONS" />
</
RootFiles>

<
Assemblies>
<
Assembly DeploymentTarget="GlobalAssemblyCache" Location="MyAssembly.dll">
<
SafeControls>
<
SafeControl Assembly="MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9eed2245513232a4" Namespace="MyNamespace" TypeName="*" />
</
SafeControls>
</
Assembly>
</
Assemblies>
</
Solution>

To update the authorizedTypes in the web.config I needed some help (remember, I’m new!)  Henry pointed me to the SPWebConfigModification class and the concept of an SPFeatureReceiver.  The theory was to use an SPFeatureReceiver and override the FeatureActivated method and use that to call into SPWebConfigModification to update the web.config file with the new authorizedType entry.  So with that, I was off!!

Other than the MSDN documentation, I found a couple other valuable resources.  The first was a post by Tony Bierman on Using SPWebConfigModification to modify a SharePoint web application’s web.config for all servers in the farm.  Sounds like what I need, right?!  Just about.  The biggest thing missing was how to get the right SPWebApplication reference.  His example was using a console app in which the web app was passed in as a parameter.  I played around with SPContext (not sure why, since there’s no http context during deployment of a wsp), as well as different options using the Parent of the Feature.  I never quite figured it out.  Then I ran into Daniel Larson’s posting on the same subject.  (I think he and I ran into each other a few years ago on a project in Parker, by the way.  I’m not sure.)

Dan was grabbing the web app using the following:

        SPWebApplication app = null;
        SPSiteCollection site = properties.Feature.Parent as SPSiteCollection;
        if (site == null)
        {
            SPWeb web = properties.Feature.Parent as SPWeb;
            if (web != null)
                app = web.Site.WebApplication;
        }
        else
            app = site.WebApplication;

Basically, using properties.Feature.Parent, as I was attempting.  He first checks to see if it’s a Site Collection and then, if not, assumes it’s a Web.  Well, guess what?  I wasn’t deploying my feature as either!  Maybe I need to rethink that, but I’m deploying it to a specific Site.  This helped me get to the right web application by doing:

        SPSite site = (SPSite)properties.Feature.Parent;
        SPWebApplication webApp = SPWebApplication.Lookup(new Uri(site.Url));

After that I needed to spend some time reviewing how SPWebConfigModification works.  I started out by creating a modification object for my authorizedTypes entry and calling webApp.WebConfigModifications.Add, but that didn’t work.  If you take a look at the MSDN docs you’ll see why I started there.  I was missing a few steps before I could call .Add.  I needed to add a few more settings to the modification, as you can see in the code below.  That’s about it for the tricky stuff, so I’ll post the code.  One more thing to notice, though, is that I also overrode FeatureDeactivating to remove the .config entry.

        public override void FeatureActivated(SPFeatureReceiverProperties properties)
        {
            try
            {
                SPSite site = (SPSite)properties.Feature.Parent;
                SPWebApplication webApp = SPWebApplication.Lookup(new Uri(site.Url));

                if (webApp != null)
                    UpdateWebConfig(webApp, false);
                else
                    throw new ApplicationException(“Could not locate a web application”);
               
                //Write log entry for success
                System.Diagnostics.EventLog el = new System.Diagnostics.EventLog();
                el.Source = “WebConfigFeature”;
                el.WriteEntry(“Activation complete.”);
            }
            catch (Exception ex)
            {
                System.Diagnostics.EventLog el = new System.Diagnostics.EventLog();
                el.Source = “WebConfigFeature”;
                el.WriteEntry(ex.Message);
            }
        }

        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
        {
            try
            {
                SPSite site = (SPSite)properties.Feature.Parent;
                SPWebApplication webApp = SPWebApplication.Lookup(new Uri(site.Url));

                if (webApp != null)
                    UpdateWebConfig(webApp, true);
                else
                    throw new ApplicationException(“Could not locate a web application”);

                //Write log entry for success
                System.Diagnostics.EventLog el = new System.Diagnostics.EventLog();
                el.Source = “WebConfigFeature”;
                el.WriteEntry(“De-Activation complete.”);
            }
            catch (Exception ex)
            {
                System.Diagnostics.EventLog el = new System.Diagnostics.EventLog();
                el.Source = “WebConfigFeature”;
                el.WriteEntry(ex.Message);
            }
        }

        private static void UpdateWebConfig(SPWebApplication webApp, bool removeModification)
        {
            SPWebConfigModification modification =
                new SPWebConfigModification(“authorizedType[@Assembly=”MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9eed2245513232a4″][@Namespace=”MyNamespace”][@TypeName=”*”][@Authorized=”True”]”, “configuration/System.Workflow.ComponentModel.WorkflowCompiler/authorizedTypes”);
         
            modification.Owner = “Ryan”;
            modification.Sequence = 0;
            modification.Type = SPWebConfigModification.SPWebConfigModificationType.EnsureChildNode;
            modification.Value =
                 string.Format(CultureInfo.InvariantCulture,
                 “<authorizedType Assembly=”{0}” Namespace=”{1}” TypeName=”{2}” Authorized=”{3}”/>”,
                 new object[] { “MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9eed2245513232a4”, “MyNamespace”, “*”, “True” });

            if (removeModification)
                webApp.WebConfigModifications.Remove(modification);
            else
                webApp.WebConfigModifications.Add(modification);

            SPFarm.Local.Services.GetValue<SPWebService>().ApplyWebConfigModifications();
        }

SharePoint

SharePoint Logs

I know I implied earlier I wouldn’t be posting trivial stuff…Although this may fall into that category, it also falls in to the category of “Ryan will forget this”.

We don’t have a way to attach a debugger to our dev site right now, so debugging is writing text out to a field somewhere and reading that, then change your code, redeploy, check again, etc.  That’s IF your code doesn’t throw an error.  If it throws an error, you need to check out the logs to see what it is.  I didn’t know they existed until I read a comment reply on this post from the SharePoint blog on SharePoint workflow.

The log files can be found at C:Program FilesCommon FilesMicrosoft Sharedweb server extensions12LOGS.  Imagine that…the LOGS directory in the 12 hive…who would’ve guessed??

SharePoint, Windows Workflow

Custom Workflow Activity for SharePoint Designer to Retrieve Manager

My recent task was to create a custom workflow activity that users could access in SharePoint Designer which would be used to retrieve a users manager from Active Directory.  No, you don’t get this out of the box.  But you almost do!  And with this post it should be taken care of.

I won’t cover the basics for how to create a custom activity and deploy it for use with SPD.  There are many other posts that cover those details.  And speaking of being lazy, turns out there is a code sample in the ECM Starter Kit that is a great starting point for exactly what I was trying to accomplish (download it, install it, sample located in “ECM Starter KitCode SamplesWorkflowECMActivities”).  Through a lot of trial and error (no debug capability in clients dev site) I was able to modify it to be a bit more stable.  I added logic to handle things like AD properties not existing and manager name being in a couple different formats ({First Name} {Last Name} and {Last Name}, {First Name}). 

I’ll post the code for the SetManagerFields method as that’s where all the customization takes place.

    public void SetManagerFields(object sender, EventArgs e)
    {
        try
        {
            SearchSuccessful = false;

            //Set up the AD objects
            DirectoryEntry dirEntry = new DirectoryEntry("GC:");
            if (null == dirEntry)
            {
                Outcome = "Could not connect to Directory";
                return;
            }

            System.Collections.IEnumerator gcChildrenEnum = dirEntry.Children.GetEnumerator();
            gcChildrenEnum.MoveNext();

            //Search for Manager of AccountName
            DirectorySearcher searcher = new DirectorySearcher((DirectoryEntry)gcChildrenEnum.Current);
            string filterString = "(samAccountName= " + AccountName.Substring(AccountName.IndexOf("") + 1) + ")";
            searcher.Filter = filterString;
            SearchResult result = searcher.FindOne();

            if (result == null)
            {
                Outcome = "Could not find user account.";
                return;
            }

            if (result.Properties.Contains("manager"))
            {
                //Parse manager display name from search result
                ManagerDisplayName = result.Properties["manager"][0].ToString();
                int index;
                int indexComma = ManagerDisplayName.IndexOf(',');
                int indexSlash = ManagerDisplayName.IndexOf('');
                //If a comma is escaped, grab the second comma to get the full display name
                if (indexSlash != -1 && indexComma > indexSlash)
                    index = ManagerDisplayName.IndexOf(',', indexComma + 1);
                else
                    index = indexComma;

                ManagerDisplayName = ManagerDisplayName.Substring(3, index  3);
                //If there's a slash, remove it (.Replace("", "") doesn't work)
                if (indexSlash != -1)
                {
                    //Find it again cause it moved
                    indexSlash = ManagerDisplayName.IndexOf('');
                    indexComma = ManagerDisplayName.IndexOf(',');

                    ManagerDisplayName = ManagerDisplayName.Substring(0, indexSlash) +
                        ManagerDisplayName.Substring(indexComma);
                }

                searcher.Filter = "(displayName= " + ManagerDisplayName + ")";
                result = searcher.FindOne();
                if (result == null)
                {
                    Outcome = "Could not find manager " + ManagerDisplayName + ".";
                    return;
                }

                //Propagate data to the promoted properties
                if (result.Properties.Contains("samaccountname"))
                    ManagerAccountName = result.Properties["samaccountname"][0].ToString();
                if (result.Properties.Contains("mail"))
                    ManagerEmailAddress = result.Properties["mail"][0].ToString();
                Outcome = "Manager retrieved successfully.";
                SearchSuccessful = true;
            }
            else
            {
                Outcome = "AD does not contain a manager property";
                return;
            }
        }
        catch (Exception ex)
        {
            Outcome = "Error: " + ex.Message;
        }
    }

Pretty nice, right?  I know, most of that I didn’t even write.  I’ll shut up.  But…Now when a user uses this activity in their workflow in SPD, they get a nice response back even if the manager property isn’t even there (which is possible depending on the quality of the directory.)  After running the action, they can check the IsSuccess variable to determine if the manager was found.  Then act accordingly.

SharePoint

New SharePoint Category

No more playing around.  After lots of labs, classes, webcasts, reading, etc. on SharePoint 2007 (MOSS), I’m now on a project actually working with it.  I’ve been on it for about 6 weeks now so I’m getting dangerous enough to post some tidbits I run into along the way that aren’t basic.  So MOSS, here we come!