fredag den 30. september 2011

Installshield MSI Multiple Instances

While working on automating installation of SuperOffice Customer Service (formerly known as eJournal ) I ran into a few issues. I started with just copying all files and changing configuration files but it didn’t quite do the trick. Doing installation of Customer Service it also install's 2 Windows Services and scripting that would just not be worth it, so I decided to see if I could control the installer instead.

If you have installed SuperOffice Customer Service once, the next time you call SuperOffice.CustomerService.exe you will get a prompt to either create a new instance or modify an existing installation

image

I’m no expert on MSI installers, but I did some digging around and it seems the way they get that to work, is doing some Transform scripting inside the installer. (that’s why it is an exe and not an MSI )

The first thing I did was extract the MSI and have a look at the properties in the installer so I could pass on the correct parameters. but when I called MSIEXEC with the SMI, I would get all kinds of different errors. (makes sense now ). After a lot of goggling I found that I could control the instance by number, by adding /Instance= to the .EXE … test that I created a couple in instances like this

SuperOffice.CustomerService.exe /instance=1 /V"EJHOSTNAME=servicesotest1.so7.wingu.dk INSTALLDIR=C:\servicesotest1.so7.wingu.dk /L*v c:\1.log /passive"
SuperOffice.CustomerService.exe /instance=2 /V"EJHOSTNAME=servicesotest2.so7.wingu.dk INSTALLDIR=C:\servicesotest2.so7.wingu.dk /L*v c:\2.log /passive"

And uninstalling

SuperOffice.CustomerService.exe /x /instance=1 /V" /L*v c:\1.log /passive"
SuperOffice.CustomerService.exe /x /instance=2 /V" /L*v c:\2.log /passive"

so I though that would be awesome, I could just pass on my CustomerService ID as Instance Name and everything would be perfect. But no … if I pass on a number higher than 1000 I fails and doesn’t work, so I had to do some more digging. I decided to see if I could emulate all the instance numbers and find a “free” instance number when installing, and the same, when uninstalling and look at install location.

I came across this neat little PowerShell Snap In/Module to get information about installed software. but you can also get all the information with WMI if you prefer that kind of stuff

$programs_installed = @{};
$win32_product = @(get-wmiobject -class 'Win32_Product' )
foreach ($product in $win32_product) {
$product | fl
# $product.InstallLocation
$name = $product.Name;

$version = $product.Version;
if ($name -ne $null) {
$programs_installed.$name = $version;
}
}

So now I can easely get a list of installed instances including my “instance number” by running


Get-MSIProductInfo -Name 'SuperOffice Customer Service' | fl ProductName, ProductCode, InstallLocation, PackageCode, InstanceType, Transforms

For a complete list, do


Get-MSIProductInfo | fl AuthorizedLUAApp, InstallContext, InstalledProductName, Language, Name, PackageCode, PackageName, ProductIcon, Transforms, Version, CollectUserInfo, Equals, GetComponentState, GetFeatureState, GetHashCode, GetType, ToString, PSPath, Item, AdvertisedLanguage, AdvertisedPackageCode, AdvertisedPackageName, AdvertisedPerMachine, AdvertisedProductIcon, AdvertisedProductName, AdvertisedTransforms, AdvertisedVersion, Context, Features, HelpLink, HelpTelephone, InstallDate, InstallLocation, InstallSource, IsAdvertised, IsElevated, IsInstalled, LocalPackage, PrivilegedPatchingAuthorized, ProductCode, ProductId, ProductName, ProductVersion, Publisher, RegCompany, RegOwner, SourceList, UrlInfoAbout, UrlUpdateInfo, UserSid, Advertised, Installed, V1, AssignmentType, DiskPrompt, InstanceType, LastUsedSource, LastUsedType, MediaPackagePath, ProductState, VersionMajor, VersionMinor, VersionString

So to wrap it all up ….


function InstallSuperOfficeCustomerService([String]$fqdn){
$InstanceID = 0
#$AllreadyInstalled = Get-MSIProductInfo -Name 'SuperOffice Customer Service'
$AllreadyInstalled = @(get-wmiobject -class 'Win32_Product' ) | where {$_.Name -eq 'SuperOffice Customer Service'}
if($AllreadyInstalled){
# add 1, to avoid getting "default" instance, and check if its free
$InstanceID = $InstanceID + 1
$isFree = $AllreadyInstalled | where { $_.Transforms -eq ":InstanceId$InstanceID.mst" }
# If not free, loop til we find one unused
while($isFree){
$InstanceID = $InstanceID + 1
$isFree = $AllreadyInstalled | where { $_.Transforms -eq ":InstanceId$InstanceID.mst" }
}
}
if(! (Test-Path 'c:\CustomerService')){ $temp = New-Item 'c:\CustomerService' -type directory }
# replace /passive with /qn to hide the UI
$parameters = "/instance=$InstanceID /V""EJHOSTNAME=$fqdn INSTALLDIR=C:\CustomerService\$fqdn /L*v c:\CustomerService\install-$fqdn.log /passive"""
# Write-Host ("X:\SuperOfficeInstall\Customer Service 7.0 SR2\SuperOffice.CustomerService.exe " + $parameters)
$installStatement = [System.Diagnostics.Process]::Start( "X:\SuperOfficeInstall\Customer Service 7.0 SR2\SuperOffice.CustomerService.exe", $parameters )
$installStatement.WaitForExit()
}

