Open Sourcing of the ASP.NET Membership PowerShell Provider

§ July 27, 2010 01:41 by beefarino |

Over the past year I've blogged and presented about the benefits of targeting PowerShell as a framework for supporting the applications you develop.  I firmly believe PowerShell is the most appropriate choice of platforms for creating interactive and flexible toolsets.  Today I'm proud to announce that one of the original projects that led to this belief - the ASP.NET Membership PowerShell Provider - is being released by Code Owls LLC as open source.

You can find the project hosted on CodePlex here.

My reasons for this don't really center around wanting to share the code.  That is, I've already written detailed blogs about creating this particular provider, and plan to round those out with a few more posts:

So in my mind, the code is already public.  The primary reason I wanted to get this project public and posted was to get people using it and contributing to the project.  At the moment, the glaring omission is Active Directory support, and this is where I need the most help since I don’t have ready access to an Active Directory environment.  If you’re interested in helping out, by all means contact me through this blog or through the CodePlex project page.

I realize this project may be a bit niche, but its a niche is begging to be filled.  The existing Membership toolset is atrocious, and the applicable PowerShell offering is robust, interactive, and full of chewy goodness.

Enjoy!



CodeStock 2010: PowerShell as a Tools Platform

§ July 4, 2010 01:54 by beefarino |

At long last, I'm home from my working vacation and have a chance to do some CodeStock postprocessing.  Several people have asked me for the resources from my PowerShell presentation.  You'll find downloadable RARs of the powerpoint and code below.  I've also placed the deck on SlideShare for convenience:

The bulk of the code is described in the following posts:

I will be adding a few posts soon to round out the code coverage.  Feel free to drop me any questions or concerns you have.  I'd love the chance to give this talk to any .NET user groups in the area!

ASPNETMembership.rar (284.35 kb)

PowerShell as a Tools Platform.rar (1.17 mb)



Creating a PowerShell Provider pt 3: Getting Items

§ July 7, 2009 04:51 by beefarino |

Now that the drive object of the ASP.NET Membership PowerShell provider is fully functional, it's time to extend the PowerShell provider to fetch users from the membership store.

The PowerShell cmdlet used to retrieve one or more items from a provider is get-item, or gi for you alias monkeys out there.  get-item works against any provider that support item retrieval; e.g., it works for files and folders:

> get-item c:\temp
    Directory: Microsoft.PowerShell.Core\FileSystem::C:\
Mode           LastWriteTime       Length Name
----           -------------       ------ ----
d----     3/12/2009  2:23 PM        <DIR> temp

It works for environment variables:

> get-item env:PROGRAMFILES
Name                           Value
----                           -----
ProgramFiles                   C:\Program Files

It works for certificates:

> get-item cert:/CurrentUser/My/E0ADE6F1340FDF59B63452D067B91FFFA09A621F
    Directory: Microsoft.PowerShell.Security\Certificate::CurrentUser\My
Thumbprint                                Subject
----------                                -------
E0ADE6F1340FDF59B63452D067B91FFFA09A621F  E=jimbo@null.com, CN=127.0.0.1, OU=sec, O=ptek, L=here, S=nc, C=US

So, how does PowerShell know which provider to invoke when it processes a get-item cmdlet?  

PowerShell Paths

The item's full path contains all of information necessary to locate the appropriate provider.  In the examples above, the drive portion of the path indicates which provider should be used to process the item request; you can see the mapping of drives to providers using the get-psdrive cmdlet:

> get-psdrive
Name       Provider      Root
----       --------      ----       
Alias      Alias
C          FileSystem    C:\   
cert       Certificate   \
Env        Environment
Function   Function
Gac        AssemblyCache Gac
HKCU       Registry      HKEY_CURRENT_USER
HKLM       Registry      HKEY_LOCAL_MACHINE
Variable   Variable

Each PowerShell drive is directly associated with a provider type: the C: drive maps to the FileSystem provider, the cert: drive to the Certificate provider, and the env: drive to the Environment provider.

PowerShell recognizes several forms of path syntax, all of them designed to allow for provider discovery; providers are expected to support these path formats as appropriate:

  • Drive-qualified: this is equivalent to a fully-qualified file path.  The drive is explicitly specified at the start of the path, as in "c:/temp/junk.txt" or "cert:/localmachine".  
  • Provider-direct: starts with "\\" (or "//"); the provider for the current location (i.e., the value of $pwd) is assumed to be the provider of the path. This syntax is often used to identify resources on other machines, such as UNC paths or remote registry hives.
  • Provider-qualified: the provider name is prepended to the drive-qualified or provider-direct item path, delimited by '::'.  E.g., "FileSystem::c:/temp/junk.txt", or "FileSystem::\\share\temp\junk.txt".  This format is used when the appropriate provider must be explicity stated.
  • Provider-internal: this is the portion of the provider-qualified path following the '::' delimiter. 

