Wednesday, June 3, 2009

Deploy Web Part as Feature with CAS

Introduction


I had started writing this blog over a year and half ago but I had hirer priority things come up. I still find that a lot SharePoint developers have challenges understanding how to deploy web parts. Specifically CAS, GAC vs bin, rely on CodePlex tools, etc. are a common issue.

As a SharePoint consultant I have seen many struggle with web parts. The issue is not usually with the development of a web parts. There are lots of blogs and books written that show how to develop web parts but the biggest issue I have seen is correctly deploying of web parts. More specifically:

  • Creating web part deployment solutions
  • Deploying web parts as a feature
  • Deploying the web parts in a secure fashion

With Visual Studio 2005, there was a plug-in that developers used to quickly deploy web parts out to a SharePoint environment however there was no control over the deployment package or the deployment process. This plug-in was great for development environments because it allowed developers to quickly deploy and iteratively debug web parts however developers would continue to use this deployment for production deployments. This caused many issues with the control, configuration and change management issues down the road.


With Visual Studio 2008, there is better tooling has been incorporated into Visual Studio by Microsoft for the creation of SharePoint web part projects.


Other tools have been created to help with the authoring WSP deployment files such as WSP Builder. This tool works pretty well but knowing how a WSP solution file is created is equally important. What I have seen is that developers become so dependent on tools such as these, they do not have an understanding of how web parts actually work.

Still the biggest issue we see with deployment is developers run into challenges with deployments of their web parts and they quickly do the following:


  • They raise the trust level on their SharePoint environments to either Medium or Full.
  • They deploy the web part DLL directly to the GAC.

Both are unacceptable solutions especially when this is a production environment (Intranet or not). What makes this worse is many forums, blogs, etc. usually state to do as such and do not outline the consequences. As well, I have not seen any truly good end to end discussions of this topic.


In this blog I will take a deep dive into how a web part should be properly deployed. The web part that will be developed will be inconsequential ("Hello World" example) because the focus will be on deployment. I will specifically focus on:


  • Deploying a web part as a Feature.
  • Deploying to web bin directory.
  • Code Access Security (CAS).

A lot of this information will become moot because we are expecting the next release of Visual Studio to fix a lot of these gaps and provide all the tooling to build SharePoint deployment packages end-to-end.


The first part of this blog is going to discuss how to create a web part feature and properly deploy it. The interesting stuff about properly setting up CAS will later on. However I believe it is important to understand the entire picture before we jump into CAS with web parts.


Creating a Web Part Project


In this first project I am going to create a basic web part project using no accelerator tools.


  • Create a C# code library project which I named WSSDistillery.Deploy.WebPart.
  • Add a references to Microsoft.SharePoint and System.Web.
  • In the AssemblyInfo.cs you will need to add [assembly: System.Security.AllowPartiallyTrustedCallers]. This will allow SharePoint to call the web part contained within dll.
  • Go into the properties of the project and sign the assembly.
  • Renamed Class1.cs to something else. I renamed it to SimplePart.cs.
  • Add an XML file called manifest.xml.
  • Add a file called WSP.ddf.
  • Add the following three folders. First "TEMPLATE", then beneath it "FEATURES", then beneath it "WSSDistillery.Deploy.WebPart".
  • Under the "WSSDistillery.Deploy.WebPart" folder add the following files elements.xml, Feature.xml and WSSDistillery.Deploy.WebPart.SimplePart.webpart.

It is pretty simple to throw together even without all of these tools out there. The final solution should look like:






I will go into further detail about each file and what goes into them. Some initial notes are:


  • It is common practice to create a hierarchy of folders in the Visual Studio project based on the folder hierarchy of files in the 12 Hive (\\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\FEATURES).
  • I named the folder "WSSDistillery.Deploy.WebPart" because this is the folder that will be deployed into SharePoint. I put the namespace into the name of the folder to lower the chances of a name conflict for other SharePoint Features that may be created over time.
  • Even though the assembly has been signed, it will not be deployed to the GAC.

Creating the Web Part


I mentioned before, this will not be a lesson on how to develop a web part. As such, this web part is going to be a simple implementation; Hello World for now. I will add some code later which will require it to have a CAS policy.