function UnInstallSuperOfficeCustomerService([String]$fqdn){
# now i could look though the instance number and call it with
# SuperOffice.CustomerService.exe /x /instance=$InstanceID /V" /L*v c:\$InstanceID.log /passive"
# but instead i'll play it safe and use the ProductCode

#$isInstalled = Get-MSIProductInfo -Name 'SuperOffice Customer Service' | where {$_.InstallLocation -like "*$fqdn*" }
$isInstalled = @(get-wmiobject -class 'Win32_Product' ) | where {($_.Name -eq 'SuperOffice Customer Service') -and ($_.InstallLocation -like "*$fqdn*")}
if($isInstalled){
# replace /passive with /qn to hide the UI
# $ProductCode = $isInstalled.ProductCode
$ProductCode = $isInstalled.IdentifyingNumber
$parameters = "/x $ProductCode /passive /L*v c:\CustomerService\uninstall-$fqdn.log "
# Write-Host ("msiexec " + $parameters)
$uninstallStatement = [System.Diagnostics.Process]::Start( "msiexec", $parameters )
$uninstallStatement.WaitForExit()
}
}

function IsSuperOfficeCustomerServiceInstalled([String]$fqdn){
$isInstalled = @(get-wmiobject -class 'Win32_Product' ) | where {($_.Name -eq 'SuperOffice Customer Service') -and ($_.InstallLocation -like "*$fqdn*")}
if($isInstalled){
return $true
}
else {
return $false
}
}

InstallSuperOfficeCustomerService 'deleteme4.domain.com'

UnInstallSuperOfficeCustomerService 'deleteme4.domain.com'

IsSuperOfficeCustomerServiceInstalled 'deleteme4.domain.com'
IsSuperOfficeCustomerServiceInstalled 'deleteme5.domain.com'

SharePoint 2010 and WebDAV

Updated 08-10-2011: Please see update here

I have known for sometime that SharePoint supports WebDAV, and in my experience, you always end up spending a few hours getting it to work. Today was no different.

A collogue of mine sent me an email complaining he couldn’t connect to he's SharePoint 2010 site in explore or use the Explorer View button within the site. "Open with Explorer”

image

I knew I had seen it work in the past but for some reason I couldn’t get it to work for me neither. I tried everything but no matter what I just kept getting an annoying “Your client does not support opening this list with Windows Explorer”

image

And when connecting with “net use * https://blah.blah” I would constantly get

System error 224 has occurred.

Access Denied. Before opening files in this location, you must first add the web site to your trusted sites list, browse to the web site, and select the option to login automatically.

image

there is a billion hits on Google searching for this, but one of the more interesting was a White paper on Microsoft.com - Understanding and Troubleshooting the SharePoint Explorer View but it didn’t really get me any further.

I’m using Claimsbased authentication on all SharePoint sites, so I started thing it had something to do with that. I found this post, that claims you cannot connect to SharePoint if it using Forms based authentication / Claimsbased authentication. But then I test on my local computer where I also SharePoint installed configured to use Claims too, and that works. This was starting to be really annoying

God know how, but then I cam across this post. Note that he writes:

The Open with Explorer and the new Upload Multiple seems to depend on Office 2010 being installed.

So I was clicking around inside SharePoint as a maniac and at one point I hit the “Connect to Office” button, and suddenly it worked.
image

I instantly opened a DOS prompt and tried “net use * https:\\blah” and it worked too.

So while writing this blog post, I created a new SharePoint site with English language instead of Danish to make some better screenshots. And clicking Connect to Office just didn’t seem to be enough, so I started looking at the differences between the Danish site and the English site. 2 things where different. I normally don’t enable Anonymous Access, but I had done that on the Danish site.

image

And I normally don’t enable Basic authentication, but I had here.

image

Still nothing. This was driving me nuts now …. so I started clicking around like a maniac again, and BAM suddenly it all worked again. What the hell ???

Now I was getting mad. I created a new site ( Basic authentication disabled, and Anonymous Access disabled like normal ). View in Explorer fails as expected. I click the “Datasheet View”
image

And after that everything suddenly works. I guess the trick here is making sure Office have “talked” with the SharePoint site in some way or the other.

What a waste of a few good hours I will never get back …

onsdag den 28. september 2011

I make mistakes, but I’m never wrong.

This will not be a technical blog post, so if you’re not in too all this insightful feelings stuff, close your browser now.

I do a lot of coding, but I’m not a developer. I do a lot of system installation and configuration, but I’m not a System Administrator. I do a lot of system designs but I’m not a system architect. So what am I? People who have known me for a while will know I would describe myself as a very talented bug finder and troubleshooter. I’m also very stubborn both in a good way and in a bad way. So let’s have a look at how “I can be oh so wrong” and make a complete fool out of my self :)

I was asked to try and implement SuperOffice 7 in the hosting environment I’m working on. As always, we had known for quite a while this was something we would need to get working, but when I finally got the assignment I had less than a week to get something up and running, because we had a potential new customer that wanted a solution NOW! (Why is it always like that?)

So I asked for the installation media and created a proof of concept installation. It had a few errors and flaws but in general I was convinced I could get this up and running. I prefer automating everything I do, while I’m doing it, to make it easier to reestablish the environment and to “catch” whatever problems I will run into, when I need to automate it anyway, once we have to go into production.

