Tuesday, November 27, 2007

Implementing a New Authorization Provider

Membership Provider Setup

Tools Needed and other Assumptions

The following is assuming you have a working Central Administration Site and Website you wish to apply the custom membership provider to. You will also need access to the latest of the following files:

  • Security.dll (Custom Membership and Role Provider)

In implementing the custom membership provider we are also assuming that you will have access to the custom provider database. Please note anything bold and red is configurable.

We also must note that these instructions apply to the changes that need to be made to the MOSS Server and will need to be done on each WFE that you wish to host the custom membership on.

Once you have completed the following directions to configure the custom provider for your site, the Sharepoint Designer application will not be able to connect to it. For this reason, I recommend you extend your web application to the "intranet" zone with AD credentials and let the designers know the URL to connect to for Sharepoint Designer.

Please keep in mind that without the SP1 of the .NET 2.0 Framework, the below will throw http Cookie exceptions in the Event viewer. If you cannot implement the Service Pack, please change CacheRolesInCookie to equal False.

GAC

The first thing we will do is GAC the following files:

  • Security.dll

Machine.config

We are now going to modify the machine.config located at C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG\machine.config. We will make the following entries:

<membership>

<providers>

    <add name="sqlmembershipproviderName" type="Security.sqlmembershipproviderName, Security, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d003b67fe3e7980d" />

     <add name="AspNetSqlMembershipProvider" type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" connectionStringName="LocalSqlServer" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="true" applicationName="/" requiresUniqueEmail="false" passwordFormat="Hashed" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="7" minRequiredNonalphanumericCharacters="1" passwordAttemptWindow="10" passwordStrengthRegularExpression="" />

</providers>

</membership>

<profile>

<providers>

<add name="AspNetSqlProfileProvider" connectionStringName="LocalSqlServer" applicationName="/" type="System.Web.Profile.SqlProfileProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />

</providers>

</profile>

<roleManager>

<providers>

    <add name="sqlroleproviderName" type="Security.sqlroleproviderName, Security, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d003b67fe3e7980d" connectionStringName="OdbcServices" applicationName="sqlmembershipproviderName" />

<add name="AspNetSqlRoleProvider" connectionStringName="LocalSqlServer" applicationName="/" type="System.Web.Security.SqlRoleProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />

<add name="AspNetWindowsTokenRoleProvider" applicationName="/" type="System.Web.Security.WindowsTokenRoleProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />

</providers>

</roleManager>


 

These entries will take the place of the existing <Membership>, <Profile>, and <Role Manager> tags.

Central Administration Web.config

Add the Membership and Role Providers directly under <System.Web>

<membership defaultProvider="sqlmembershipproviderName">

<providers>

<remove name="sqlmembershipproviderName" />

<add connectionStringName="LocalSqlServer"

passwordAttemptWindow="10"

enablePasswordRetrieval="false"

enablePasswordReset="true"

requiresQuestionAndAnswer="true"

applicationName="sqlmembershipproviderName"

requiresUniqueEmail="false"

passwordFormat="Hashed"

description="Stores and retrieves membership data from the Microsoft SQL Server database"

name="sqlmembershipproviderName"

type="Security.sqlmembershipproviderName, Security, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d003b67fe3e7980d" />

</providers>

</membership>

<roleManager defaultProvider="AspNetWindowsTokenRoleProvider"

enabled="true"

cacheRolesInCookie="true"

cookieName=".ASPROLES"

cookieTimeout="30"

cookiePath="/"

cookieRequireSSL="false"

cookieSlidingExpiration="true"

cookieProtection="All" >

<providers>

</providers>

</roleManager>


 

Replace the <PeoplePickerWildcards/> with the following keys

<PeoplePickerWildcards>

<clear />

<add key="sqlmembershipproviderName" value="%" />

<add key="sqlroleproviderName" value="%" />

<add key="AspNetSqlMembershipProvider" value="%" />

</PeoplePickerWildcards> 


 

Add the following directly under </appsettings>

<connectionStrings>

<add name="SQLDB"

connectionString="Data Source=SQLServer;Initial Catalog=WSS_Membership;User ID=sa; Password=password"

providerName="System.Data.SqlClient" />

</connectionStrings>


 

Web Application Web.config

Add the Membership and Role Providers directly under <System.Web>

<membership defaultProvider="sqlmembershipproviderName">

<providers>