public class SimplePart : System.Web.UI.WebControls.WebParts.WebPart
{
protected override void Render(HtmlTextWriter writer)
{
base.Render(writer);

writer.WriteLine("Hello World");
}

}

Get DLL Strong Name


Might as well go ahead and get this now, because this will be needed in several places. Open the Visual Studio command prompt and run the following command:


  • Sn –Tp [path]\[dll name]
  • sn -Tp "C:\WSSDistillery\Deploy\WSSDistillery.Deploy\WSSDistillery.Deploy.WebPart\bin\Debug\WSSDistillery.Deploy.WebPart.dll"

Two values will be generated. The first is a Public Key which is really long, go ahead and get that. The second is a Public Key Token which will be used in several places. These will be used in some of the files that will be created next.


Webpart File


I know "WSSDistillery.Deploy.WebPart.SimplePart.webpart" is a long filename. Again, it is good practice to have the namespace in the name of the file to ensure there are no name conflicts down the line during your web part deployments. This file will be deployed into the Web Part Gallery within a site collection. This file has a reference to the DLL that has the web part code. As well, the Data XML elements contain the text that a user will see when adding a to a web part page within SharePoint. A good description here of what the web part is required for the users of SharePoint.

<webParts>
<webPart xmlns="http://schemas.microsoft.com/WebPart/v3">
<metaData>
<type name="WSSDistillery.Deploy.WebPart.SimplePart, WSSDistillery.Deploy.WebPart, Version=1.0.0.0, Culture=neutral, PublicKeyToken=aa9a1a08fbf22452" />
<importErrorMessage>Cannot import WSSDistillery.Deploy.WebPart.SimplePart</importErrorMessage>
</metaData>
<data>
<properties>
<property name="Title" type="string">Simple Web Part</property>
<property name="Description" type="string">This is a very simple web part for demonstrating deployments.</property>
</properties>
</data>
</webPart>
</webParts>

elements.xml File


This file will be responsible for deploying the WSSDistillery.Deploy.WebPart.SimplePart.webpart file to the Web Part gallery in the site collection.

<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Module Name="WebParts" List="113" Url="_catalogs/wp">
<File Url="WSSDistillery.Deploy.WebPart.SimplePart.webpart" Type="GhostableInLibrary" />
</Module>
</Elements>

Feature.xml File


This is a standard SharePoint Feature file. There are references to both elements.xml and WSSDistillery.Deploy.WebPart.SimplePart.webpart files ion the ElementManifests.

<?xml version="1.0" encoding="utf-8" ?>
<Feature Id="8CD4DE2E-353E-40e1-A2AB-8004F6E8AA5F"
Title="Simple Web Part"
Description="This is a very simple web part for demonstrating deployments."
Version="1.0.0.0"
Scope="Site"
Hidden="FALSE"
DefaultResourceFile="core"
xmlns="http://schemas.microsoft.com/sharepoint/">
<ElementManifests>
<ElementManifest Location="elements.xml" />
<ElementFile Location="WSSDistillery.Deploy.WebPart.SimplePart.webpart"/>
</ElementManifests>
</Feature>

manifest.xml File


This file contains the instructions that will be used by SharePoint to deploy the solution across the SharePoint farm. First, the FeatureManifest element has the location for where the Feature.xml file is. You can deploy many SharePoint Features using a single deployment package; in this example we are only deploying one.


Second the Assemblies element controls where the dll with the web part will be deployed. Notice the DeploymentTarget attribute is set to WebApplication. When this web part is deployed, SharePoint will create a folder called "WSSDistillery.Deploy.WebPart" on every web front end (WFE) web server. This folder will be created within the bin directory of IIS for the SharePoint web application. Within that new folder will be the WSSDistillery.Deploy.WebPart.dll file. The result it the dll is now deployed to the "bin" directory instead of the GAC. For many reason this is the most secure methodology for deploying a web part. Most notably dll will only be accessible to the SharePoint web site and not other applications can use it.

Finally the SafeControls element contains instructions that will modify the web.config file of the SharePoint web application. Specifically it will add the web part as a safe control so that it can be used within SharePoint.

