Check out my new blog at https://shibumiware.blogspot.com

Friday, February 29, 2008

Microsoft Project 2007 Lookup Table Value Importer

I wrote a tool today that bulk imports lookup table values into Microsoft Project 2007 using the PSI.  The importer works off an Excel workbook contain one or more worksheets containing columns of data.  Each column's values can map to a specific lookup table.

I am trying to get permission to post more code here (and also hand it off to some friends who want it...Brian!).

;-)

The icons are from Mark James.  Even when I am building a tool for internal use, I try to spruce it up with Mark's icons for my own sake really.  I tend to spend a lot of time in my own tools...

image

Have a good weekend!

Thursday, February 28, 2008

Managing Microsoft Project 2007 Custom Fields using the PSI

Chris Boyd just posted a webcast and some samples on how to do basic management operations on enterprise custom fields using the PSI.   Early on when I started learning the 2007 SDK, I decided to implement the custom field editor that is found in SharePoint as a WinForms application (actually a WinForms app shell with a class library implementation of all the forms).  See the screen shots below.  I had somebody ask me if it was really possible to do everything Microsoft is doing in SharePoint via the PSI (and in certain cases such as formulas, the Windows client).  The answer is YES.  Here is the SharePoint tool:

image

Here it is in WinForms

image

 image

image

In my implementation, you can even setup graphical indicators

image

 

 

 

 

 

 

 

 

 

I will try to get some of this posted as samples but not today!

Micosoft Project 2007 PSI Custom FIeld CRUD

Chris Boyd posted a web cast plus samples on doing CRUD on custom field definitions using the PSI... http://blogs.msdn.com/project_programmability/archive/2008/02/28/custom-field-and-lookup-table-webcast.aspx#comments

Wednesday, February 27, 2008

Project Management, Atlas Shrugged, The Glass Bead Game, and my Years at Hogwart's School of Witchcraft and Wizardry

As a child, I was always building something, either alone or with a small team of other kids in my neighborhood. At the time I didn’t know it, but I was often acting as the project manager. My friends and I built model rockets, go-carts, high-gain radio antennas, a river dam, a raft, forts, working model airplanes, kites, and even a parachute of sorts. Alone, I built countless electronic projects including a robotic arm when I was 11 and a rudimentary laser when I was 12 or so. In middle-school, I wrote a grant proposal to my grandmother requesting funding for some of my experiments and won a modest stipend to pursue research about carbon dioxide’s behavior in a closed system to support plant growth (at the time, I wanted to go into the space program and Bio Sphere was a big deal). Until I was about 14, my hobbies were exclusively project-based, and then I spent a few years being a rebellious skateboarding adolescent.

Before my 16th birthday the movie Dead Poet’s Society was released. This movie had a profound impact on me, as did some rather uncomfortable run-ins with local authorities (related to skateboarding and generally just being a punk). I was grounded for a while during that summer and spent a lot of time reading. One of the books I read (and reread) was Atlas Shrugged by Ayn Rand, which makes profound statements about the role of the mind and industry in society. Also that summer I read The Glass Bead Game by Hermann Hesse, which is one of my all-time favorite books. In this book, Hesse draws the reader into thinking about the role of intelligence, academia, and pure research in society. In Atlas Shrugged, the main characters give up on society and take their talents, profits, and products to the utopian Galt’s Gulch and effectively go on strike, thus causing the world to falter and civilization to crumble (Ayn Rand was born in Russia and her work is strongly opposed to any form of communism, redistribution of wealth or socialistic tendencies whatsoever). On the other hand, the main character of The Glass Bead Game, Joseph Knecht, lives as part of ascetic order funded by the state to run boarding schools, do pure research and master the glass bead game (the book’s namesake). It was interesting to read how Joseph slowly started to call into question the validity and value of living disconnected from the problems of the real world and how Rand’s characters were so important to the world that when they withdrew, the world collapsed.

In any event, those two books really woke me up that summer and I decided I should do something to get out of small-town Central Washington State and onto bigger things. Back to The Dead Poet’s Society (and in some ways, the Glass Bead Game given that its setting too was a boarding school)…