<remove name="sqlmembershipproviderName" />

<add connectionStringName="LocalSqlServer" passwordAttemptWindow="10" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="true" applicationName="sqlmembershipproviderName" requiresUniqueEmail="false" passwordFormat="Hashed" description="Stores and retrieves membership data from the Microsoft SQL Server database" name="sqlmembershipproviderName" type="Security.sqlmembershipproviderName, Security, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d003b67fe3e7980d" />

</providers>

</membership>

<roleManager defaultProvider="sqlroleproviderName" enabled="true" cacheRolesInCookie="true" cookieName=".ASPROLES" cookieTimeout="30" cookiePath="/" cookieRequireSSL="false" cookieSlidingExpiration="true" cookieProtection="All">

<providers>

</providers>

</roleManager>


 

Replace the <PeoplePickerWildcards/> with the following keys

<PeoplePickerWildcards>

<clear />

<add key="sqlmembershipproviderName" value="%" />

<add key="sqlroleproviderName" value="%" />

<add key="AspNetSqlMembershipProvider" value="%" />

</PeoplePickerWildcards> 


 

Add the following directly under </appsettings>

<connectionStrings>

<add name="SQLDB" connectionString="Data Source=SQLServer;Initial Catalog=WSS_Membership;User ID=sa; Password=password" providerName="System.Data.SqlClient" />

</connectionStrings>


 

Set Authentication Provider for Web Application

Log in to your central administration site and go to Application Management à Authentication Providers. This can be accessed at http://ServerName/_admin/authenticationproviders.aspx.


Make sure that you are in the right web application that you wish to implement the provider for.

Click on the default zone which should be the only one listed at this moment.


Choose forms authentication type and type in [membershipproviderName],[sqlroleproviderName] in the appropriate boxes. Choose Save.

Set Policy Administrator for your web application

Since you have now changed the authentication for your web application, no one can actually log in quite yet. What you want to do is set the administrator for the account. I do not recommend setting more than one or two at this level.

Log in to your central administration site and go to Application Management à Policy for Web Application. This can be accessed at http://ServerName/_admin/policy.aspx.


Click on "Add Users" and choose Default zone


Choose Next

Use the People Picker to set your administrator


Choose Full Control and click on Finish.

Designer Changes Best Practices

Purpose

This document is going outline the Microsoft Best Practices regarding designer changes.


 

Introduction

So we all know that objects in a production environment should follow some pretty fundamental principles. Some of these would be objects should be source controlled, objects should go through test before deployment to production, objects should be performant to the best of their ability and so on.


 

With code this is pretty well documented. Use Source Safe, VSTS or some other Source Control system, create drop zones and move from environment to environment. Even the more complex GAC procedures and the like are very well documented and have several approaches to accomplish the environment issues.


 

Unfortunately, this is not the case for designer changes.


 

What are designer changes?

Designer changes consist of items such as but not limited to:

  • Master Pages
  • Layouts
  • Cascading Style Sheets
  • Themes


 

Feature Overview

To understand the solution is to understand MOSS features. The following is quote from Microsoft Office SharePoint Server 2007:


 

"New to the Microsoft Office SharePoint Server 2007 architecture, Features offer flexibility

in terms of developing and deploying extended functionality—such as page templates,

lists, content types, Web Parts, workflow, and events—to new and existing Office Share-

Point Server 2007 sites. By default, SharePoint Server 2007 includes prepackaged Features

as part of its base installation, such as a My Site Feature. The Feature framework has

been extended to allow developers to create custom Features.

If you worked with site definitions in SharePoint Portal Server 2003, you'll appreciate the

flexibility of Features! With SharePoint Portal Server 2003, if you wanted to add a list or

document library to an existing site definition, you had to work with one large

ONET.XML file to modify the XML code and then track each of those changes throughout

the ONET.XML file. Likewise, if you wanted to add items to the SharePoint toolbars

or menus, you had to work with complex Collaborative Application Markup Language

(CAML). Features overcome the complexity of injecting such changes by chunking code

into smaller, more manageable files, which can be more easily tracked, versioned, and

deployed.

Both developers and administrators will benefit from using Features throughout a Share-

Point Server 2007 deployment. Through Feature schemas, developers can scope and add

simple changes, such as provisioning new pages to a SharePoint site, or registering and

deploying complex solutions developed in Microsoft Visual Studio 2005, such as event

handlers or workflows. Developers can also work with the SharePoint object model,