Of the four supported path formats, the ASP.NET Membership PowerShell provider will support these three:

  • Drive-qualified: users:/username
  • Provider-qualified: ASPNETMembership::users:/username
  • Provider-internal: this is idential to the drive-qualified path syntax

Provider-direct paths and UNC-style provider-internal paths will not be supported by the ASP.NET Membership PowerShell provider.

Knowing the path formats to expect, it's time to implement support for the get-item cmdlet.

Enabling Get-Item

Enabling item cmdlet support for the ASP.NET Membership PowerShell provider begins with deriving the provider from ContainerCmdletProvider:

using System.Management.Automation;
using System.Management.Automation.Provider;
namespace ASPNETMembership
{
    [CmdletProvider( "ASPNETMembership", ProviderCapabilities.None )]
    public class Provider : ContainerCmdletProvider
    {
        // ...
    }
}

Deriving from ContainerCmdletProvider adds many item-related methods to the provider.  To enabling the get-item cmdlet, at least two of these methods must be overridden. 

GetItem

The first required override is the GetItem method, which is called to process a get-item invocation at runtime:

protected override void GetItem( string path )
{
    var user = GetUserFromPath(path);
    if( null != user )
    {
        WriteItemObject( user, path, false );
    }
}

The GetItem override delegates almost all of the work to the GetUserFromPath utility method; if GetUserFromPath returns a valid object reference, it is written back to the current pipeline using the WriteItemObject method of the provider object's base (line 6).

GetUserFromPath uses the provider's custom drive object to access the ASP.NET Membership provider.  The drive object for the path is available in the PSDriveInfo property; PowerShell conveniently sets this value to the appropriate DriveInfo object for the item's path before calling GetItem:

MembershipUser GetUserFromPath( string path )
{
    var drive = this.PSDriveInfo as MembershipDriveInfo;
    var username = ExtractUserNameFromPath( path );
    return drive.MembershipProvider.GetUser( username, false );
}
static string ExtractUserNameFromPath( string path )
{
    if( String.IsNullOrEmpty( path ) )
    {
        return path;
    }
    // this regex matches all supported powershell path syntaxes:
    //  drive-qualified - users:/username
    //  provider-qualified - membership::users:/username
    //  provider-internal - users:/username
    var match = Regex.Match( path, @"(?:membership::)?(?:\w+:[\\/])?(?<username>[-a-z0-9_]*)$", RegexOptions.IgnoreCase );
    if( match.Success )
    {
        return match.Groups[ "username" ].Value;
    }
    return String.Empty;
}

The custom drive object exposes the ASP.NET Membership Provider, which offers a GetUser method that returns the MembershipUser object for a valid username (line 5).  The username is extracted from the path string using a simple regular expression that matches the three path formats supported by the PowerShell provider (line 17).

ItemExists

The second required override is the ItemExists method, which is called by PowerShell to determine if the provider contains an item at a specified path (e.g., by the test-path cmdlet).

PowerShell calls ItemExists before the GetItem method when processing get-item; if ItemExists returns false, GetItem is not called and a "cannot find path" error is reported on the pipeline.  The ASP.NET Membership provider reuses the GetUserFromPath utility method to ascertain whether the path contains a valid username:

protected override bool ItemExists( string path )
{
    return null != GetUserFromPath( path );
} 

With these two overrides and their supporting utility methods, our provider can support the get-item cmdlet.

Testing Get-Item

Build and run; in the PowerShell console, create the users drive as follows:

> new-psdrive -psp aspnetmembership -root "" -name users -server localhost -catalog awesomewebsitedb; 
Name       Provider      Root                                   CurrentLocation
----       --------      ----                                   ---------------
users      ASPNETMemb... 

Once the drive is created, you can use get-item to fetch MembershipUser objects from the ASP.NET Membership user store:

> get-item users:/testuser
PSPath                  : ASPNETMembership::testuser
PSDrive                 : users
PSProvider              : ASPNETMembership
PSIsContainer           : False
UserName                : testuser
ProviderUserKey         : 09a9c356-a400-4cff-825d-231207946c94
Email                   : user@hotmail.com
PasswordQuestion        : what is your favorite color?
Comment                 :
IsApproved              : True
IsLockedOut             : False
LastLockoutDate         : 12/31/1753 7:00:00 PM
CreationDate            : 6/11/2009 12:59:45 PM
LastLoginDate           : 6/11/2009 12:59:45 PM
LastActivityDate        : 6/11/2009 12:59:45 PM
LastPasswordChangedDate : 6/11/2009 12:59:45 PM
IsOnline                : False
ProviderName            : AspNetSqlMembershipProvider

At this point, a whole new world of complexity is available from our provider:

> ( get-item users:/testuser ).ResetPassword( 'asdf1234' )
^PlpmNMON@7A]w

We can also leverage some of the built-in goodies of PowerShell against our ASP.NET Membership store in a natural way:

$u = get-item users:/testuser;
if( $u.IsLockedOut ) 
{ 
    $u.UnlockUser(); 
}  

Pretty cool.

Coming Up

Discovery is a big part of PowerShell, and in the post I'll extend the ASP.Net Membership PowerShell provider to support the get-childitem (alias dir or ls) cmdlet, to enable listing of all users in the store.  I'll also add support for the set-location (alias cd) cmdlet, which will allow operators to set the shell's current location to our custom users drive.

The code for this post is available here: ASPNETMembership_GetItem.zip (5.55 kb)

As always, thanks for reading, and if you liked this post, please Kick It, Shout It, trackback, tweet it, and comment using the clicky thingies below!



Creating a PowerShell Provider pt 2: Parameters and Default Drives

§ June 11, 2009 16:26 by beefarino |

My previous post in this series discussed the first major step in developing a PowerShell provider: creating a custom drive object.  This post rounds out the discussion by making the drive configurable with custom initialization parameters.  In addition, I'll demonstrate how a provider can create one or more "default" drives without user intervention.

This post builds on code from the previous post, which you can download from here.  The completed code for this post is also available.

Parameterizing the Drive

At the moment, the ASP.NET Membership provider configuration lives in code; that should really change so this drive can be used with other providers or for multiple sites.  Such drive configurability is gained by supporting the NewDriveDynamicParameters method of the PowerShell provider.

The NewDriveDynamicParameters method is called each time the new-psdrive cmdlet is invoked to allow the provider to add drive creation parameters.  Although the MSDN documentation of this method isn't terribly helpful, implementing it is pretty simple.  All you need to do is return an object containing properties or fields for each custom parameter.  

First, we need to know what parameters are needed to create the ASP.NET Membership provider.  Take a quick look at the configuration items hard-coded in the drive creation logic:

// DriveInfo.cs
public MembershipDriveInfo( PSDriveInfo driveInfo )
    : base( driveInfo )
{
    var connectionStrings = ConfigurationManager.ConnectionStrings;
 
    var fi = typeof( ConfigurationElementCollection )
        .GetField( "bReadOnly", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic );
    fi.SetValue( connectionStrings, false );
 
    connectionStrings.Add(
        new ConnectionStringSettings(
            "ProviderConnectionString",
            "data source=localhost;Integrated Security=SSPI;Initial Catalog=AwesomeWebsiteDB"
        )
    );
 
    provider = new SqlMembershipProvider();
    var nvc = new System.Collections.Specialized.NameValueCollection
    {
        { "connectionStringName", "ProviderConnectionString" },
        { "enablePasswordRetrieval", "false" },
        { "enablePasswordReset", "true" },
        { "requiresQuestionAndAnswer", "false" },
        { "requiresUniqueEmail", "false" },
        { "passwordFormat", "Hashed" },
        { "maxInvalidPasswordAttempts", "5" },
        { "minRequiredPasswordLength", "6" },
        { "minRequiredNonalphanumericCharacters", "0" },
        { "passwordAttemptWindow", "10" },
        { "passwordStrengthRegularExpression", "" },
        { "applicationName", "/" },
    };
    provider.Initialize( "AspNetSqlMembershipProvider", nvc );
}

There are a number of parameters used to initialize the ASP.NET membership provider.  Based on the code, I would want to configure any of the following facets of the ASP.NET Membership provider:

  • the Connection String server and databse name;
  • EnablePasswordRetrieval;
  • EnablePasswordReset;
  • RequiresQuestionAndAnswer;
  • RequiresUniqueEmail;
  • PasswordFormat;
  • MaxInvalidPassordAttempts;
  • MinRequiredPasswordLength;
  • MinRequiredNonalphanumericCharacters;
  • PasswordAttemptWindow;
  • PasswordStrengthRegularExpression;
  • ApplicationName.