<Solution xmlns="http://schemas.microsoft.com/sharepoint/" SolutionId="052C7D59-7DCF-4782-93B4-51C3F6DECF3B">
<FeatureManifests>
<FeatureManifest Location="WSSDistillery.Deploy.WebPart\Feature.xml"/>
</FeatureManifests>
<Assemblies>
<Assembly Location="WSSDistillery.Deploy.WebPart\WSSDistillery.Deploy.WebPart.dll" DeploymentTarget="WebApplication" >
<SafeControls>
<SafeControl Assembly="WSSDistillery.Deploy.WebPart.SimplePart, WSSDistillery.Deploy.WebPart, Version=1.0.0.0, Culture=neutral, PublicKeyToken=aa9a1a08fbf22452"
Namespace="WSSDistillery.Deploy.WebPart"
Safe="True"
TypeName="*"/>
</SafeControls>
</Assembly>
</Assemblies>
</Solution>

WSP.ddf File


Last file to be created is the ddf file. It is really not that hard to get the hang of:

  • Create a name for the *.wsp file that will be created.
  • Create a destination for the where the file will be created.
  • Get the manifest.xml file.
  • Create a folder within the wsp called WSSDistillery.Deploy.WebPart. Special note, this folder must correspond to the paths to the assembly and feature files that were created within the manifest.xml.
  • Reference the dll, elements.xml, Feature.xml and WSSDistillery.Deploy.WebPart.SimplePart.webpart files.
.OPTION Explicit
.Set CabinetNameTemplate="WSSDistillery.Deploy.WebPart.wsp"
.Set DiskDirectory1="C:\WSSDistillery\Deploy\WSSDistillery.Deploy"

manifest.xml

.Set DestinationDir="WSSDistillery.Deploy.WebPart"

%outputDir%WSSDistillery.Deploy.WebPart.dll
TEMPLATE\FEATURES\WSSDistillery.Deploy.WebPart\elements.xml
TEMPLATE\FEATURES\WSSDistillery.Deploy.WebPart\Feature.xml
TEMPLATE\FEATURES\WSSDistillery.Deploy.WebPart\WSSDistillery.Deploy.WebPart.SimplePart.webpart

.Delete outputDir
To create the WSP file, go the project properties and add the following commands to the Post Build Events.

cd $(ProjectDir)
MakeCAB /D outputDir=$(OutDir) /f "WSP.ddf"

Now build the project and the WSSDistillery.Deploy.WebPart.wsp should be created in the C:\WSSDistillery\Deploy\WSSDistillery.Deploy folder.


Deploying the Web Part Files


The great thing about creating a WSP file is that you can now use the stsadm command line to deploy your SharePoint Feature. In this case, my Feature is a web part. I personally believe that if you cannot deploy a complete solution using a SharePoint Feature, you should reconsider your deployment approach. Deployment into your production environments should be a controlled, repeatable and automated.

The following commands need to be run to deploy the solution to SharePoint.


stsadm.exe -o addsolution -filename C:\WSSDistillery\Deploy\WSSDistillery.Deploy\WSSDistillery.Deploy.WebPart.wsp


stsadm.exe -o deploysolution -name WSSDistillery.Deploy.WebPart.wsp -url http://mossserver:28921 -immediate -force

stsadm.exe -o execadmsvcjobs

Note that the URL to the web application is specified in the deploysolution command. The is necessary because the Feature.xml is a scope of Site.


As a result running these commands the following changes occurred:

  • In C:\Inetpub\wwwroot\wss\VirtualDirectories\XXXX\bin\ a folder called WSSDistillery.Deploy.WebPart will be created and the WSSDistillery.Deploy.WebPart.dll has been deployed there.
  • In C:\Inetpub\wwwroot\wss\VirtualDirectories\XXXX\ web.config the WSSDistillery.Deploy.WebPart entry has been added to the SafeControls tag.
  • In C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\FEATURES and new folder called WSSDistillery.Deploy.WebPart was created and elements.xml, Feature.xml and WSSDistillery.Deploy.WebPart.SimplePart.webpart will be in that folder.

I point this out because it is important to know all the changes that have occurred to your SharePoint environment based on the WSP solution deployment. As well, you will need to verify your deployment to ensure that everything was put in the right place – especially if this is a production deployment.