which includes Feature classes to effect changes throughout the life cycle of Features.

Examples of these changes include whether certain actions or events occur when a Feature

is installed, activated, deactivated, or uninstalled. In addition, administrators can

install and deploy Features with ease, using command-line tools, and will have at their

fingertips the ability to switch Features on or off via the activate and deactivate options on

administrative user-interface pages"


 

The benefits of using features in this context are as follows:

  • Ability to leverage a rich source control management solution: Because everything is now on the file system, this is no different than your non-SharePoint ASP.NET 2.0 Web projects, or class libraries, etc... they are just files on the file system so you can easily add them to your source control management system of choice! This is HUGE!
  • Ability to package up the feature (and all it's files) in a SharePoint solution: See where I'm going? If you package up your feature and it's files into a SharePoint solution file (*.WSP) which you can then use to...
  • Deploy to all SharePoint web front end (WFE) servers in your farm at one time: Solution deployment can be scheduled to run at a future time... and the best part: SharePoint's solution framework will automatically deploy the feature to ~all~ your WFE's in the farm at that time. But that's not the only advantage of solutions...
  • Ability to retract deployed solutions: Ever had that "oh crap, we gotta get that off the servers NOW!" moment? The solution framework has the ability to yank a deployed solution back at a scheduled time as well (or immediately if you have the "oh crap" moment).
  • No need for developers to have access to your production environment: Since the files are in a feature that's packaged in a single solution (*.WSP) file, your production administrators can easily add the files to your production environment without developers having to open SharePoint Designer to make changes!
  • All files remain ghosted (uncustomized) on the file system: Remember, they are provisioned as Type=GhostableInLibrary which means the content doesn't really reside within the content database, they are on the file system. This is HUGE! This way...
  • Easier to make changes to existing files going forward: Everyone has changes to files once deployed into production. Using this mechanism, you can just upgrade the solution with an updated feature (don't change the solution or feature ID's) and every site that's activated the feature will automatically get the changes applied to their site! This is HUGE!

Deployment of Features

Now we move on to the technical implementation.


 

[Will follow up with actual technical details but essentially we will use stsadm with an upgrade flag to remove, and re-add these files]


 

Summary

The proper way to move these designer changes should be done utilizing feature deployment methodology in MOSS.

Monday, November 26, 2007

Writing your First Custom Authorization Provider

Intro

There are several people who are experts in the area of Custom Authorization Providers in MOSS. I want to thank each one of them who have helped me over the last year on my authorization provider. First and foremost I would like to thank Steve Peschka in relation to the base structure and more recently Mitch Prince both from MSFT. What I would like to demonstrate in this post is writing a brand new Custom Authentication Provider for MOSS for the beginner. I will make mention that in a different Post I will be addressing Site Minder integration with the custom provider, however, what you need to know here is that this provider will work regardless.

I will be utilizing a basic SQL table structure although you will see that it is not necessary, however, for simplicity sake, I figure it will be the easiest to demonstrate.

Creating the Data Structure

The first thing we are going to do is just set up some simple tables that we are going to store our users and roles (groups) in.

CREATE TABLE dbo.spUser(

    username VARCHAR (50) NOT NULL,

    email VARCHAR (200) NOT NULL,

)


 

GO


 

CREATE TABLE dbo.spRole(

    spRole VARCHAR (200) NOT NULL

)


 

GO


 

CREATE TABLE dbo.spRole_spUser(

    username VARCHAR (50) NOT NULL,

    spRole VARCHAR (200) NOT NULL,

)


 

Normally, you would add identities, primary keys, foreign keys with relationships etcetera but you get the point. We have a table to store users a table to store roles and an association between the two. Once again, this could be whatever data store you wish and have whatever other attributes like telephone number, password, hair color….

Understanding the Methods (Membership)

Now that we have the base data structure we are going to open up .NET and build our role Provider. In this case, I will be hooking each critical method up to a SQL Stored Procedure simply because I am more comfortable with this approach.

The first thing we will do is have an understanding of the methods and what they are used for. I will be only going over the necessary overloads that you must implement and correctly write for your provider to work. There will be several other properties and methods that you must override, but we will simply throw an exception on these.

public
override
void Initialize(string name, NameValueCollection config)


 

Initialize simply will set the properties of the provider. Usually these properties are set in the web.config and read in as a NameValueCollection.

public
override
bool ValidateUser(string name, string password)


 

The Validate User method simply takes in a user name and password and returns true or false based on your credential cache. Here is where you would authenticate. Your login page then would check this method first and based on the result do something.

public
override
MembershipUser GetUser(object userId, bool userIsOnline)


 

This Overload of the Get User method will simply return a Membership User object based on an object. The way that I normally implement this is to actually take the ID, and pass to the other Get User Method as a .ToString(). This way there is only one code base.

public
override
MembershipUser GetUser(string name, bool userIsOnline)


 

Here we will be getting the Membership User based on name. Use intellisense on the new MembershipUser(

public
override
string GetUserNameByEmail(string email)


 

I have searched and searched but cannot find where this method is actually called, however, easy enough to implement and so just get the string that you would pass to the GetUser method based on the email passed in.

public
override
MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out
int totalRecords)


 

This method is called on the people picker and will be hit when searching for a person. Simply pass in the email and set the totalRecords (you can do this with your MembershipCollection.Count property and you are good to go.

public
override
MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out
int totalRecords)


 

This method is also hit as part of the people picker. It is the most important people picker method in my opinion and you can write your own wildcard characters. So something like:

SELECT username, Email FROM spUser WHERE UserName LIKE %userNameToMatch%


 

The following methods we will not implement even though "must override" is on. We will set Booleans to false or throw not implemented exceptions here.

public
override
MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out
int totalRecords)