While all of these should be configurable, assumptions can be made for most of them. 

The first step towards implementing the NewDriveDynamicParameters method is to define an object containing read/write properties for each configurable parameter:

// DriveParameters.cs
using System;
using System.Management.Automation;
using System.Web.Security;
namespace ASPNETMembership
{
    public class DriveParameters
    {
        public DriveParameters()
        {
            EnablePasswordReset = true;
            EnablePasswordRetrieval = false;
            RequiresQuestionAndAnswer = false;
            RequiresUniqueEmail = false;
            MaxInvalidPasswordAttempts = 5;
            MinRequiredNonalphanumericCharacters = 0;
            MinRequiredPasswordLength = 6;
            PasswordAttemptWindow = 10;
            PasswordStrengthRegularExpression = String.Empty;
            ApplicationName = "/";
            PasswordFormat = MembershipPasswordFormat.Hashed;
        }
 
        [Parameter( Mandatory=true )]       
        public string Server { get; set; }        
 
        [Parameter( Mandatory = true )]
        public string Catalog { get; set; }
 
        public bool EnablePasswordRetrieval { get; set; }
        public bool EnablePasswordReset { get; set; }
        public bool RequiresQuestionAndAnswer { get; set; }
        public bool RequiresUniqueEmail { get; set; }
        public MembershipPasswordFormat PasswordFormat { get; set; }
        public int MaxInvalidPasswordAttempts { get; set; }
        public int MinRequiredPasswordLength { get; set; }
        public int MinRequiredNonalphanumericCharacters { get; set; }
        public int PasswordAttemptWindow { get; set; }
        public string PasswordStrengthRegularExpression { get; set; }
        public string ApplicationName { get; set; }
    }
}

Note the use of the System.Management.Automation.ParameterAttribute on lines 24 and 27 to identify the Server and Catalog parameters as required (Mandatory=true).  The remaining optional parameters are initialized to default values in the constructor.

With the DriveParameters type defined, the NewDriveDyanmicParameters method of the ASP.NET Membership provider can now be implemented:

// Provider.cs
protected override object NewDriveDynamicParameters()
{
    return new DriveParameters();
}

PowerShell will call this method each time the new-psdrive cmdlet is invoked for this provider.  The object returned to PowerShell is filled with parameter values and passed back to the provider's NewDrive method via the DynamicParameters property; in the Membership PowerShell provider, NewDrive simply passes the parameters to the MembershipDriveInfo constructor:

// Provider.cs
protected override System.Management.Automation.PSDriveInfo NewDrive( PSDriveInfo drive )
{
    var driveParams = this.DynamicParameters as DriveParameters;
    return new MembershipDriveInfo(drive, driveParams);
}

which must then act on the parameters when initializing the ASP.NET Membership provider:

// DriveInfo.cs
using System;
using System.Configuration;
using System.Management.Automation;
using System.Web.Security;
namespace ASPNETMembership
{
    public class MembershipDriveInfo : PSDriveInfo
    {
        MembershipProvider provider;
        public MembershipDriveInfo( PSDriveInfo driveInfo, DriveParameters parameters )
            : base( driveInfo )
        {
            var connectionStrings = ConfigurationManager.ConnectionStrings;
 
            var fi = typeof( ConfigurationElementCollection )
                .GetField( "bReadOnly", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic );
            fi.SetValue( connectionStrings, false );
 
            var connectionString = String.Format(
                "data source={0};Integrated Security=SSPI;Initial Catalog={1}",
                parameters.Server,
                parameters.Catalog
                );
 
            var moniker = Guid.NewGuid().ToString("N");
            var connectionStringName = "cxn_" + moniker;
            var providerName = "pvdr_" + moniker;
 
            connectionStrings.Add(
                new ConnectionStringSettings(
                    connectionStringName,
                    connectionString
                )
            );
 
            provider = new SqlMembershipProvider();
            var nvc = new System.Collections.Specialized.NameValueCollection
            {
                { "connectionStringName", connectionStringName },
                { "enablePasswordRetrieval", parameters.EnablePasswordRetrieval.ToString() },
                { "enablePasswordReset", parameters.EnablePasswordReset.ToString() },
                { "requiresQuestionAndAnswer", parameters.RequiresQuestionAndAnswer.ToString() },
                { "requiresUniqueEmail", parameters.RequiresUniqueEmail.ToString() },
                { "passwordFormat", parameters.PasswordFormat.ToString() },
                { "maxInvalidPasswordAttempts", parameters.MaxInvalidPasswordAttempts.ToString() },
                { "minRequiredPasswordLength", parameters.MinRequiredPasswordLength.ToString() },
                { "minRequiredNonalphanumericCharacters", parameters.MinRequiredNonalphanumericCharacters.ToString() },
                { "passwordAttemptWindow", parameters.PasswordAttemptWindow.ToString() },
                { "passwordStrengthRegularExpression", parameters.PasswordStrengthRegularExpression },
                { "applicationName", parameters.ApplicationName },
            };
            provider.Initialize( providerName, nvc );
        }
        