Activating the Web Part Feature


The next step is we need to activate the Feature with the following commands:

stsadm.exe -o installfeature -filename WSSDistillery.Deploy.WebPart\Feature.xml -force

stsadm.exe -o activatefeature -filename WSSDistillery.Deploy.WebPart\Feature.xml -url http://mossserver:28921 –force


The results are:

  • If you go to http://mossserver:28921/_layouts/ManageFeatures.aspx?Scope=Site in the Site Collection Features you will see the "Simple Web Part" Feature activated.
  • If you go to http://mossserver:28921/_catalogs/wp/Forms/AllItems.aspx you will see the WSSDistillery.Deploy.WebPart.SimplePart.webpart in the Web Part Gallery.
  • You will find the web part in the Miscellaneous group when adding the Simple Web Part to a web part page.

That is it! I know it seems like a lot – but it is really important that you know how all this is working under the hood regardless of what accelerator tool is being used to deploy your web part.


SharePoint and CAS Background


Before diving into how to configure a web part to work with CAS, let us get an understanding of how it works within SharePoint. I will try to make this as simple as can be. By default, whenever a new web application is created within SharePoint, if you go to the web.config you will find the following tag:

<trust level=" WSS_Minimal" originUrl="" />

This basically means that your SharePoint site is running under minimal trust. If you ever come to a SharePoint site that is not running minimal or WSS_Custom (I will explain shortly) there should be major concerns. If for any reason this has been switched to FULL – this would be cause for alarm and not acceptable for any reason. Most SharePoint solutions can be deployed under minimal trust.

In the web.config file, there are elements called trustLevel. By default there will be WSS_Medium and WSS_Minimal. The policyFile attribute shows where this policy file resides within the 12 hive. Let us look at the WSS_Minimal in \\12\config\wss_minimaltrust.config. Please note that these files should not be change for any reason!!!


In the WSS_Minimal you will see that the following libraries are trusted:


  • System.Security.Policy.AllMembershipCondition
  • System.Web.AspNetHostingPermission
  • System.Security.Policy.FirstMatchCodeGroup
  • System.Security.NamedPermissionSet
  • System.Security.Permissions.SecurityPermission
  • System.Security.Policy.StrongNameMembershipCondition
  • System.Security.Policy.UnionCodeGroup
  • System.Security.Policy.UrlMembershipCondition
  • Microsoft.SharePoint.Security.WebPartPermission
  • System.Security.Policy.ZoneMembershipCondition

The first thing you will notice is that you do not have permissions to the SharePoint itself, basically you cannot open something like a SPWeb object with these permission levels. All you basically have permissions to do is some basic .NET code and you cannot do anything integrated with SharePoint, databases, the file system, etc.


Now let us look at the WSS_Medium file. In addition to the ones above, the following have been included:

  • System.Net.DnsPermission
  • System.Security.Permissions.EnvironmentPermission
  • System.Security.Permissions.FileIOPermission
  • System.Security.Permissions.IsolatedStorageFilePermission
  • System.Drawing.Printing.PrintingPermission
  • Microsoft.SharePoint.Security.SharePointPermission
  • System.Net.Mail.SmtpPermission
  • System.Data.SqlClient.SqlClientPermission
  • System.Security.Permissions.UIPermission
  • System.Net.WebPermission

As you can see running under medium level trust now you have the ability to run SharePoint calls but it opened up several other things like FileIO, Smtp and SQL. Now many might think that is harmless to change the permission level to Medium but think about that for a moment. What this basically means is if you change the web.config file from minimal to medium just so you can run a SharePoint API command, you now open up all these things throughout the entire SharePoint web application. So now malicious code could be uploaded into your SharePoint site, and use the permissions levels to do harm or access resources that they should not have access to!!!


This is unacceptable when there is the ability within SharePoint to be granular. I have seen too many people say, change the permission level to Medium. The real solution is to use CAS policies within your maninfest.xml give your web part the proper permissions to execute code. In the manifest.xml file you have the ability to create CAS policies that specific to the web part dll only. If the web part only needs to have access to the SharePoint API, we can create a policy stating that WSSDistillery.Deploy.WebPart.dll can only run Microsoft.SharePoint.Security.SharePointPermission and nothing else. This is highly secure and ensures that no other resources are inadvertently opened.

