Sunday, 19 August 2007

Automatically setting custom permissions on new sites

This is the third and final article in a series of three, where I demonstrate how how to perform custom processing in the site creation process. See 'Article series - custom permissions with a site definition' for the full run down on the article series. Specifically, I wanted to show how to use code to modify sites as they are created, in order to do things which aren't normally possible with site definitions/site templates. In the example I'm using, I'm setting custom permissions on the created sites. A scenario where this might be useful is if say, your organization is using SharePoint in a collaboration sense and users are creating sites themselves, but certain sites need to be secured so that access is restricted to specific users. Often end users might not understand the details of the SharePoint security model, so it would be nice if we could take care of this automatically for them.

The solution

In the last article 'Site definitions - custom code in the site creation process', I showed how it's possible to use a Feature receiver in conjunction with the site definition to do pretty much anything you might want to do as sites are created. Based on this approach, my solution is based around the following:



  • Custom list which stores the list of authorized users in the site collection's root web. This list stores a mapping of users to the permissions they should have in the created site.
  • Custom site definition, created by copying an existing definition as described in the SDK.
  • Feature which doesn't have any Feature elements defined, but is attached to a Feature receiver. A property is defined to pass in the name of the permissions list.
  • Feature receiver code which uses the object model to iterate the permission list and grant appropriate permissions to each user listed.

So let's break down each element of the solution. Note that all the code etc. is available for download and is linked to at the end of the article. First of all, we create a list which looks like this (click to enlarge):



Looking at the the edit view for the list (shown below), we see that the two key columns are:

  • 'User' - Person or Group data type
  • 'Permission level' - choice data type, with allowable values 'Owner', 'Contributor' and 'Viewer'




If we are creating sites which are restricted we would probably want to secure the list so that curious users cannot add themselves, and then gain access to any future restricted sites which are created.


So that's the list. The site definition in my example doesn't do anything special - it's just a copy of the 'BLANKINTERNET' definition to keep the example simple. However, 'Creating, deploying and updating custom site definitions' has more information on the kinds of customizations you can make with your site definitions.

The Feature is defined to reference the Feature receiver class we are creating. This ensures our custom code will run when the Feature is activated.

<Feature  Title="SiteProvisioning" Id="7C020FFF-FF42-4fe2-8A9B-9BCA0D5F8001" Description="" Version="1.0.0.0" Scope="Web"

          Hidden="TRUE" DefaultResourceFile="core"

          ReceiverAssembly="COB.Demos.SiteDefinition, Version=1.0.0.0, Culture=neutral, PublicKeyToken=cd9b418c14cff42e"

          ReceiverClass="COB.Demos.ProjectXSiteDefinition.SiteProvisioning" xmlns="http://schemas.microsoft.com/sharepoint/">

  <Properties>

    <!-- could also retrieve list by GUID by passing in as property and amending code slightly -->

    <Property Key="PermissionsListName" Value="Project X Permissions" />

  </Properties>

</Feature>


The 'ReceiverAssembly' and 'ReceiverClass' attributes have the values which point to our Feature receiver class which contains our custom code. Note also we are passing in a value which can be retrieved in the code by using a Feature property. This can be used as a more flexible alternative to hardcoding values in the class - in this case we are using it pass in the name of the 'authorized users' list, meaning that this Feature can be reused across different requirements (which would use different lists). A minor tweak to the code and property will allow you to use the list GUID if you prefer, though note that list GUIDs will be different if the list is recreated in another SharePoint environment.

So that's all great but what ensures the Feature gets activated? Ah you've lost the thread since the last article haven't you?! The Feature is activated automatically when sites are created using the definition courtesy of this line in the WebFeatures section of the onet.xml file which specifies what the site definition consists of:


<Feature ID="7C020FFF-FF42-4fe2-8A9B-9BCA0D5F8001">


And so finally, onto the code which we have written in our Feature receiver class:

using System;

using Microsoft.SharePoint;

 

namespace COB.Demos.ProjectXSiteDefinition

{

    class SiteProvisioning : SPFeatureReceiver

    {

        public override void FeatureActivated(SPFeatureReceiverProperties properties)

        {

            SPWeb currentWeb = null;

            SPSite currentSite = null;

            object oParent = properties.Feature.Parent;

 

            // retrieve the permissions list by name..

            string sPermsListName = properties.Definition.Properties["PermissionsListName"].Value;

 

            // only perform processing if the site definition is being used to create a web within the expected site collection..

            if (properties.Feature.Parent is SPWeb)

            {

                currentWeb = (SPWeb)properties.Feature.Parent;

                currentSite = currentWeb.Site;

 

                SPList permsList = currentSite.RootWeb.Lists[sPermsListName];

 

                // ensure the web is set to use unique permissions, we won't copy existing permissions from parent site..

                if (!currentWeb.HasUniqueRoleAssignments)

                {

                    currentWeb.BreakRoleInheritance(false);

                }

 

                foreach (SPListItem perm in permsList.Items)

                {

                    string sPermLevel = (string)perm["Permission level"];

 

                    SPFieldUserValue userValue = (SPFieldUserValue)perm.Fields["User"].GetFieldValue(perm["User"].ToString());

                    SPUser user = userValue.User;

                    setPermission(currentWeb, user, sPermLevel);

                }

 

                currentWeb.Update();

            }

        }

 