        public MembershipProvider MembershipProvider
        {
            get
            {
                return this.provider;
            }
        }
    }
}

Not a lot of new code here; the constructor now accepts an additional DriveParameters argument that is used to initialize the connection string and membership provider configuration.  Using dynamically-generated connection string and provider names prevents naming collisions if more than one drive is initialized by the provider (lines 26-28).

Using the Drive Parameters

Build and run. The new dynamic drive parameters can be specified directly to the new-psdrive cmdlet just like any other cmdlet parameter:

> new-psdrive -psprovider aspnetmembership -name users -root '' 
    -server localhost -catalog AwesomeWebsiteDB 
    -MinRequiredPasswordLength 8 -PasswordAttemptWindow 4 
 
Name       Provider      Root                                   CurrentLocation
----       --------      ----                                   ---------------
users      ASPNETMemb...

Note that PowerShell prompts the user for the any required parameters if they aren't supplied:

> remove-psdrive users
> new-psdrive -psprovider aspnetmembership -name users -root ''
cmdlet New-PSDrive at command pipeline position 1
Supply values for the following parameters:
Catalog: 
> AwesomeWebsiteDB
Server:
> localhost
 
Name       Provider      Root                                   CurrentLocation
----       --------      ----                                   ---------------
users      ASPNETMemb...

Creating Default Drives

Another feature available to PowerShell providers is the ability to initialize a set of default drives when the provider assembly is first loaded.  This is accomplished by overriding the provider's InitializeDefaultDrives method to return a list of PSDriveInfo objects describing the drives to create:

// Provider.cs
 
protected override Collection<PSDriveInfo> InitializeDefaultDrives()
{
    var driveInfo = new PSDriveInfo(
        "users",
        this.ProviderInfo,
        "",
        "Default ASP.NET Membership Drive",
        null
        );
    
    var parameters = new DriveParameters
                     {
                         Catalog = "AwesomeWebsiteDB",
                         Server = "localhost"
                     };
 
    return new Collection<PSDriveInfo>
           {
               new MembershipDriveInfo(
                   driveInfo,
                   parameters
                   )
           };
}

Each PSDriveInfo object in the returned collection will then be passed to the NewDrive method of the provider to complete the drive creation (the NewDriveDynamicParameters method is not called in this case).  As such, the ASP.NET Membership PowerShell provider's NewDrive method must be modified to acommodate pre-initialized drives:

protected override PSDriveInfo NewDrive( PSDriveInfo drive )
{
    // see if the drive has already been initialized
    //    e.g., via InitializeDefaultDrives()
    if( drive is MembershipDriveInfo )
    {
        return drive;
    }
 
    var driveParams = this.DynamicParameters as DriveParameters;
    return new MembershipDriveInfo(drive, driveParams);
}

Testing Default Drive Creation

Build and run; you can verify that the default drive was created using the get-psdrive cmdlet:

> get-psdrive
 
Name       Provider      Root                                   CurrentLocation
----       --------      ----                                   ---------------
...
users      ASPNETMemb...

No need to use the new-psdrive cmdlet - the users drive is created automagically as PowerShell loads the provider assembly.

Coming Up

That's about it for drives; for here on in, the focus will be on making the provider work with all the features available in PowerShell.  The next post will add support for the get-item cmdlet and discuss the various flavors of item paths the provider must support.

The code for this post is available here:

ASPNETMembership_pt1_drives.zip (4.26 kb) 

As always, thanks for reading, and if you liked this post, please Kick It, Shout It, trackback, tweet it, and comment using the clicky thingies below!