Modify Simple Web Part


Now let us modify the web part to use an object for which it does not have permission to when running under minimal trust. In this case, lets change the Simple Web Part to reference SPContext.Current.Web. In the code below, I simply want to modify the web part to show a list of sub sites below the current site.

public class SimplePart : System.Web.UI.WebControls.WebParts.WebPart
{
protected override void Render(HtmlTextWriter writer)
{
base.Render(writer);

try
{
StringBuilder webUrls = new StringBuilder();

using (SPWeb currentWeb = SPContext.Current.Web)
{
foreach (SPWeb web in currentWeb.Webs)
{
webUrls.AppendFormat("<a href=\"{0}\">{1}</a><br>", web.Url, web.Title);
web.Dispose();
}
}

writer.WriteLine(webUrls.ToString());
}
catch (Exception ex)
{
writer.WriteLine(ex.Message);
}
}

}

If the code were to be redeployed as is, you will receive the following error.


The "SimplePart" Web Part appears to be causing a problem. Request for the permission of type 'Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c' failed.


Many new SharePoint developers will see this as a harmless code and get frustrated that this has been locked down. Well this is because SharePoint wants to make sure that things are secure and controlled which is not a big deal; you must always account for security in your implementations.


In the next few steps I will give you a more detailed understanding of how CAS works and how your SharePoint environment will be changed as a result of adding the CAS instructions. In my opinion is very important to know how this works.


PermCalc Tool


There is a tool called PermCalc.exe which we will use to help us generate the CAS permissions. I had suggestion to try to use as a way to quickly figure out permissions for a web part. After playing with it for several hours and putting a lot of thought into it, I could not come up with a simple best practice on how to use this for the "everyday" developer.


I was able to get it to generate permission sets which will be discussed shortly, however it did not make things any easier. This is because if you do run this against your web part it will not give you any suggestions on how to add SharePointPermissions like what we had to do above. The only way was to decorate the class, constructor or methods with the following:


[SharePointPermission(SecurityAction.Demand, Unrestricted=true)]

Then if you run the PermCalc tool it would generate the correct IPermission.


So you pretty much have to anticipate the errors; which is not a bad thing either. Really, I think the approach I will explain below is easier and you do not need to add in these decorations to give your web parts these permissions to get them to work in a secure fashion.


Adding CAS

To resolve the issue we need to add CAS instructions to the deployment.

First let's go to the wss_minimaltrust.config file that is \\12\config\. You will see a PermissionSet named SPRestricted. Copy this into your manifest XML file.

<PermissionSet   class="NamedPermissionSet" version="1" Name="SPRestricted">
<IPermission class="AspNetHostingPermission" version="1" Level="Minimal" />
<IPermission class="SecurityPermission" version="1" Flags="Execution" />
<IPermission class="WebPartPermission" version="1" Connections="True" />
</PermissionSet>

Now all I need to do is add an IPermission for SharePoint. Next I opened the wss_mediumtrust.config in \\12\config\. In there under the SPRestricted you will find this

<IPermission class="SharePointPermission" version="1" ObjectModel="True" />

Now the class SharePointPermission is not part of the minimal trust file, so what you do is change this to be:

<IPermission class="Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" version="1" ObjectModel="True" />

All you need to do is scroll up to the SecurityClasses node within the wss_mediumtrust.config.


Done. You can do this for other things like SQL, IO, DNS, etc. permission.


This is how the final modified manifest.xml file will look like.