So after 4 days I had the following issues, in order to give the final go on my solution.

  • I couldn’t automate the process of creating a new blank SuperOffice database
  • I couldn’t get SuperOffice to start up inside App-V but was working fine on my test servers and deployment machine, so was convinced this would be easy to solve.
  • While I was converting my ugly PowerShell scripts to PowerShell CmdLet’s I noticed it was using insane amounts of memory that I was worried would impact the performance of the provisioning engine.

Except the creating of the SuperOffice database everything was automated so a Support case was opened on the 2 upper problems. While testing and trying out different things I noticed something funny in my PowerShell snap in. Sometimes I would get the same error message “Failed to create Identity from tokens” as I got on the SuperOffice client . While digging in a little deeper I realized that I really hadn’t done my homework properly. The NetServer API SuperOffice offers to .NET developers doesn’t allow you to “swap” between databases and the only way to work around that would be to place the code inside different AppDomains and I had never tried coding something that used different AppDomains. I hated that, and started getting frustrated at SuperOffice, which was completely wrong of me for 3 good reasons. For one they had a valid reason for making that choice, and two I learned a lot of new exciting stuff about working with AppDomains that I know will be useful to me in other cases and lastly. Being able to unload all the SuperOffice stuff from within the memory space of the provisioning engine, also solve one of the 3 problems mentioned above.

So 1 weeks passes and I just can’t “crack” the problem with the SuperOffice client and SuperOffice wasn’t helping us out a lot, on the case. I could reproduce the error in almost any environment ( Windows 7, Windows 2008, and Windows 2008 R2 servers. From 32 and 64 bit. With and without App-V. With or without being run from Remote Desktop or XenApp desktops. I tried using SQL 2005, SQL 2008 and SQL 2008 R2 servers … nothing would work for me, but everyone I talked with kept saying “but it works for me, no problem. It sure sounds weird” ) , so a colleague put me in touch with a partner we use when implementing SuperOffice at other customers. They solved the database problem in a few hours and we sat down and went through the whole setup to see if we could figure out what I was doing wrong in regard with the SuperOffice Client. While showing my approach for different things they were complaining a lot. I wasn’t doing things the “SuperOffice” way. I refused to use “CRM5” as Schema prefix and insisted on keeping DBO as owner on all objects (will give problems when working with 3rd party add-ons). I used a controversial way of loading SuperOffice by using inipath as start parameter instead of keeping the configuration inside the SuperOffice client installation folder ( will give problems with reporting, office add-ons and 3rd party add-ons). And I refused to add the user’s as SQL users in the database. ( Remember, I am stubborn. and I had read somewhere you didn’t need to do it. ) Any way, he suggested I added the users ( as windows users ) to the database and BAM, everything worked, I mean everything. I was so embarrassed. Anyone who have worked with SuperOffice know this is how you do things, and people had been telling me nonstop since I started on this project I had to remember this. But I was so convinced this was not needed, but I couldn’t remember where I had read it. I just made sense to me, why add the user, when SuperOffice already had a username/password from the superoffice.config . And the NetServer API worked flawlessly without doing that. We had even priced the product to not include an SQL CAL for each user.
So Just be sure I sent an email to a contact within SuperOffice asking him to confirm, that this had been my problem all along. The reply I got wasn’t quite what I had expected. He told me I was correct, users do not need access to the database and the problem was with the ODBC DSN links I had created. I was about to explode at that point. I felt I was bouncing back and forward all the time and getting nowhere. I mean there is only one way to create a DSN link, you choose an ODBC provider fill in server and database and you are done. How freaking hard can it be.

Moving on, we decided to stick with adding the users to database and just be done with it. While restoring a customer’s data from one SuperOffice database too another we started getting some access denied messages. As always my first tool of choice to trouble shoot SQL problems, was opening SQL Profiler and look at what was going on. I noticed dbsetup.exe was executing half the queries as the windows user we were logged on as, and not the SQL login it was being told to use. The Support Technician from SuperOffice say; as it was the most natural thing in the world “Did you remember to set the DSN connection to use SQL login” ?

God . If I hadn’t already felt dumb enough as it was, that sure did the trick. Why, oh why hadn’t I opened the freaking SQL profiler a long time ago while trouble shooting the client problems ? Why didn’t I listen when SuperOffice told me to check the DSN connection ?

If you look at this screen shot. Since the username and password doesn’t get saved, but it affects the “test” button at the end of the wizard, I have always assumed this only affected the test in the end. But that is defiantly not the case. If you leave it as show, it will add a “Trusted_Connection” entry in registry with the value of “yes”. If you change the radio button down to SQL Authentication it doesn’t add that registry entry, or it changes the value to “no”

clip_image001

Most connection strings I have worked with have always had Trusted Connection=yes. I generally try and avoid using SQL login when I can (but it can be useful in some cases) What happens in the case of SuperOffice is, they don’t specify that when they connect using the DSN, and therefor “inherits” whatever setting you choose in the wizard. That can be quite handy, and confusing at the same time. But it means that even thou SuperOffice send a username and password, it gets ignored if Trusted_Connection is set to yes, flip it back to SQL and it will now connect using the username and password SuperOffice sends with it.

Anyway . . .So I went back and changed all my code and scripts to create the DSN links with Trusted_Connection set to “no” and removed the all users from the Windows group I had created to give users access to databases and everything worked like a charm. So now we don’t need to bill all users for an SQL CAL, but only one CAL per SuperOffice installation.