I wanted to go to a boarding school. I approached my mom about it and she was gung ho to help me research and pursue the idea even though we didn't know if we could afford to do something like that...

But never discount the single-mother and her power to perform miracles. We found a school about 5 hours away by car in Central Oregon called the Delphi Academy. I refer to Hogwart's of Harry Potter fame when describing what Delphi was like. At a staggering $25,000 a year for tuition, room and board my mother had to refinance the house, I got a scholarship, and other members of the family chipped in to pay my way. Even still, we only had enough to send me for 2 years of a 4-6 year program. While others had done the program in 2-3 years, it was extremely rare but I decided to try it anyway. Two years later, without a summer break, I graduated The Delphi Academy with a 4.0 GPA and college scholarship.

While at Delphi, I took on a very serious project: managing my education. The program at Delphi is largely self-directed (and from some people’s perspective controversial, but it was great for me) and with my hard constraint of finishing within two years and an immutable scope (Delphi’s graduation requirements are severe and nonnegotiable), I had only the schedule variable to play with. I petitioned the school to let me stay up as late as I wanted (bed check was usually 10:30 PM and I would study until midnight or beyond and get up at 6:30 AM and do it all over again). I didn’t take weekend trips like most and I participated in a work-study program during the summer.

In addition to directing my own education, I had to accrue a large number (measuring in the hundreds) hours of real-world experience in my major (like college, students at Delphi select a major—mine was science and technology). I took on a variety of projects but the three most interesting ones were working at a robotics laboratory, writing a database management system for tracking student physical education test scores (this was 1992 and the system was implemented in QuickBasic and subsequently rewritten in C++ by a 13 year old boy who at 16 was EarthLink’s chief software architect!), and building a laser interferometer.

The Delphian School employed interesting educational methods. There were three basic concepts:

  1. When reading, never skip a word of which you don’t fully understand its definition
  2. Always traverse a course of study according to specific levels of complexity and depth so as to ensure each concept is understood before moving on
  3. Always augment your lexical and conceptual understanding with practical application to ensure that the knowledge is both useful and durable over time.

These three ideas, although simple (know what you read, master each level of study, and apply what you learn), are extraordinarily powerful. Fifteen years later I still pause in my reading to lookup a word or backup when I feel like I am bogged down, and where possible I try to use what I learn—which in some cases is impossible because I read about 10 books a month that have no practical application in my work.

Monday, February 25, 2008

Microsoft Project 2003 to 2007 Migration Workbench Part II