<Solution xmlns="http://schemas.microsoft.com/sharepoint/" SolutionId="052C7D59-7DCF-4782-93B4-51C3F6DECF3B">
<FeatureManifests>
<FeatureManifest Location="WSSDistillery.Deploy.WebPart\Feature.xml"/>
</FeatureManifests>
<Assemblies>
<Assembly Location="WSSDistillery.Deploy.WebPart\WSSDistillery.Deploy.WebPart.dll" DeploymentTarget="WebApplication" >
<SafeControls>
<SafeControl Assembly="WSSDistillery.Deploy.WebPart.SimplePart, WSSDistillery.Deploy.WebPart, Version=1.0.0.0, Culture=neutral, PublicKeyToken=aa9a1a08fbf22452"
Namespace="WSSDistillery.Deploy.WebPart"
Safe="True"
TypeName="*"/>
</SafeControls>
</Assembly>
</Assemblies>
<CodeAccessSecurity>
<PolicyItem>
<Assemblies>
<Assembly PublicKeyBlob="00240000048000009400000006020000002400005253413100040000010001
00ad51a9cdfbe7db0a0f6d16a257d874a19993707b1bb0e873bda1c18c5b1592e24070f57b637
e0be2b00790fd67bc3e0c61205af5c0e5753780f257acb2c0b4b4830e23ace37be05c7ab52478
2731f85786d7648e6e14a99a83dc474081d5cf5e1fc1b20da22fc3b5a94b44ad3903aa8f081cc
0508e71b5606927824114f2ecd1"
/>
</Assemblies>
<PermissionSet class="NamedPermissionSet" Name="WSSDistillery.Deploy.WebPart" version="1" Description="WSSDistillery.Deploy.WebPart">
<IPermission class="AspNetHostingPermission" version="1" Level="Minimal" />
<IPermission class="SecurityPermission" version="1" Flags="Execution" />
<IPermission class="WebPartPermission" version="1" Connections="True" />
<IPermission version="1" class="Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" ObjectModel="True" />
</PermissionSet>
</PolicyItem>
</CodeAccessSecurity>
</Solution>

Notice the following changes:

  • A new CodeAccessSercurity node has been added.
  • Within it you see the new permission sets.
  • Noticed that I renamed the PermissionSet to match the name of my webpart dll.
  • As well, you see an assemblies node. Notice that I use the PublicKeyBlob which is a value I got from the sn command line earlier when generating a strong file name. Since I am deploying to the bin, this seems to be the only way I can the permissions to pick up. Every time I try to reference the DLL name it does not work.

Deployment of Web Part with CAS


We can deploy the web part in the same fashion as earlier however, add -allowCasPolicies switch to the stsadm.exe -o deploysolution command line, otherwise you will get an error during the deployment.


Affects of the CAS Deployment


Here is how your SharePoint environment changed as a result of this. Go to the web.config file for the SharePoint site. First you will see that the truest level has changed to WSS_Custom. You will that a new treustLevel node has as well been created called WSS_Custom. You will see that SharePoint took the WSS_Minimal file and then created a new file from it. When you go to this new file in the \\12\config\, you will see the permission set that was added to the manifest.xml file has been added here.

Why is this Important


This is all very important from a security perspective:

  • First the web part is deployed to the SharePoint meaning it is only accessible to SharePoint and is not in the GAC. Deploying to the GAC makes the library accessible to any application running on that server.
  • Second only the web part deployed has permissions to SharePoint resources. No other future web parts can tap into resources that is should not have access to. In this case, our custom web part has access to SharePoint Object model and nothing else.

Closing


Again, I know this may seem like a lot of work given all of the CodePlex tools but still having a fundamental understanding of how this all works under the hood is extremely important.


References


7 comments:

Greg Galipeau said...

Great article Jason!
As someone who likes tools like WSPBuilder, I still think it is very important that people understand how things like CAS work. And, just to add another good thing about my favorite tool, WSPBuilder actually adds a custom CAS policy automatically for you that incorporates anything you do in the code.

Priya said...

Good and Detailed. Thanks!

VIKRAM said...

This is very Good.....
Thanks for the information

venkatx5 said...

This is very good article which am looking.. I hope creating ddf file can be automated with some extension..

Jason Apergis said...

The ddf can definately be extended.

If you go to Visual Studio 2010, much of this work goes completely away.

mike said...

Thanks a lot for the example provided, it worked perfectly. However when i try to install multiple web parts through multiple features by adding multiple features in the manifest it fails. is there a way to deploy a solution with multiple web parts?

Jason Apergis said...

Mike,

This approach absolutely supports having more than one web part in the solution. You do not have to create a dedicated Feature for each web part (unless that is what you want to do and that will work too). You can put multiple web part classes into the same DLL.

Jason