Who would have guessed, not only do I make mistakes I can also be wrong.

mandag den 26. september 2011

SuperOffice 7 Web and SO_ARC on different servers

I’m playing around with SuperOffice 7 web. I’ve managed to automate the process of creating separate websites per SuperOffice instance, but after the initial setup I started getting some weird errors.
I’m far from, done with all this, and I defiantly don’t know everything I need to know to make all this work, but I’m taking things as they come.

When downloading files, I would get errors like
clip_image002

My first though was I hadn't escaped the file path properly.
When setting up SuperOffice web I found that these 4 files needs to be updated
~\bin\Reporter\SoReporter.Executer.exe.config
~\bin\BatchService\SoBatchService.exe.config
~\bin\Reporter\SuperOffice.CONFIG
~\Web.config
I modified my SuperOffice PowerShell snap in to support loading all files and changing the content, to make it easier for me while automating the process.

In the above error the SO_ARC is places on \\fs01\Customers\SOTEST2\APPS\SO7\SO_ARC so that’s what I had in SoBatchService.exe.config, SuperOffice.CONFIG and Web.config. … I escaped the string with $SO_ARC = $SO_ARC.Replace("\", "\\") … Provisioned the website and tried again.

Still now luck, so open Process Monitor and look what's going on …. AAHHH!

image

That’s weird, I specifically set the credentials I want it so connect as and yet its accessing the files as IUSR …

    <Documents>
<!-- Location of SO_ARC -->
<add key="ArchivePath" value="\\\\fs01\\customers\\SOTEST2\\APPS\\SO7\\SO_ARC" />
<!-- Location of template folder.
This folder only needs to be specified when it is other than default.
-->
<!--<add key="TemplatePath" value="\\\\qa-build\\StateZeroSoArc\\Template" />-->
<!-- Location of temporary folder for streaming files.
This path must resolve to the same location for farms/culsters.
-->
<add key="TemporaryPath" value="c:\\temp" />
<!-- Impersonate user when accessing the document archive or the temporary folder -->
<add key="ImpersonateUser" value="False" />
<!-- Name of the user to impersonate with -->
<add key="ArchiveUser" value="soadmin_SOTEST2" />
<!-- Password of the user to impersonate with -->
<add key="ArchivePassword" value="SuperSecret" />
<!-- Domain of the user to impersonate with -->
<add key="ArchiveDomain" value="INT" />
<!-- Size of internal buffer in KB -->
<add key="BufferSize" value="32" />
</Documents>


Ah well, no worry, I modified my script to create an application pool with a domain account and set the websites anonymous account to run as the application pool.


# Set application Pool identity
Set-ItemProperty ('IIS:\AppPools\' + $InstanceName) -name processModel -value @{userName=($ADDomainName + '\' + $SOAdminUser);password=$SOAdminPassword;identitytype=3}
# Set Application Pool Identity as Anonymous User
Set-WebConfigurationProperty -filter /system.webServer/security/authentication/anonymousAuthentication -name userName -value "" -PSPath IIS:\ -location ('IIS:\Sites\' + $InstanceName)

And now it works… Guess that is a bug, or I don’t understand the parameters well enough.


And tonight I’m going to throw some love at reporting … Every time I try and generate a report I get this error in the browser after a minute’s time.


clip_image002[6]


Updated 27-09-2011: Problem solved. First of all, I had forgotten to create the DSN link. But it still failed.
There was no error messages being generated anywhere, so I fired up process monitor and had a look. And sure enough, SoReporter.Executer.exe was getting access denied creating so_log.txt, so I gave the application poll write permissions in the /bin and had a look. After doing that, every time I tried generating a report, instead of getting the above error I would know be logged out of the website. hmmm, strange …

After looking at some of the warnings in the normal log files I started to wonder if “Failed to parse Assembly 'Edanmo.OleStorage, Version=1.0.847.22469,Culture=neutral,PublicKeyToken=8840063030bd4bce'.” had something to do with this so began looking more into errors after finding DLL files. Edanmo.OleStorage quickly lead me to MsgReader – DLL . I bet they chanced something in the code and was not in the mode for reflector, but since I know now I had something to do with mail I remembered seeing something in the above config files


    <Factory>
<DynamicLoad>
<add key="DefaultSoMail" value="C:\Program Files\SuperOffice\SuperOffice 7 Web\SuperOffice\bin\SoMail.dll" />
<add key="SaleImpl" value="SuperOffice.Sale.Services.Implementation.dll" />
<add key="ProjectImpl" value="SuperOffice.Project.Services.Implementation.dll" />

Now, I know that is not right. I remember thinking the first time I saw it, that I couldn’t harm pointing at the “wrong” directory. the DLL was there too … but I know better than that. so just for fun I went though all the config files mentioned above and removed the path


        <add key="DefaultSoMail" value="SoMail.dll" />

Love and be hold. We have a winner …. Now I can generate reports.
I modified my provisioning scripts, deleted everything and created all from scratch ( to fix permissions, things I had chanced in config files etc. ) and re-tested and it still works .. so that is the fix …

søndag den 25. september 2011

Hosting SuperOffice 7 on Citrix/RDP

I have been spending a lot of time looking into SuperOffice 7 the last few weeks, preparing automatic provisioning and hosting of SuperOffice 7. I want to briefly talk about some of the issues I ran into, and how I worked around them to get a working environment up and running.

SuperOffice can work in a 2-tier solution with a SQL database on a server somewhere and a fat client installed on a client computer. In this solution when SuperOffice is started it load its configuration from the installation directory (C:\Program Files (x86)\SuperOffice\SuperOffice 7 Windows)
SuperOffice can also run as a Web Client in a semi 3-Tier solution. You have your database on some severe, you have a Webserver running 1 or more websites, each website with it’s own configuration file(s).

From a hosting perspective that means you would prefer the web approach, but not everything in SuperOffice works from the web client. And, for now, some of the administrative tasks cannot be done though the web interface either. Many 3rd party add-ons for superoffice is not designed to work from the web client either.

In a hosting environment with Citrix/Remote Desktop servers is sometimes installed as dedicated per customer, and sometimes installed in a “shared hosting” environment where different customers facilitate the same farm of server. If customers have their own servers, keeping the configuration files in a single location shouldn't post any problems, but in a shared environment you don’t want different users from different customers being able to only talk with 1 SuperOffice database

I'm sure there's many ways to handle this, but for now I see 2 working solutions. The SuperOffice 7 client supports an parameter called IniPath. If we start SOCRM.exe or SOAdmin.exe with this parameter it will load the superoffice.config and superoffice.ini files from that location instead. That does how ever pose a few problems. SuperOffice still use a lot of COM interfacing ( ActiveX ). Say you have word open and want to archive the document you have open. If SuperOffice is running you will see
image
and everything works perfectly. If how ever, SuperOffice is not running you will see
image
And if you click that button it will launch the SuperOffice client for you. This launch will not know about the IniPath parameter and SuperOffice will now be searching the wrong place for the configuration files.

I’m not sure if you could do an search and replace in registry and change the launch parameters, but there is another way

In the environment I’m working with right now we use Microsoft Application Virtualization ( App-V ) to deploy applications on all Citrix XenApp and Remote Desktop Servers. When you are sequencing SuperOffice 7 you can modify the File Properties for SuperOffice.config and SuperOffice.ini to make them User Specific

image

So I created a small “initializer” application and added to the package, that each user have to call once before using SuperOffice. That application copy in the configuration files for that specific customer, and from now on every time superoffice starts it is reading the “correct” configuration files. ( Note: you need to set the right permissions on the files, to allow users to change these files, while sequencing SuperOffice 7 )

lørdag den 24. september 2011

SuperOffice Powershell snap in updated

Updated 02-10-2011: Added new command called New-Smart Process. Read more about it here and here. Also made it easier to install as a module instead of PS Snapin
Updated 28-09-2011
: Fixed an error when creating new associates. Fixed a bug in the DSN links created by the snap-in. Added a handful more commands.

I’m starting to use the powershell snap in I created more and found quite a bit of bug’s, spelling errors and needed more commands for my tasks.
Here is a short list of commands

  • Close-SO7Session
    Close down any open connection to SuperOffice and unloads the appdomain
  • Decrypt-SO7H
    Decrypts an 7C: string. Used within superoffice.config to protect database username and password
  • Encrypt-SO7H
    Encrypts an text to an 7C: string. Used within superoffice.config to protect database username and password
  • Get-MySO7Identity
    Return an SO7Associate object representing who you are within the current connection to SuperOffice
  • Get-SO7Associate
    Return one or more SuperOffice Associate
  • Get-SO7config
    Get an empty config object. Used to represent an connection to SuperOffice
  • Get-SO7Credentials
    Get one or more SuperOffice Associate Credentials ( CRM5 / Active Directory / Tickets and so on )
  • Get-SO7Email
    return one or more Email Address from within SuperOffice
  • Get-SO7Person
    return one or more SuperOffice Person objects
  • Get-SO7Secret
    Used for Calculating CRM5 secrets
  • New-SO7Associate
    Create new SuperOffice Associate. Can auto create a Person if needed
  • New-SO7Credentials
    Attach/update credentials for an existing SuperOffice Associate
  • New-SO7Person
    Create new SuperOffice Person
  • Remove-SO7Associate
    Remove one or more SuperOffice Associates
  • Remove-SO7config
    Removes SuperOffice config files and removes DSN links
  • Remove-SO7Credentials
    Remove one or more SuperOffice Credentials
  • Remove-SO7Email
    Remove one or more SuperOffice Email Address
  • Remove-SO7Person
    Remove one or more SuperOffice Persons
  • Save-SO7config
    Save config object as SuperOffice config files ( superoffice.config/superoffice.ini and create DSN link )
  • Set-SO7config
    Set running configuration. (what superoffice database do you want to work with right now ) Will Close any existing connection and unload its appdomain
  • Set-SO7Person
    Update an existing SuperOffice person.

Download here. Compiles fine under both .NET 3.5 and 4.0.

torsdag den 22. september 2011

Implementing Active Federation in a Passive Federation website.

This post is mostly notes for my self. I spend a few hours trying to get this to work so didn’t want to loose it.

I have a running STS that works perfectly with Passive Federation. I provide a GUI where customers can authenticate them self though various social providers like Facebook and linked in, but they can also login using their own STS (mostly ADFS). I needed to offer this though Active Federation too. (user sends a Issue Request Token with a UserNameWSTrustBinding and I need to validate this against all STS providers I have registered for that username )

There is a billion examples on Google on how to do this but I kept getting HTTP error 404 when calling the web service. but after trying hundred of thousand's different web.config setups I finally managed to get it working.

So I create a class that can validate username and password

Imports Microsoft.IdentityModel.Tokens
Imports Microsoft.IdentityModel.Claims
Imports System.IdentityModel.Tokens

Public Class CustomUserNameSecurityTokenHandler
Inherits UserNameSecurityTokenHandler

Private _CanValidateToken As Boolean = True
Public Overrides ReadOnly Property CanValidateToken() As Boolean
Get
Return _CanValidateToken
End Get
End Property

Public Overrides Function ValidateToken(token As System.IdentityModel.Tokens.SecurityToken) As Microsoft.IdentityModel.Claims.ClaimsIdentityCollection

Dim userNameToken As UserNameSecurityToken = TryCast(token, UserNameSecurityToken)
If userNameToken Is Nothing Then
Throw New ArgumentException("The security token is not a valid username token.")
End If

If userNameToken.UserName = userNameToken.Password Then
Dim identity As IClaimsIdentity = New ClaimsIdentity()
identity.Claims.Add(
New Claim(ClaimTypes.Name, userNameToken.UserName))
identity.Claims.Add(
New Claim(ClaimTypes.Role, "NoobsAndMorrons"))
Return New ClaimsIdentityCollection(New IClaimsIdentity() {identity})
Else
Throw New InvalidOperationException("Username/password is incorrect in STS.")
End If
Return MyBase.ValidateToken(token)
End Function
End Class

And then I added these sections to my web.config file
First we need to remove Microsoft's default handler and add the above class


  <microsoft.identityModel>
<service>
<securityTokenHandlers>
<remove type="Microsoft.IdentityModel.Tokens.WindowsUserNameSecurityTokenHandler, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add type="CustomUserNameSecurityTokenHandler" />
</securityTokenHandlers>
<service>
<microsoft.identityModel>

Next we add a placeholder for the web service requests. Add an text file and rename it issue.svc, and add this to it.


<%@
ServiceHost
Factory
="Microsoft.IdentityModel.Protocols.WSTrust.WSTrustServiceHostFactory"
Service
="CustomSecurityTokenServiceConfiguration"
%
>

Next go back to web.config and add this


  <system.serviceModel>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
<services>
<service name="Microsoft.IdentityModel.Protocols.WSTrust.WSTrustServiceContract" >
<endpoint address="" binding="customBinding" contract="Microsoft.IdentityModel.Protocols.WSTrust.IWSTrustFeb2005SyncContract"/>
<endpoint address="" binding="customBinding" contract="Microsoft.IdentityModel.Protocols.WSTrust.IWSTrust13SyncContract"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior>
<federatedServiceHostConfiguration />
<serviceDebug includeExceptionDetailInFaults="true" />
<serviceMetadata httpGetEnabled="true"/>
<serviceCredentials>
<serviceCertificate findValue="7A41CF269D6BCDED80DDD9B6FD517E37891453B5" storeLocation="LocalMachine" storeName="My" x509FindType="FindByThumbprint" />
</serviceCredentials>
</behavior>
</serviceBehaviors>
</behaviors>
<bindings>
<customBinding>
<binding >
<security authenticationMode="UserNameOverTransport" />
<httpsTransport />
</binding>
</customBinding>
</bindings>
<system.serviceModel>

Enjoy …I know I didn’t

Claims authentication made simple

I promised an simple example on how to authenticate against ADFS and get a token back. I honestly don’t think this code is production ready, but when your messing around with all this, it truly helps having some working example code to get a better understanding of what's going on.

So here a little solution to get you started.

  • Project ClaimsAuthentication
    This contains all the code needed to authenticate against an Secure Token Service. Most code is wrapped up to talk with Active Directory Federation Service 2.0, but it could be any STS that understands SAML 2.0 and WS-Federation.
  • Project CloudAPI
    Is an simple wrapper class used to call a web service that implements claimsbased authentication. I added it to give a few examples on how to get a token and then use it to authenticate with.
  • Project ClaimsAuthenticationTest
    Is a windows forms application that shows how to use the 2 other classes. In a fairly simple way, it try to make it simple to test Asymmetric/Symmetric/Bearer token types; How to use Windows/UserName/Certificate and IssuedToken Authentication schemes; And as a little goody, I also wrapped up a simple way to authenticate against an ADFS server, and then get back a FedAuth Cookie, that is needed when talking with SharePoint 2010. I also added a an example on how to upload and download from SharePoint 2010 after authentication.

Lets have a short look at what all this is. IF you download and run the sample you will see this

image

On the left you have the basic information needed to authenticate against an Secure Token Service ( here called Identity Provider )

  • Identity Provider: this could be an ADFS 2.0 server
  • Realm: What website URL or urn do you want to authenticate for.
  • Authenticate by : Windows/UserName/Certificate and IssuedToken
  • Key Type: (how to encrypt claims inside token) Asymmetric/Symmetric/Bearer
  • If you choose to authenticate by UserName, fill out username and password

And that’s it. If everything goes well, you will see “Token” on the right side show how long the token is valid for. If you choose bearer or if you have loaded a certificate with the private key for a Symmetric encrypted token. Claims will show you all the claims inside the token.

If you can get a token, you can now try and type in the URL of an SharePoint 2010 website in “SharePoint URI” and click “Get FedAuth”. The the 2 list box's in the bottom left corner will show all SharePoint list’s and all items inside each list. You can also upload and download files to the lists or delete items.

In the bottom right corner is 3 buttons. Each one represent different ways of talking with a Web Service that implements Claimsbased authentication. Note here ActAs is issued to “impersonate” another user though he's token. And require you to have permission to do this. If on the other hand you have an token with Symmetric signed claims you can reuse this token to authenticate again using “IssuedToken”. ( hint hint. This is why you want to sign your claims )

Project compiles fine under both .NET version 3.5 and 4.0 .. When you swap to 3.5 you will get 2 errors. Read my remarks above those 2 lines.

lørdag den 17. september 2011

SuperOffice 7 making my head hurt

Updated 23-09-2011: I moderated my comment about superoffice. That was childish of me
Updated 24-09-2011: New version updated with a ton of bug fixes .. read more here

My original approach to handling users in superoffice didn't work as expected. I don’t know why I didn’t catch this while testing ( I apparently don’t test well enough )

The whole point of this powershell add on was to make a simple way of managing users in multiple SuperOffice databases. Turns out the SuperOffice NetServer API is hardcoded to declare everything as internal static’s ( that’s private shared for us VB nuts ). They don’t expose a function to clear them, and pretty much everything inside the API is handled though static’s. And they don’t offer you a way to change configuration. Even SuperOffice.Configuration.ConfigFile.SetConfigFile fails. ( they check internal static variable superoffice.socontext._currentPrincipal . That means you can “sign-out” but you’re a “stuck” with the database you connected to the first time. If you change the configuration file (like I did in my old code) SuperOffice will now throw
Authentication failed
Failed to create Identity from tokens
If you (but you wont, its not nice to spy at others bad coding habit's ) look inside SOCore.dll with reflection you can easily see that, SuperOffice is talking with the database ( so DSN link, database and database username/password is correct ) but the identity token it created with your windows principal or CRM username/password doesn’t match anything in database. Since its still looking in the old database

I tried messing a bit around with reflection and overwrite the variables. But didn’t really get me anyway (but I learned a lot of new trick while trying ) so I decided to go down the path I read about on devnet.superoffice.com .

AppDomain … Now there is something I don’t know anything about. I know what it is, but I have never, ever tried coding with/against it.I created a ton of small test applications going down different paths, but nothing really seemed to work. *IF* I managed to get different things loaded, superoffice would go nuts and not work. Then after several hours of getting more and more desperate i found a very simple example (sorry lost the link) and that got me kicked in to gear.

So here is the updated version of my powershell add-on to manage users in SuperOffice. Its pretty much the same but

  • I renamed all functions to start with so7.
  • I’ve added a Type Formatting file ( .ps1xml ) so output looks better.
  • I fixed a small bug in the configuration files. SuperOffice client goes nuts when it see <security> tag, so removed that unless you specifically set the Symmetric keys.
  • And best of all. Now it works against multiple SuperOffice instances.

Enjoy ….

lørdag den 10. september 2011

Working with SuperOffice users though PowerShell

Updated 24-09-2011: Rewritten, see more here
So you maybe saw my ugly PowerShell scripts to work with SuperOffice in my last blog post. Here is a more “clean” way to handle this

I created a small PowerShell PSCmdlet that you can use to

  • List/Find/Create/Delete and Create Associates with Person objects
  • List/Find/Create/delete Associates
  • Create/Manage/delete License information for Associates
  • Create/Manage/save/remove SuperOffice configurations ( all with superoffice.config / superoffice.ini and the fucking lame DSN links )

Might be worth mentioning that an Associates is the same as an souser. in most cases you create a Person and then link that person to an Associates and then add credentials ( CRM5 / CRM7 / Windows and what ever ) to that person to give them the ability to access superoffice

 

  • Get-CRM7config
    Returns an empty CRM7Config object. This is what I use to manage connections to different SuperOffice installations and to create/remove client/server configurations
  • Remove-CRM7config
    Removes the files superoffice.config and superoffice.ini and the DSN link.
  • Save-CRM7config
    Creates and saves superoffice.config and superoffice.ini . if need to be called from a client or with a server installation, add –CreateDSN to also create that.
  • Set-CRM7config
    When calling all other powershell commands it looks for this configuration. (saved in local variable $CRM7Config )
  • Get-CRM7Person
    Gets all or a specific Person from SuperOffice
  • Remove-CRM7Person
    Removes the specified Person from SuperOffice (does not check if user also have Associates )
  • Get-CRM7User
    Get all or a specific Associate
  • New-CRM7User
    Creates a new Associate ( InternalAssociate, Anonymous, System, External or Resource). Create a Person if needed or associates with the one supplied.
    Also handles AD integration
  • Remove-CRM7User
    Delete the specified Associate. use –RemovePerson to clean all.
  • Get-CRM7UserLicense
    Returns a more code/user-friendly object you can use to manage permissions for a given user.
    Make your chances to license’s/permissions for the user and then call UpdateUserLicense on the object to save it.
  • Close-CRM7Session
    Close down any open session you might have to SuperOffice
  • Get-MyCRM7Identity
    Returns an instance of the user you are currently logged in as

Precompiled assembly's for .NET version 3.5 and 4.0 attached and the complete source code.
Updated 12-09-2011: I’ve fixed a few errors added a few more powershell commands and a lot more powershell parameters. Notably I’ve added –EncryptedDBUser to Save-CRM7config . That way you can get dbUser and dbpassword encrypted within the superoffice.config file

fredag den 9. september 2011

SuperOffice 7 – Creating a new user

24-09-2011: This can be done more smoothly, please see this post.

God I spend way to much time finding a solution for this

    Function CreateNewUser(mySession As SuperOffice.SoSession, ADLogin As Boolean, sFullName As String, loginName As String, sEmail As String, sPassword As String) As SuperOffice.CRM.Administration.SoUser
Dim soDB As SoDatabase = SoDatabase.GetCurrent()
Dim Person As Person = Person.CreateNew
Person.SetDefaults()
Person.Firstname
= Mid(sFullName, 1, InStr(sFullName, " ") - 1)
Person.Lastname
= Mid(sFullName, InStr(sFullName, " ") + 1)
Person.Contact
= GetPrimaryContact()
Person.Save()

Dim user As SuperOffice.CRM.Administration.SoUser = SuperOffice.CRM.Administration.SoUser.CreateNew(Person.PersonId, License.UserType.InternalAssociate)
user.LogonName
= loginName
'user.SetValidUserName(loginName)
user.LoginRight = True
user.LicenseFieldRight.DemandRight(CRM.Security.EFieldRight.Read)
user.SetPassword(sPassword)
user.RoleIdx
= 1
user.GroupIdx
= 1
user.OtherGroupIds
= New Integer() {2, 3, 4}
If ADLogin Then
Dim ADUser = FindADUSer(sEmail)
Dim SID As New System.Security.Principal.SecurityIdentifier(ADUser.Properties("objectSid").Value, 0)
user.AddCredential(
"ActiveDirectory", SID.Value, sEmail)
End If
user.Save()
Return user
End Function

So we start by creating a Person and then we associate a new user with this person. We set the basic information for the user and if the user is going to be a windows user, add some Windows Credentials by getting the user’s SID from ad.
Here is a quick way to find a user by UPN


    Function FindADUSer(upn As String) As System.DirectoryServices.DirectoryEntry
Try
Dim defaultNamingContext As String
Using rootDSE As New System.DirectoryServices.DirectoryEntry("LDAP://RootDSE")
defaultNamingContext
= rootDSE.Properties("defaultNamingContext").Value.ToString()
End Using

Dim entry As New System.DirectoryServices.DirectoryEntry("LDAP://" & defaultNamingContext)
Dim search As New System.DirectoryServices.DirectorySearcher(entry)
search.Filter
= "(userPrincipalName=" & upn & ")"
search.PropertiesToLoad.Add(
"sAMAccountName")
search.PropertiesToLoad.Add(
"userPrincipalName")
search.PropertiesToLoad.Add(
"objectSid")
Dim result As System.DirectoryServices.SearchResultCollection = search.FindAll()
For Each res As System.DirectoryServices.SearchResult In result
Return res.GetDirectoryEntry
Next
Catch ex As Exception
Throw ex
End Try
Throw New Exception("Could not find user in AD")
End Function

Cool, so we have a user that can login and do everything, (though code) but you cant open the SuperOffice client. To do that you need to assign a license to the user.


    Function GetIndexByModuleOwner(SOUser As SuperOffice.CRM.Administration.SoUser, ownername As String) As Integer
'SuperOffice.License.AssociateModuleLicenseOwner
For i As Integer = LBound(SOUser.Licenses) To UBound(SOUser.Licenses)
Dim lic = SOUser.Licenses(i)
If lic.Owner.OwnerName.ToLower = ownername.ToLower Then
Return i
End If
Next
Return 0
End Function

Sub SetUserLicense(User As SuperOffice.CRM.Administration.SoUser, Enabled As Boolean)
Dim licenseModuleOwner As Integer = GetIndexByModuleOwner(User, "Superoffice")
If licenseModuleOwner >= 0 Then
'LicAssocLnk[ModuleOwner (int) ][ModuleLicense (int or string)]
'User.Licenses(licenseModuleOwner)(SuperOffice.License.SoLicenseNames.Web).Assigned = True
User.Licenses(licenseModuleOwner)(SuperOffice.License.SoLicenseNames.User).Assigned = Enabled
User.Licenses(licenseModuleOwner)(SuperOffice.License.SoLicenseNames.Windows).Assigned
= Enabled
User.Save()
End If
End Sub

Sub SetTravelLicense(User As SuperOffice.CRM.Administration.SoUser, Enabled As Boolean, RemoteTravel As Boolean)
Dim licenseModuleOwner As Integer = GetIndexByModuleOwner(User, "Superoffice")
If licenseModuleOwner >= 0 Then
'LicAssocLnk[ModuleOwner (int) ][ModuleLicense (int or string)]
User.Licenses(licenseModuleOwner)(SuperOffice.License.SoLicenseNames.Travel).Assigned = Enabled
User.Licenses(licenseModuleOwner)(SuperOffice.License.SoLicenseNames.RemoteTravel).Assigned
= RemoteTravel
User.Save()
If Enabled Then SetUserLicense(User, Enabled)
End If
End Sub

I decided to split this up a bit. I’m going to be toggling the features a lot so makes more sense in my code.


So now we can just


        SoSession.Authenticate("user", "supersecretpassword")
Dim user As CRM.Administration.SoUser = CreateNewUser(mysession, True, "Allan Zimmermann", "AZ", "alz@wingu.dk", "MegaHemmeligt!")
SetUserLicense(user,
True)
SetTravelLicense(user,
True, True)