Part I of my discussion about migrating from Microsoft Project 2003 to 2007 talked mostly about the various maps and caches I use to move custom field list data from 2003 to 2007, as well as a sort of late-bound mapping of task data from a 2003 task to an 2007 task.   Before I proceed with the discussion, it occurred to me that I overlooked a major requirement for the migration process:  Learn everything I possibly can about the PSI.  There are easier and more efficient migration strategies than doing a task-by-task analysis and transformation from 2003 to 2007, but then I won't learn anything about the PSI and Project's client-side object model (I haven't done any Project automation since my days as technical product manager for Project 98 (ten years ago?!)).

Anyway, onto our migration discussion.

As I mentioned in Part I, we have a single MPP that houses a variety of activity types and each activity has a different schema.  For example, the Holiday type only has three components: Holiday Name, Start Date, and Duration, whilst the schema for a Onsite Training engagement includes Client, Trainer, City, Country, and about ten other components.  Rather than mixing activity types in Project 2007, we have created a number of distinct project in which tasks from the 2003 MPP will be partitioned during migration.  This implies another mapping problem.  Which task fields' data should be copy/updated into the new Project 2007 implementation and into which Project?   I created a simple (and not normalized) table in the database I created that houses all of my migration maps called  WiseConnectTaskComponent, and it looks like this:

image

     The table is not normalized because it is really optimized for fast lookups as I iterate through the tasks in the 2003 MP and I only reach out to SQL server for the map once in the very beginning of the migration pass.  Each 2003 MPP task has an event type that (for the most part) corresponds to a ProjectGuid in the table above.  For each relevant field (remember, each event type's task schema is different), there exists a corresponding Project2007FieldIdentifier.  Note that "FieldName" is not used in the 2007 column name because the value of the attribute can either contain a field GUID or a field name.  If you skip to the last column "Type", you will see a number present which is actually a bit field:

image

Go here an article on the Flags attribute.  The task components that have a value of 0 (zero) are built in fields so the data is copied/updated from the 2003 task to the 2007 task like this:

image

The only interesting thing about this code is the use of Enum.Parse.

For those task component maps that have a value other than zero, the value is a bit field indicating that it is a Custom field with the type of custom field type bit also set.  For each task in the 2003 MPP, I can look up the corresponding 2007 project it belongs to, analyze the fields (as mentioned in Part I) to ensure all of the enterprise custom field values are present, and then map the 2003 task to the 2007 task by looking at the type of field it is an setting the appropriate fields on the ProjectDataSet.TaskCustomFieldsRow object:    

image

Notice for TaskComponentType.CustomLookup, I am looking up the value Guid from my value cache and assigning it to CODE_VALUE.  This is because for those fields in Project 2007 that are based around an enterprise lookup table requires knowing and setting CODE_VALUE to the Guid that corresponds to the particular list value. 

More later.

Friday, February 22, 2008

Microsoft Project 2003 to 2007 Migration Workbench Part I

We are moving from a rudimentary Microsoft Project 2003 implementation to a Microsoft Project 2007 implementation complete with an enterprise resource pool and full use of Project Web Access.  I say our Microsoft Project 2003 implementation is rudimentary because we manage all our activities through a single MPP file stored on a network share.  A small team using Microsoft Project 2003 Professional makes changes to the MPP file and periodically, using a variety of techniques and technologies, project information is published to the appropriate systems.

I cannot go into too much detail about what we use Microsoft Project for and why, but I will say that we currently manage close to 5,000 activities.  This singular MPP file houses a variety of activity types including project and non-project work, as well as personal time off, holidays, and other slices that consume resource availability.

With Microsoft Project 2007, we are moving to a multi-project scheme and resource availability will be housed in the enterprise resource pool.  It is possible to use a shared resource pool or the enterprise resource pool in previous versions of Microsoft Project but for historical reasons, we have not done this.

We use custom fields extensively to store task-specific information.  In particular, we have an Event Type that indicates the type of event or activity on a per-task basis.  A event type might be a holiday, personal time off, internal project work, or any number of activity types that model our most fundamental business activities such as holding a class or delivering an e-learning session.  We use 27 of the 30 available local custom text fields, a few flags, and a few numeric custom fields.

I have been tasked with migrating our existing activities into Microsoft Project 2007 in a fashion that looks ahead to how we will manage our business further down the road but gets us migrated relatively quickly.   Most critically, we need to get our project information into our data center and globally accessible via Project Web Access.  A quick sketch of the requirements looks like this:

  1. Migrate from Microsoft Project 2003 to Microsoft Project 2007
  2. 100% fidelity in data migration
  3. Move from local resources to enterprise resources
  4. Automatically move custom fields from the MPP into enterprise custom fields in 2007
  5. Using lookup tables wherever possible
  6. Unpack custom field values in the MPP and push them into the appropriate enterprise custom fields' lookup tables
  7. Migrate activities to the appropriate project dependant on Event Type (instead of a single project there are multiple projects containing like activities)
  8. The migration method must be something we can back out of if it looks like there are issues

The first step was to figure out the natural partitions in our 5,000 activities and create projects along those lines.  Second was to implement all of the custom fields we require (there are quite a few) but leave the associated lookup tables empty.  My intention was to automatically add items to the lookup tables during a migration pass (I call it a "pass" because the scheme I came up with allows the old system to stay in place and functioning, so many "passes" will occur right up to the last day). 

Once the projects were created and the enterprise custom fields defined, it was time to start building something to process the MPP and migrate the data to the new system.  Again, keep in mind, that I needed to be able to take snapshot of our current MPP and merge it into the new system periodically as we reviewed our requirements and experimented with PWA and the enterprise resource pool.

The figure below is the Migration Workbench, a .Net Windows application written in C# using Microsoft Visual Studio 2008 (icons came from FamFam).  The Migration Workbench makes heavy use of Microsoft Project 2007 PSI web services to do its work.

image

From the image, you can see that several "caches" and "maps" are loaded when the migration pass is kicked off.  First, I iterate through the resources in the MPP's local resource sheet and check to ensure a corresponding enterprise resource exists in Microsoft Project 2007.  If it doesn't, I create it and add the new RES_UID (Microsft Project 2007 uses UIDs (GUIDs) all over the place for unique identifiers which is just great) to an in-memory cache of the enterprise resource pool called the Resource Map).  The Resource Map looks like this:

private Dictionary<string, Guid> _resourceMap = new Dictionary<string, Guid>();

Where the dictionary key is the Microsoft Project Resource Name and the dictionary value is the corresponding resource guid.  Reading the enterprise resource pool is easy using the PSI.  As needed, I have implemented a C# class library that wraps the PSI web services and provides additional services where required.  A partial class diagram can be seen here.

The object model allows for calls like such as ProjectServer.Resources.GetMinimiumInformation() , which returns a ResourceDataSet containing the resource's name, type, and RES_UID:

image 

GetResources is the actual call to the PSI resources web service:

image

Creating a resource (and adding authorization is also straightforward):

image

After caching resource pool information, I load the enterprise lookup tables into a local lookup table library.  My ProjectIntegration library provide the following call to load a lookup table into a library:

_projectServer.LookupTables.AddLookupTableToLibrary(_lookupTableLibrary, LookupTableGuids.City);

image

The following shows the detail behind the call.

image

I created several maps that relate structure and data in Microsoft Project 2003 with structure and data in 2007.  Each task in the 2003 MPP has an Event Type that corresponds to a separate project in 2007.  The Event Type Map maps a task's event type to a project GUID so I can write the task to the appropriate 2007 project.

Another map relates Project 2003 custom fields to Project 2007 enterprise custom fields.  This map is called the Field Map.  See the image below that shows the relationships between various custom fields and enterprise custom fields

image

I analyze each task in the MPP file to ensure that lookup table values exist in Project 2007 for each custom field before I create or update the task in Project 2007.  Remember that I maintain a local cache of each lookup table?  I also keep a very lightweight Value Cache for fast lookups of a value's corresponding Guid (used later on):

image

So, we have three tiers associated with a lookup table value:  first, look to the value cache to quickly find out the value's Guid.  If not found there, then go to the lookup table cache and look for it.  If found in the lookup table, then add it to the value cache and be on our merry way.  If it isn't located in either the value cache or the lookup table cache, then the value doesn't exist in the enterprise lookup table and must be added.   Keep in mind that the value cache doesn't contain any data and is created on the fly (this is why you see the call to __valueCache.ContainsKey(fieldGuid)--the value cache for the lookup table might not exist yet).

Also, this code is executing on its own thread.  Events are subscribed to and Form.Invoke is used to all back into the UI thread (that's what all the OnXXX) calls are.

 image   Adding a text value to an enterprise custom lookup table looks like this:

image

As we will see later on, I often add a lookup table value and immediately attempt to add or update a task's custom field data with the new lookup table value.  This was throwing CustomFieldInvalidUID errors so I had to implement a Wait operation by using the queue web service and calling GetJobCount on QueueMsgType.ReportingLookupTableSync (this seems to work, but I am not sure if there is a better queue message type to look for.

Okay, it is Friday and I have to get out of here!  More next week!

Project Integration Partial Class Diagram (click to open for larger version)

The image below is related to a post I am currently working on... More shortly!

ClassDiagram

Wednesday, February 20, 2008

LookupTableSortOrderMustComeAfterParentSortOrder

Microsoft Project Server 2007 errors codes are great because even though I Googled the known universe and found no useful documentation, I was able to understand that adding a text value to a custom field either requires one to set the index of the row or setup the lookup table to sort descending or ascending.

  image

public void AddLookupTableTextValue(string lookupTableGuid, string value)
{
    LookupTableMultiLangDataSet lookupTableDataSet = null;
    bool checkedOut = false;

    try
    {
        lookupTableDataSet = 
            _lookupTableWebService.ReadLookupTablesMultiLangByUids(new Guid[] {new Guid(lookupTableGuid)}, true);

        checkedOut = true;

        LookupTableMultiLangDataSet.LookupTableStructuresRow structureRow =
            lookupTableDataSet.LookupTableStructures.NewLookupTableStructuresRow();
        structureRow.LT_UID = new Guid(lookupTableGuid);
        structureRow.LT_STRUCT_UID = Guid.NewGuid();
        lookupTableDataSet.LookupTableStructures.Rows.Add(structureRow);

        LookupTableMultiLangDataSet.LookupTableValuesRow newValue =
            lookupTableDataSet.LookupTableValues.NewLookupTableValuesRow();

        newValue.LCID = 1033;
        newValue.LT_VALUE_TEXT = value;
        newValue.LT_STRUCT_UID = structureRow.LT_STRUCT_UID;
        newValue.LT_VALUE_SORT_INDEX = lookupTableDataSet.LookupTables[0].LT_SORT_ORDER_ENUM;
        lookupTableDataSet.LookupTableValues.AddLookupTableValuesRow(newValue);
        _lookupTableWebService.UpdateLookupTablesMultiLang(lookupTableDataSet, false, true);

        checkedOut = false;
    }

    #region Exception Handlers

    catch (SoapException soapException)
    {
        ProjectServer.ProcessMsProjectErrors(soapException, _errorsTextBox, _useEventLogging);
        ProjectServer.ProcessException(soapException.Message, soapException.InnerException, EventLogEntryType.Error);
        if (_continueOnError == false)
        {
            throw;
        }
    }
    catch (Exception exception)
    {
        ProjectServer.ProcessException(exception.Message, exception, EventLogEntryType.Error);
        if (_continueOnError == false)
        {
            throw;
        }
    }
    finally
    {
        if (lookupTableDataSet != null)
        {
            lookupTableDataSet.Dispose();
        }

        if (checkedOut)
        {
            try
            {
                _lookupTableWebService.CheckInLookupTables(new Guid[] {new Guid(lookupTableGuid)}, true);
            }
            catch
            {
                /*  Swallow  */
            }
        }
    }

    #endregion
}

Wednesday, February 13, 2008

Itty-Bitty Add-In & VBA Project Update Engine

A while back I created an add-in for Microsoft Project 2003 that synchronizes SharePoint list values with custom field values. Since then the add-in has grown to include additional functionality implemented both in C# and VBA. This new functionality is changing daily as I work through short-term migration and conversion challenges (we are moving to Microsoft Project 2007). I needed a quick way to push out new bits to the add-in user base without requiring I come around and install things manually. In particular, I needed a way to push out new VBA modules, classes and forms without the option of using an enterprise global template. In just a matter of a couple of hours, I created a very simple update engine.

Requirements

  • Update an IDTExtensibility2 add-in automatically when a new version is detected. Implies a bootstrap installation scheme because the add-in itself calls the update code

  • For any given Microsoft Project plan, check to see if new VBA objects are available and update said objects

  • Update metadata stored on the LAN

  • Updates are stored on the LAN as MSI for updating the add-in and ZIP archives for VBA objects

  • Don't take more than a few hours to implement

Implementation: Metadata & Add-In Update

Update information is stored on a network drive as XML. The schema is very simple:

image There There are two types of updates indicated in the above XML: type="1" and type="2", which correspond to Enum values UpdateType.AddIn and UpdateType.VBAProject in the update engine. The name attribute for type UpdateType.AddIn is simply the name of the add-in for which an update might be available. To determine if an update is available for a particular add-in and apply it, do the following

Version latestVersion;
Updater updater = new Updater();
updater.Open(UpdateType.AddIn, "ProjectAssist");
if (updater.IsNewerVersionAvailable(Assembly.GetCallingAssembly().GetName().Version.ToString(), out latestVersion))
{
   updater.UpdateAddIn(Process.GetCurrentProcess().Id);
}

Calling updater.Open() will go out to the network location where the update metadata XML is stored and load instance data if there is an section with name="ProjectAssist".

The current version of the add-in assembly is passed to updater.IsNewerVersionAvailable. The assembly version is compared to the "latestversion" attribute and returns true or false based on that comparison. The call to updater.UpdateAddIn() updates the add-in bits by running the installation packaged indicated in the "updatelocation" attribute.

The call to update.UpdateAddIn passes the calling process's ID to the update engine. The add-in installation cannot run while the add-in's host application is running. I wrote a little bootstrap that takes a process ID and a path to an MSI that handles launching the setup as soon as the host application is unloaded.

As long as the setup runs correctly, the new version of the add-in will be installed when the host application (in this case, Microsoft Project) runs next.

Not very difficult. Total implementation time was about 30 minutes of coding and about an hour to get it up and running.

Implementation: VBA Project Update

As mentioned previously, some of the functionality I am pushing out is implemented in VBA as a set of forms, modules and class modules. The add-in mentioned in the previous section not only updates itself, but also includes an update engine for updating VBA components in any Microsoft Project file. The VBA objects are collected and zipped into a file also containing a "manifest" of the VBA objects. The manifest is very simple (remember, I wanted this done and working in a matter of a couple of hours). Below is a sample manifest:

@PublishTrainerCalendarsForm.frm

-ImportDataForm.frm

@CalendarPublisher.cls

@TrainerWiseFx.bas @Reports.bas

The "@" prefix indicates that the object may already exist in the Microsoft Project file and needs to be updated. An "-" prefix indicates that the object should be removed. Not mentioned above but also implemented is a "+" prefix that asks the engine to add the new VBA object. Updating a VBA project requires knowing both the name of the Microsoft Project file and the VBA project name. Similar to updating the add-in, the following code does the job:

Updater updater = new Updater();
updater.Open(UpdateType.VBAProject, _projectApplication.ActiveProject.Name);
if (updater.ProjectName == null) return;
_projectApplication.VBE.ActiveVBProject = _projectApplication.VBE.VBProjects.Item(updater.ProjectName);
if (updater.IsNewerVersionAvailable(_projectApplication.VBE.ActiveVBProject.Description, out latestVersion))
{
updater.UpdateVBAProject(_projectApplication.VBE.ActiveVBProject);
_projectApplication.VBE.ActiveVBProject.Description = latestVersion.ToString();
_projectApplication.FileSave();
}

The call to updater.Open passes the UpdateType and the name of the currently active Microsoft Project file. The Open method loads up the Update object's instance data if there is and Update matching the name of the MPP file. I need to change the Open method to return bool so I don't have to make the updater.ProjectName call on the following line. If there is an Update that matches the MPP file name and the VBA Project name, I set the ActiveVBProject to the named VBA Project and call updater.IsNewVersionAvailable. If there is, I update the VBA project and then set the description of the VBA project equal to the new version number. I chose to hijack the description field because the MPP file version is used for something else and doesn't correspond to the VBA objects' versions. Also, I control all of the VBA that is pushed using this method and remember I had to get this done in a few hours).

I unpack the zip containing the VBA objects into a temp directory and load the manifest. Line by line I perform the requested actions on the VBA project. Nothing real special here (this is all wrapped in a try{}finally{} to clean up the temp directory and release the stream reader):

manifestReader = new StreamReader(manifest); 
string manifestEntry; 
char manifestEntryType; 
while ((manifestEntry = manifestReader.ReadLine()) != null) 
{ 
    manifestEntryType = manifestEntry[0]; 
    manifestEntry = manifestEntry.Substring(1); 
    switch (manifestEntryType) 
    { 
        case '+':
            vbProject.VBComponents.Import(Path.Combine(tempPath, manifestEntry)); 
            break; 
        case '-':DeleteVBComponent(vbProject, manifestEntry); break; 
        case '@':DeleteVBComponent(vbProject, manifestEntry); 
            vbProject.VBComponents.Import(Path.Combine(tempPath, manifestEntry)); break; 
        default:
            throw new InvalidOperationException(string.Format("Invalidmanifest entry : {0}", manifestEntry)); 
    } 
} 

The update ZIP files are deflated using the ICSharpCode.SharpZipLib library available here.

Wrapping It Up

The itty-bitty update engine works great and took just a few minutes to implement. The only thing I am considering adding is a way to update views, tables, filters, fields, and maps in Microsoft Project where a enterprise global is not possible.

Tuesday, February 12, 2008

Solitary Developer...

Our team is small and geographically distributed so I find myself working alone quite a bit--with just virtual ad hoc discussions with other developers.  After having worked at Microsoft and other technology companies and spending quite a bit of time in development teams, I find it challenging to work outside of a team.   One way I try to stay connected is to read smart people's blogs.  One of my favorites these days, even though he hasn't written for a long time (off on some top secret gig at Microsoft), is Chris Brumme.  Interestingly, Chris learned how to program when he spent a year in bed due to a broken back!  His BA is in biology and classic languages

Wednesday, February 06, 2008

Safe(r) Impersonation in .Net 2.0

Impersonation is about assuming the identity of another security principal in order to perform operations with the principle's security context.  The first step in impersonation is obtaining proof that you have the right to assume another identity.  I chose to wrap the ADVAPI32.DLL export LogonUser which obtains a security token if proper credentials are passed and authenticated.   The wrapper class looks like this (and thanks to this post for the assist):

    internal class Impersonation
    {
        [DllImport("advapi32.dll", SetLastError = true)]
        private static extern bool LogonUser(
            string principal,
            string authority,
            string password,
            LogonSessionType logonType,
            LogonProvider logonProvider,
            out IntPtr token);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool CloseHandle(IntPtr handle);

        public static bool LogonUser(string userName, string domainName, string password, out IntPtr userToken)
        {
            return LogonUser(userName, 
                             domainName, 
                             password, 
                             LogonSessionType.Interactive, 
                             LogonProvider.WinNT50, 
                             out userToken);
        }

        public static int GetLastError()
        {
            return Marshal.GetLastWin32Error();
        }

        #region Nested type: LogonProvider

        private enum LogonProvider : uint
        {
            Default = 0, // default for platform
            WinNT35, // sends smoke signals to authority
            WinNT40, // uses NTLM
            WinNT50 // negotiates Kerb or NTLM
        }

        #endregion

        #region Nested type: LogonSessionType

        private enum LogonSessionType : uint
        {
            Interactive = 2,
            Network,
            Batch,
            Service,
            NetworkCleartext = 8,
            NewCredentials
        }

        #endregion
    }

I have a little dialog for soliciting credentials:

image

After calling Impersonation.LogonUser successfully, I have a valid security token with which I can impersonate.  .Net provides two classes to assist: WindowsIdentity and WindowsImpersonationContext.  As both implement IDisposable, one might write something like the following:

IntPtr userToken;

if (Impersonation.LogonUser(_userName, _domain, _password, out userToken))
{
    using (WindowsIdentity identity = new WindowsIdentity(userToken))
    using (WindowsImpersonationContext impersonationContext = identity.Impersonate())
    {
        try
        {
            GetServiceStatuses();
        }
        finally
        {
            impersonationContext.Undo();

            Impersonation.CloseHandle(userToken);
        }
    }
}

The problem here is that "finally" will execute EVENTUALLY, but there is the potential for an evil-doer to execute code while still impersonating. I did some reading, and I recommend this article by Eric Lippert here. Referenced deep in the comments is the CLI Standard.  For a deep dive into the CLI's Exception Model, visit this blog.

The implementation I settled on looks like the following (there are things that could be improved on here as well).   The key here is that the I still throw the exception to the calling code but I undo the impersonation before that happens.

  IntPtr userToken = IntPtr.Zero;
  WindowsIdentity identity = null;
  bool isImpersonationReversed  = false;
  WindowsImpersonationContext impersonationContext = null;
  
  try
  {

      if (Impersonation.LogonUser(_userName, _domain, _password, out userToken))
      {
          identity = new WindowsIdentity(userToken);
          impersonationContext = identity.Impersonate();
          
          GetServiceStatuses();
      }
  }
  catch(Exception exception)
  {
      if (impersonationContext != null)
      {                        
          impersonationContext.Undo();
          isImpersonationReversed = true;
      }                        
      throw;
  }
  finally
  {
      if (impersonationContext != null)
      {
          if (!isImpersonationReversed)
          {
              impersonationContext.Undo();
          }
          impersonationContext.Dispose();
      }

      if (identity != null)
      {
          identity.Dispose();
      }

      if (userToken != IntPtr.Zero)
      {
          Impersonation.CloseHandle(userToken);
      }                    
  }

 

Okay, back to work!

Tuesday, February 05, 2008

Turn off your brain...

I really hate doing this kinda of thing, but sometimes you just need Project to turn its brain off.  I have a peculiar case where I want a start date/time to NEVER change and not be effected by the scheduling algorithms (to include calendar exceptions).  Here is the VBA:

SetTaskField field:="Constraint Type", value:="Must Start On", TaskID:=currentTask.ID
SetTaskField field:="Task Calendar", value:="24 Hours", TaskID:=currentTask.ID
SetTaskField field:="Ignore Resource Calendar", value:="Yes", TaskID:=currentTask.ID

currentTask.Start = startDate

Monday, February 04, 2008

Windows Live Writer Plugin : Insert Code

I found a plugin for Windows Live Writer for inserting code into a post.  Very cool, however, it doesn't work correctly with Blogspot unless you add the plugin's stylesheet output to the Posts style sheet.  So, if you are a Blogspot user, take the CSS below, go to Customize\Template\Edit HTML and locate the Blogs CSS section.  Paste it in and you are good to go FOR C# ONLY.  Get the plugin here.
.csharpcode, .csharpcode pre
{
    font-size: small;
    color: black;
    font-family: consolas, "Courier New", courier, monospace;
    background-color: #ffffff;
    /*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
    background-color: #f4f4f4;
    width: 100%;
    margin: 0em;
}
.csharpcode .lnum { color: #606060; }
 Here is a sample.
     public Dictionary<string, SPTS20User> GetUserList(string site)
        {
            try
            {
                if (_rsspUserGroupWebService == null)
                {
                    _rsspUserGroupWebService = new MsSpUserGroupWebService.UserGroup();
                    _rsspUserGroupWebService.Url = _serverURL.ToString() + site + _UsersServicePath;
                    _rsspUserGroupWebService.Credentials = _credentials;
                    _rsspUserGroupWebService.Discover();
                }

                XmlNode usersXml = _rsspUserGroupWebService.GetUserCollectionFromSite();
                Dictionary<string, SPTS20User> users = new Dictionary<string, SPTS20User>();

                foreach (XmlNode userNode in usersXml.FirstChild.ChildNodes)
                {
                    SPTS20User user = new SPTS20User();

                    user.Email = userNode.Attributes["Email"].Value;

                    if (users.ContainsKey(user.Email) == false)
                    {
                        user.Account = userNode.Attributes["LoginName"].Value;

                        user.IsDomainGroup = Convert.ToBoolean(userNode.Attributes["IsDomainGroup"].Value);
                        user.IsSiteAdmin = Convert.ToBoolean(userNode.Attributes["IsSiteAdmin"].Value);
                        user.SID = userNode.Attributes["Sid"].Value;
                        user.Notes = userNode.Attributes["Notes"].Value;

                        user.Email = user.Email.ToLower();

                        users.Add(user.Email, user);
                    }
                }

                return users;
            }
            catch (System.Exception e)
            {
                throw;
            }
        }

Test Blog from Microsoft Windows Live Writer

Microsoft's Windows Live Writer (WLW) is great.  I assumed that it would not work with blogspot (not sure where the assumption came from, but I just assumed that).  After installing WLW, I was prompted to provide my blog url, user name, and password.  The setup wizard went out and thought about my blog for a minute (Please wait, detecting blog settings or something like that) and came back a second later and asked "would you like WLW to detect your theme by posting a temporary post?'.  Sure.

Bam. Done deal...

WLW works with blogspot.  

Nice job Microsoft!

Get it here.

Friday, February 01, 2008

Microsoft Project File Bloat

I have been adding and deleting huge numbers of tasks via automation...and, of course, my mpp files are bloat-holio. A simple File\Save As directly after opening the mpp will cull and defragment the file down to a smaller footprint. Reference is here.

Disclaimer

Content on this site is provided "AS IS" with no warranties and confers no rights. Additionally, all content on this site is my own personal opinion and does not represent my employer's view in any way.