        private void setPermission(SPWeb currentWeb, SPUser user, string sPermLevel)

        {

            SPGroup permissionsGroup = null;

 

            switch (sPermLevel)

            {

                case "Owner":

                    permissionsGroup = currentWeb.AssociatedOwnerGroup;

                    break;

                case "Visitor":

                    permissionsGroup = currentWeb.AssociatedVisitorGroup;

                    break;

                case "Member":

                    permissionsGroup = currentWeb.AssociatedMemberGroup;

                    break;

                default:

                    throw new NotImplementedException(string.Format("Group '{0}' not yet implemented.", sPermLevel));

                    break;

            }

 

            permissionsGroup.AddUser(user);

      }

 

        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)

        {

        }

 

        public override void FeatureInstalled(SPFeatureReceiverProperties properties)

        {

        }

 

        public override void FeatureUninstalling(SPFeatureReceiverProperties properties)

        {

        }

    }

}


Stepping through the code, we first find the 'authorized users' list, tell SharePoint we don't want to inherit permissions for the web being created, and then iterate through the list adding each user to the appropriate security group for the web as we find them. Note the SPWeb object has properties to allow you to easily reference the 'Owners', 'Visitors' groups etc. - these will be named in the form '[My site name] Owners' so this avoids you having to do any nasty string concatenation here.

In terms of how where this class fits alongside the rest of the files, I just store it in the same VS project. In this example I'm not using VSeWSS to create the Solution, but as I mentioned last time this can make things much simpler. I'm choosing not to here because I wanted to pass the list name using a Feature property, and in the current version VSeWSS does not have the flexibility to support this. In any case, having the class in the same VS project means that when the project is compiled, the receiver assembly is built and is output to the same project's bin directory. My .ddf file which is passed to makecab.exe then adds this dll and all the other files to the Solution package (.wsp) which is built for deployment. You may choose to use a post-build type solution (MSBuild, post-build script etc.) to automatically deploy this to your local environment on every compile, either by straight XCOPY or using the STSADM commands - my 'Building and deploying SharePoint Solution packages' article has more information on this. So my overall project structure looks like this:




So for deployment this means everything is in one package - on deployment the assembly hits the GAC before the Feature activation process runs, meaning the Feature receiver code is in place and will execute successfully. Once deployed, the site definition is available for use and new sites can be created from it. If we go ahead and create a site, if we look at the different security groups we see the appropriate users have been added according the configuration data we stored in the 'authorized users' list.

Owners group:



Visitors group:


So that's it! We now have our solution which enables us to 'package up' custom permissions with a site definition. Clearly we could store the permissions mappings in some other store such as a database table or XML file, but all things being equal I'm a big advocate of using SharePoint lists to store such data. The user interface is provided for you, and security can be applied to ensure standard users are unaware of the list's presence.

All the files I used can be downloaded from http://sharepointchris.googlepages.com/sitedefinitionwithcustompermissions.

In terms of using the files, you can follow this process to use the technique:

  • create the authorized users list from the list template I supply - 'PermissionsListTemplate.stp'
  • add your users and permission mappings to the list
  • add your site definition files to the appropriate places in the VS project, and modify the onet.xml file etc. as necessary
  • if files have been added, amend the .ddf file to include these and rebuild the Solution package
  • deploy the package using STSADM (a .bat file is included in the zip) and create sites from the definition!

Hopefully this series has been some use. While the approach is certainly useful in my scenario of rolling out a site definition used to create automatically secured sites from, in general terms you can use the technique to do any custom processing you want in the site creation process. If there are any queries please leave a comment!

13 comments:

Anonymous said...
This comment has been removed by a blog administrator.
Anonymous said...

A very good article!

The example code is not linked from http://sharepointchris.googlepages.com/sitedefinitionwithcustompermissions but can be downloaded directly from http://sharepointchris.googlepages.com/COB.Demos.SiteDefinitionWithCustomPe.zip

Theo said...

Chris

From a google search I came across this page and it seems close to what I need to do.

Unfortunately I do not have enough time (and knowledge of MOSS or .Net) to try this out before having an answer... but I suspect it might be possible.

I need to know if it is possible to add (as a feature or package) custom permissions, already mapped to predefined groups (I guess groups added to the 'new site') at site creation... does that make sense?

In short, these groups and permissions must be ready for use when the site is deployed. The customer only need to add users to the groups.

Please let me know whether one can do that or not.

Cheers
Theo

Chris O'Brien said...

Hi Theo,

Yes, it would definitely be possible to do what you want. I would suggest a good approach would be to use the technique in this article, but modify the code to apply your custom permissions.

Without looking at it in too much detail, I think you would need to do something like the following:

- find each list you wish to add custom permissions to
- then call SPList.BreakRoleInheritence()
- add custom groups using SPList.RoleAssignments.Add()

Have a look along those lines.

HTH,

Chris.

Ramesh Krishnan said...

