The first step to creating a full-featured PowerShell provider is to be able to create a new drive. The goal of this post is to enable the most basic functional drive that provides access to the ASP.NET Membership user store. Before you begin, make sure you have the project set up properly as I describe here, or you can download the project pre-coded (3.24 kb).
We'll start by creating a minimal provider implementation. Add a class to the project named Provider and fill it in like so:
// Provider.cs
using System.Management.Automation;
using System.Management.Automation.Provider;
namespace ASPNETMembership
{
[CmdletProvider( "ASPNETMembership", ProviderCapabilities.None )]
public class Provider : DriveCmdletProvider
{
protected override PSDriveInfo NewDrive( PSDriveInfo drive )
{
return base.NewDrive( drive );
}
protected override object NewDriveDynamicParameters()
{
return base.NewDriveDynamicParameters();
}
}
}
A few items of note:
-
the Provider class derives from System.Automation.Provider.DriveCmdletProvider. All PowerShell providers derive from one of the abstract *CmdletProvider base classes. I'll discuss these classes a bit in the next post, but for now know that DriveCmdletProvider is the most basic base class you can use to create a provider.
-
the class is decorated with a System.Management.Automation.Provider.CmdletProviderAttribute. This attribute is required to get PowerShell to recognize your class as a provider when the assembly is loaded. In addition, the attribute identifies some of the capabilities the provider supports; more on this in a later post.
The class overrides two methods related to drive creation - at the moment they are simply placeholders and defer to the DriveCmdletProvider implementations. When PowerShell is presented with a new-psdrive cmdlet specifying the ASPNETMembership provider, it will ferret the call to our provider's NewDrive method to create the drive.
Compile and run; in the PowerShell console that opens, enter the following command (note that my convention is to mark commands you type in with leading >'s):
> new-psdrive -PsProvider ASPNETMembership -Name users -Root "";
This will invoke the NewDrive method of the ASPNETMembership provider and create a new drive named "users" in the PowerShell session. To verify the drive exists, use the get-psdrive cmdlet. You should see output similar to the following :
> get-psdrive
Name Provider Root CurrentLocation
---- -------- ---- ---------------
C FileSystem C:\ ...ensions\Profile
cert Certificate \
Env Environment
Function Function
Gac AssemblyCache Gac
HKCU Registry HKEY_CURRENT_USER
HKLM Registry HKEY_LOCAL_MACHINE
users ASPNETMemb...
Custom Drive Objects
Providers are transient: PowerShell will create many instances of this provider in response to different cmdlets. So it's not possible to persist any state in a provider object. Instead, state needs to be stored in a custom drive object that is returned from the NewDrive method of the provider. PowerShell caches this instance and makes it available to the provider via the DriveCmdletProvider.PsDriveInfo property.
The only real requirement for a custom drive object is that it derive from System.Automation.PSDriveInfo. This class is the most basic implementation of a custom drive object possible; go ahead and add it to the project:
// DriveInfo.cs
using System.Management.Automation;
namespace ASPNETMembership
{
public class MembershipDriveInfo : PSDriveInfo
{
public MembershipDriveInfo( PSDriveInfo driveInfo )
: base( driveInfo )
{
}
}
}
Then modify the Provider class to return a new instance of this drive object in the NewDrive method:
// Provider.cs
// ...
protected override PSDriveInfo NewDrive( PSDriveInfo drive )
{
return new MembershipDriveInfo( drive );
}
// ...
Build and run; in the console, enter the following commands:
> new-psdrive -psp aspnetmembership -name users -root ""
Name Provider Root CurrentLocation
---- -------- ---- ---------------
users ASPNETMemb...
> ( get-psdrive users ).gettype().fullname
ASPNETMembership.MembershipDriveInfo
Note that the type of the users drive is now our custom drive object type - at this point, we have our first soild hook into the powershell provider system. Now it's time to fold ASP.NET Membership into the custom drive.... but there's a small problem to deal with first.
Configuring ASP.NET Membership Programatically
Configuring ASP.NET Membership in a web application is pretty easy. You just pop in a little config section into the web.config and everything just works; e.g., this web.config sets up the SqlMembershipProvider for a website:
<configuration>
...
<connectionStrings>
<add name="MembershipProviderServices"
connectionString="data source=localhost;Integrated Security=SSPI;Initial Catalog=AwesomeWebsiteDB"/>
</connectionStrings>
...
<membership>
<providers>
<clear/>
<add name="AspNetSqlMembershipProvider"
type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
connectionStringName="MembershipProviderServices"
enablePasswordRetrieval="false"
enablePasswordReset="true"
requiresQuestionAndAnswer="false"
requiresUniqueEmail="false"
passwordFormat="Hashed"
maxInvalidPasswordAttempts="5"
minRequiredPasswordLength="6"
minRequiredNonalphanumericCharacters="0"
passwordAttemptWindow="10"
passwordStrengthRegularExpression=""
applicationName="/" />
</providers>
</membership>
...
</configuration>
In PowerShell, there is no equivalent to a web.config or app.config we can use to configure the membership provider. Instead, we have to configure the memberhip provider via code. Not a big deal; this short snippit of code results in the same membership provider configuration defined by the web.config example:
var provider = new SqlMembershipProvider();
var nvc = new System.Collections.Specialized.NameValueCollection
{
{ "connectionStringName", "MembershipProviderServices" },
{ "enablePasswordRetrieval", "false" },
{ "enablePasswordReset", "true" },
{ "requiresQuestionAndAnswer", "false" },
{ "requiresUniqueEmail", "false" },
{ "passwordFormat", "Hashed" },
{ "maxInvalidPasswordAttempts", "5" },
{ "minRequiredPasswordLength", "6" },
{ "minRequiredNonalphanumericCharacters", "0" },
{ "passwordAttemptWindow", "10" },
{ "passwordStrengthRegularExpression", "" },
{ "applicationName", "/" },
};
provider.Initialize( "AspNetSqlMembershipProvider", nvc );
The only wrinkle is on line 4, where we reference a connection string by its name. The ConfigurationManager.ConnectionStrings collection is read-only, so how can we add a new connection string to it? Simple, we cheat and make the collection writeable (many thanks to David Gardiner for posting a similar solution on his blog!):
using System.Configuration;
var connectionStrings = ConfigurationManager.ConnectionStrings;
// find the private bReadOnly field in the connection strings base type
var fi = typeof( ConfigurationElementCollection );
.GetField( "bReadOnly", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic );
// change the field value to false, making the object writeable
fi.SetValue( connectionStrings, false );
// add a new connection string
connectionStrings.Add(
new ConnectionStringSettings(
"MembershipProviderServices",
"data source=localhost;Integrated Security=SSPI;Initial Catalog=AwesomeWebsiteDB"
)
);
Now with the ability to add connection strings to the configuration manager programmatically, we can add ASP.NET Membership services to our custom drive object.
Adding ASP.NET Membership Features to the Drive
Modify the custom drive class with our ASP.NET Membership configuration code as follows:
// DriveInfo.cs
using System.Configuration;
using System.Management.Automation;
using System.Web.Security;
namespace ASPNETMembership
{
public class MembershipDriveInfo : PSDriveInfo
{
MembershipProvider provider;
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 );
}
public MembershipProvider MembershipProvider
{
get
{
return this.provider;
}
}
}
}
Note that the custom drive holds a reference to the ASP.NET Membership provider that is initialized in the constructor - since PowerShell caches the drive object, it's safe to save the provider reference. The drive also exposes the membership provider through the public MembershipProvider property.
Build and run. Now the fun begins - first, let's create get a reference to our drive:
> $d = new-psdrive -P
sProvider ASPNETMembership -Name users -Root ""
> $d.MembershipProvider | get-member
TypeName: System.Web.Security.SqlMembershipProvider
Name MemberType Definition
---- ---------- ----------
ValidatingPassword Event System.Web.Security.Membersh...
ChangePassword Method System.Boolean ChangePasswor...
ChangePasswordQuestionAndAnswer Method System.Boolean ChangePasswor...
CreateUser Method System.Web.Security.Membersh...
DeleteUser Method System.Boolean DeleteUser(St...
Equals Method System.Boolean Equals(Object...
FindUsersByEmail Method System.Web.Security.Membersh...
FindUsersByName Method System.Web.Security.Membersh...
GeneratePassword Method System.String GeneratePasswo...
GetAllUsers Method System.Web.Security.Membersh...
GetHashCode Method System.Int32 GetHashCode()
GetNumberOfUsersOnline Method System.Int32 GetNumberOfUser...
GetPassword Method System.String GetPassword(St...
GetType Method System.Type GetType()
GetUser Method System.Web.Security.Membersh...
GetUserNameByEmail Method System.String GetUserNameByE...
Initialize Method System.Void Initialize(Strin...
ResetPassword Method System.String ResetPassword(...
ToString Method System.String ToString()
UnlockUser Method System.Boolean UnlockUser(St...
UpdateUser Method System.Void UpdateUser(Membe...
ValidateUser Method System.Boolean ValidateUser(...
ApplicationName Property System.String ApplicationNam...
Description Property System.String Description {g...
EnablePasswordReset Property System.Boolean EnablePasswor...
EnablePasswordRetrieval Property System.Boolean EnablePasswor...
MaxInvalidPasswordAttempts Property System.Int32 MaxInvalidPassw...
MinRequiredNonAlphanumericCharacters Property System.Int32 MinRequiredNonA...
MinRequiredPasswordLength Property System.Int32 MinRequiredPass...
Name Property System.String Name {get;}
PasswordAttemptWindow Property System.Int32 PasswordAttempt...
PasswordFormat Property System.Web.Security.Membersh...
PasswordStrengthRegularExpression Property System.String PasswordStreng...
RequiresQuestionAndAnswer Property System.Boolean RequiresQuest...
RequiresUniqueEmail Property System.Boolean RequiresUniqu...
As you can see, the membership provider is fully accessible from PowerShell via the drive object. We can create users:
> $status = 0;
> $d.MembershipProvider.CreateUser( "myuser", "mypassword1234", "myuser@hotmail.com", "what is your favorite color?", "plaid", $true, [Guid]::NewGuid(), ([ref]$status))
UserName : myuser
ProviderUserKey : b6ebc2a4-2ff8-4cf8-928e-969416edd704
Email : myuser@hotmail.com
PasswordQuestion : what is your favorite color?
Comment :
IsApproved : True
IsLockedOut : False
LastLockoutDate : 1/1/1754 12:00:00 AM
CreationDate : 6/11/2009 1:13:50 PM
LastLoginDate : 6/11/2009 1:13:50 PM
LastActivityDate : 6/11/2009 1:13:50 PM
LastPasswordChangedDate : 6/11/2009 1:13:50 PM
IsOnline : True
ProviderName : AspNetSqlMembershipProvider
We can get existing users:
> $d.MembershipProvider.GetUser( "myuser", $false )
UserName : myuser
ProviderUserKey : b6ebc2a4-2ff8-4cf8-928e-969416edd704
Email : myuser@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 1:13:50 PM
LastLoginDate : 6/11/2009 1:13:50 PM
LastActivityDate : 6/11/2009 1:13:50 PM
LastPasswordChangedDate : 6/11/2009 1:13:50 PM
IsOnline : True
ProviderName : AspNetSqlMembershipProvider
We can even remove users:
> $d.MembershipProvider.DeleteUser( "myuser", $true )
True
> $d.MembershipProvider.GetUser( "myuser",$false )
#note that no output is returned this time
Using the drive directly to manage users may work, but
it is a bit clumsy. You may be asking yourself why we implemented the
drive at all, given that PowerShell could create the MembershipProvider
object directly. The drive is the basis on which all PowerShell
provider functionality will be based; many standard cmdlets leverage the data stored in a drive object. So things may look cumbersome at the moment, but soon we'll be working
with cmdlets, pipes, and sets of user objects - all thanks to our
custom drive object.
Coming Up
At the moment, the ASP.NET Membership provider configuration lives
code; that should really change so this drive can be used with other
providers or for multiple sites. The NewDriveDynamicParameters method
of the PowerShell provider object allows the provider to request
additional drive creation parameters from the new-psdrive cmdlet. My next post will describe how to properly implement this method.
In upcoming posts, I'll show you how to implement support for different core cmdlets, such as get-item, remove-item, new-item, and get-childitem (or 'dir' for you alias users). In addition, I'll dig deep into the various path syntaxes that PowerShell expects your provider to support.
The code for this post is available here:
ASPNETMembership_pt1.zip (3.24 kb)
As always, thanks for reading, and if you liked this post, please Kick It, Shout It, trackback, or comment.