public
override
bool DeleteUser(string name, bool deleteAllRelatedData)

public
override
int GetNumberOfUsersOnline()

public
override
MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out
MembershipCreateStatus status)

public
override
bool ChangePasswordQuestionAndAnswer(string name, string password, string newPwdQuestion, string newPwdAnswer)

public
override
string GetPassword(string name, string answer)

public
override
bool ChangePassword(string name, string oldPwd, string newPwd)

public
override
string ResetPassword(string name, string answer)

public
override
void UpdateUser(MembershipUser user)

public
override
bool UnlockUser(string userName)

public
override
bool EnablePasswordRetrieval

public
override
bool EnablePasswordReset

public
override
bool RequiresQuestionAndAnswer

public
override
string ApplicationName

public
override
int MaxInvalidPasswordAttempts

public
override
int PasswordAttemptWindow

public
override
bool RequiresUniqueEmail

public
override
MembershipPasswordFormat PasswordFormat

public
override
int MinRequiredPasswordLength

public
override
int MinRequiredNonAlphanumericCharacters

public
override
string PasswordStrengthRegularExpression

Provision a New List with Data

This is going to be a first in a larger series related to the SharePoint provisioning concept that may be re-written in a more organized fashion, but I figured while it was on my mind, the prerogative of a blogger J

So as many of you may or may not have heard the way to deploy new "stuff" in SharePoint is by means of a feature deployments and this phantom thing called "provisioning". We will go over specific feature deployments in a later post but for now, I wanted to take the easy way and show you how to take the pre-built definitions for VSE and build a new list complete with data.