Great Article. I've one question though in regards to the property usage from the feature. Do you need to add any serializable code for accessing dynamic values?

Chris O'Brien said...

Hi,

Not sure what you mean exactly - the code above shows retrieving values from Feature properties, but if you want to do something else, give me some more details and I'll try to understand/help.

Cheers,

Chris.

Unknown said...

This is an excellent article actually. I was trying to do this in WSS 3.0 for a while and after getting to a half solution I ran into this blog. Good stuff!

I have a question. I got everything running but I am getting an "Unknown error" right after I click "Create" from the UI to create a site. For some reason when I run the code, the Groups "Member" and "Owner" do not exist yet and permissionGroup ends up being "null" (although "Visitors" is present). This is causing an "Object reference not set to an instance of an object" exception which is causing the "Unknown Error" on the front end. Am I missing some configuration?

Chris O'Brien said...

Hi Elton,

Sorry for the delay in replying. This seems an unusual one - are you definitely using the provided properties to access the 'owner' and 'member' groups (as shown in my code)? If so, I'm not sure why these would not yet be available - there is no configuration other than what I discuss in the article.

Do you get the same effect in different environments?

Cheers,

Chris.

Anonymous said...

Elton,

I'm having similar fun with WSS3.0. I've been testing using a Feature and a class inheriting from SPWebProvisioningProvider to create site collections from scratch.

Both the SPWebProvisioningProvider Provision method and the Feature Activation method occur before Sharepoint has created any groups for the new site collection.

You can add your own custom groups to the collection and set them as the Associated Groups but Sharepoint will continue to create it's own [siteName] Owners, Members and Visitors groups.

Unknown said...

Hi Chris, I hope you don't mind but i found your blog and I thought "YES!!!" so I am implementing the Feature as described, but I get the following errors in the MOSS log (my permission list is in the parent web). I used the permLevels Owner, Visitor and Member as detailed in the code example. I am puzzled about what could be happening - it looks like the list name is not found (?)- Log Entries: "07/25/2008 16:22:09.45 w3wp.exe (0x1460) 0x1BE8 Windows SharePoint Services Feature Infrastructure 88jm High Feature receiver assembly 'ClientInfo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=add2ef901e07e5b1', class 'ClientInfo.SiteProvisioning', method 'FeatureActivated' for feature '7c020fff-ff42-4fe2-8a9b-9bca0d5f8001' threw an exception: System.ArgumentException: Value does not fall within the expected range. at Microsoft.SharePoint.SPListCollection.GetListByName(String strListName, Boolean bThrowException) at Microsoft.SharePoint.SPListCollection.get_Item(String strListName) at ClientInfo.SiteProvisioning.FeatureActivated(SPFeatureReceiverProperties properties) at Microsoft.SharePoint.SPFeature.DoActivationCallout(Boolean fActivate, Boolean fForce)

07/25/2008 16:22:09.45 w3wp.exe (0x1460) 0x1BE8 Windows SharePoint Services General 72by High Feature Activation: Threw an exception, attempting to roll back. Feature 'SiteProvisioning' (ID: '7c020fff-ff42-4fe2-8a9b-9bca0d5f8001'). Exception: System.ArgumentException: Value does not fall within the expected range. at Microsoft.SharePoint.SPListCollection.GetListByName(String strListName, Boolean bThrowException) at Microsoft.SharePoint.SPListCollection.get_Item(String strListName) at ClientInfo.SiteProvisioning.FeatureActivated(SPFeatureReceiverProperties properties) at Microsoft.SharePoint.SPFeature.DoActivationCallout(Boolean fActivate, Boolean fForce) at Microsoft.SharePoint.SPFeature.Activate(SPSite siteParent, SPWeb webParent, SPFeaturePropertyCollection props, Boolean fForce) "

Chris O'Brien said...

Hi Mark,

Well it definitely seems that the code isn't finding the list to apply the permissions to.

Are you retrieving it from a Feature property as in my example? If so, did you update the value in the XML to match your list name? Otherwise, suggest stepping through with the debugger to see if there's some other kind of typo.

HTH,

Chris.

Anonymous said...

Hello Chris
First of all thanks for the article. This has helped us a lot in moving in the right direction to create a seamless site creation process. We followed the steps you have provided so that the custom definition (when selected from the create site screen) assigns the users a custom full control permission level for owners, custom contribute permission level for members and custom read permission level for visitors. Since we are breaking inheritance on the code, we would like to find out
a. if it is possible to only allow users to select use unique permissions on the newsbweb.aspx.
b. If it is possible to completely override the next page shown to the users so we directly get to the new site page as it does not serve much use since the groups have already been created as part of the site definition.

Again i appreciate the steps you detailed.

Thanks
- Arvind.

Chris O'Brien said...

Hi Arvind,

Glad you found it useful.

The changes you mention could be slightly more complex. I guess for the first one, you could look at doing something with JavaScript on that page (this would not be supported however!), or write your code so that this setting is always overwritten regardless of what the user selected in the control.

With regards to the second, you might have some luck if you use Reflector to find out where SharePoint gets that URL to redirect to.

Best of luck,

Chris.