Let us say I want to create a new list that has lookup data in it (Wait for another post and you will be awed and amazed at how we will build on top of this listJ.

We will store Types of People in this list so I will split it up for now between Business Contacts and Personal Contacts. We will also store some other piece of information related to this obscure level of data. Let's make it Send Christmas Cards Boolean. I know that typically this would be stored at a contact level, but just appease me here. We'll assume that this will be a default and that individuals can override the base setting.

So essentially our list is going to look something like:

Contact Type

Send Christmas Cards

Business

FALSE

Personal

TRUE


 

Pretty easy so far right? It really is, however, as a colleague of mine recently stated: "this type of info seems kinda hard to find, like it is somekind of arcane tribal knowledge not-often shared with outsiders".

So first things first, let's go ahead and download the SharePoint Extensions for Visual Studio 2005 located at: http://www.microsoft.com/downloads/details.aspx?familyid=19F21E5E-B715-4F0C-B959-8C6DCBDC1057&displaylang=en.

Before we move on, I have to say if this is your first development experience inside of MOSS, you may want to read my article on MOSS Development 101.

Now open Visual Studio 2005 and create a new Project. You will notice some new cool templates when this occurs.


Pretty cool stuff yes? Now, in a later post, I will outline some of the stuff this will be doing for you behind the scenes but this is just basics right now.

You will get a pop-up when you choose ok with a drop-down and two checkboxes.


The drop-down outlines which SharePoint list you wish to inherit from. Depending on what you choose, we will get different fields pulled in. For purposes of this example we will be choosing "Custom List".

The first checkbox essentially is asking if you want to pre-populate this list with values. Note that these values can be changed once this list is provisioned or created in the site. For this example we definitely want to do this.

The second checkbox is asking if you want to write code when an event fires for this list. We are not going to do this for this list but we will be doing this in a later post.

After choosing ok on this window, Visual Studio will create all the necessary code to create a list and deploy this list in a solution package. We do, however, need to configure it the way we wish.

This will be done in two steps:

  1. Configure the Fields we want to use
  2. Configure the Data we want to pre-populate

For the first step, we are going to focus on the newly created schema.xml file.


Note specifically the <Fields> Section. Here is the actual:

        <Fields>

<Field
ID="{244AB5C6-102E-4355-905A-8A4C1F394C19}"
Name="ContactType"
DisplayName="Contact Type"
Type="Text"
Group="Sample Fields" />

<Field
ID="{BB365ADB-3B48-4bc5-B57C-9063226B5C31}"
Name="SendChristmasCards"
DisplayName="Send Christmas Cards?"
Type="Boolean"
Group="Sample Fields" />

        </Fields>


 

Notice the IDs. You may wonder where I got these. Well, truth be told, I made them up, or rather, I had Visual Studio do this for me.

Creating New GUIDs: To create new GUIDs, simply open up Visual Studio 2005, choose Tools, Create New Guid and select Registry setting. Then copy to clipboard and paste. Ta-da! Keep in mind that some items in SharePoint require the curly braces {} and some do not.


 

Now for step two (Creating the Data):

Here we are going to focus on the newly created instance.xml.


In this case, you will notice that we added the <Data> tags. We can at this point build and deploy. When building it will create a set.bat in the debug/release folder and you can modify this to point to the correct site.

Adding Users to a SharePoint List

Say you have a list and you wish to copy and paste from excel through the Grid-View Function. You will notice two things as it relates to the People and Groups Field.

  1. It will not take users that do not already exist in SharePoint
  2. It will not copy at all if the people and groups has the "Allow Multiple Selections" checkbox checked

Well now that's a bummer.

The following solution is just what I came up with:

What we are going to do is make a new field that will store a semi-comma delimited list and populate the users in there. We are then going to write some quick c# do take the data from that column and populate correctly across to the correct column.

The first function we will write will take in the SPWeb that you wish to do this for as well as the semi-comma delimited list.


//Return properly validated and added users


private SPFieldUserValueCollection UserValidation(SPWeb web, string Users)

{

SPFieldUserValueCollection userCollection = new SPFieldUserValueCollection();


string FormatedUsers = string.Empty;


string[] UserArray = Users.Split(';');


foreach (string sUser in UserArray)

{

SPUser user = null;


try

{

user = web.AllUsers[sUser];

}


catch { }


 


if (user == null)

{


try

{

web.AllUsers.Add(sUser, "", sUser, "");

web.Update();

user = web.AllUsers[sUser];

}


catch { }

}


 


if (user != null)

{

userCollection.Add(new SPFieldUserValue(web, user.ID, user.LoginName));

}

}


 


return userCollection;

}

}


 

The second function we will write will be to take the list, and actually update the list based on the previous method.

public
void UpdateMyList()

{


try

{


//Should do something like:


//string TopLevelSite = ConfigurationSettings.AppSettings["Site"];


string TopLevelSite = "http://r2-basemachine:1111";


using (SPSite SPSSite = new SPSite(TopLevelSite))

{


using (SPWeb sPWeb = SPSSite.OpenWeb())

{


string listName = "DatasheetTest";

SPList list = null;


DataTable dt = null;

SPListItemCollection listCol = null;


 

list = sPWeb.Lists[listName];

listCol = list.Items;

dt = listCol.GetDataTable();


 


foreach (DataRow dr in dt.Rows)

{

SPFieldUserValueCollection userCollection = new SPFieldUserValueCollection();

userCollection = UserValidation(sPWeb, dr["Multi"].ToString());

SPListItem ListItem = list.GetItemById((int)dr["Id"]);

ListItem["PersonField"] = userCollection;

ListItem.Update();

}

}

}

}


catch (Exception ex)

{


Console.WriteLine("Error: " + ex.ToString());

}

}


 

Happy coding!