IsInRole While Disconnected; No Longer IsInHole

In an application I am continuing to write, during startup, the user’s security is determined by the user’s security groups and permissions are granted within the program based on the group membership.  This has worked fine.  Then one day in an airport I wanted to work on some documentation and I quickly discovered that I could not run the program.  I could not run the program because I was not on the VPN and the Active Directory (AD) groups could not be enumerated to check my permissions.  At the time, I was kind of grateful I couldn’t do any work and didn’t give it much more thought. Our application is not designed to be run in a disconnected fashion.  You have to be on the VPN to get to the database server and use it.  Lately, I revisited the problem and decided to resolve it.

In my particular instance, I had a database running locally, but I was missing an Active Directory server to read from.  I had my cached credentials, shouldn’t that be enough?  Well, it is, if you do it the hard way.

Everyone should know that a user or a group is just a name.  There is an ID behind that group or user, which allows you to rename the group/user without breaking anything.  Behind the scenes, Windows always uses the ID.  The name is just for your benefit.

Using the excellent tool WhoAmI, I saw the following (don’t chide me for running as Administrator):

C:\>whoami /groups /sid

[Group  1] = "700CB\Domain Users"  S-1-5-21-2454202000-1896829455-2950045386-513
[Group  2] = "Everyone"  S-1-1-0
[Group  3] = "DA3\Debugger Users"  S-1-5-21-854245398-1547161642-725345543-1007
[Group  4] = "DA3\Offer Remote Assistance Helpers"  S-1-5-21-854245398-1547161642-725345543-1004
[Group  5] = "BUILTIN\Users"  S-1-5-32-545
[Group  6] = "BUILTIN\Administrators"  S-1-5-32-544
[Group  7] = "NT AUTHORITY\INTERACTIVE"  S-1-5-4
[Group  8] = "NT AUTHORITY\Authenticated Users"  S-1-5-11
[Group  9] = "LOCAL"  S-1-2-0
[Group 10] = "700CB\Domain Admins"  S-1-5-21-2454202000-1896829455-2950045386-512

When disconnected form the LAN (you might need to reboot while disconnected to clear the cache), it looks like:

C:\>whoami /groups /sid

[Group  1] = ""  S-1-5-21-2454202000-1896829455-2950045386-513
[Group  2] = "Everyone"  S-1-1-0
[Group  3] = "DA3\Debugger Users"  S-1-5-21-854245398-1547161642-725345543-1007
[Group  4] = "DA3\Offer Remote Assistance Helpers"  S-1-5-21-854245398-1547161642-725345543-1004
[Group  5] = "BUILTIN\Users"  S-1-5-32-545
[Group  6] = "BUILTIN\Administrators"  S-1-5-32-544
[Group  7] = "NT AUTHORITY\INTERACTIVE"  S-1-5-4
[Group  8] = "NT AUTHORITY\Authenticated Users"  S-1-5-11
[Group  9] = "LOCAL"  S-1-2-0
[Group 10] = ""  S-1-5-21-2454202000-1896829455-2950045386-512

My local group names were resolved to their names, but my domain group names couldn’t resolve because AD was unreachable.  My cached profile still had the SIDs though.

So, if I could do an IsInRole check using the SID instead of the domain group name, I’d be golden.  And this is just what I did.

Imports System.Security.Principal
Imports System.Threading

Dim sid As New SecurityIdentifier("S-1-5-21-1859785585-1835888107-1082013118-1025")
Dim p As WindowsPrincipal = CType(Thread.CurrentPrincipal, WindowsPrincipal)

MsgBox(p.IsInRole(sid))

So I took the SID for the groups I was testing for and tested for them instead.  Obviously, if an admin deleted and recreated the group thinking nobody would notice, it would be hell to troubleshoot, so if you’re not fully in control of your environment, you might want to steer clear.  Maybe do the SID after checking IsNetworkAvailable to reduce the exposure to failure?

But for me, it works like a champ, and now I can work in airports.  Hmmm.  Why did I figure this out again?

Columns Autosize: Listview in List View

Here’s another little snip of code I couldn’t find online when I needed it.  Geez, when I became a programmer, I wasn’t thinking I’d have to actually figure things out on my own.  That’s a lot of work.

Anyway, the problem I faced was when I had a Listview control on a form and I changed views from anything to List view, the columns of the items were really small, so I’d get ellipses’ after all the entries.  Sure, I could just set the column with to some obnoxious amount like 500, but that’s a waste of space.  So after searching and seeing a bunch of postings about using a Win32 API call to autoresize the column, then getting disappointed because it was for VB 4/5/6, I just hacked through it.

The Details view has an autosize feature, but apparently they didn’t extend it to the List view.  But we can still make use of it.  Why not switch to detail, set the autosize of column 0 to true, then measure how wide it makes the column, then use that as the column width in List view?  That’s a dumb idea.  Who would do something like that.  Oh, what do you know, it works.

Dim maxSize As Integer

lstItems.View = View.Details
lstItems.Columns(0).AutoResize(ColumnHeaderAutoResizeStyle.ColumnContent)
maxSize = lstItems.Columns(0).Width

lstItems.View = View.List
lstItems.Columns(0).Width = maxSize
lstItems.Refresh()

Yay.

A Totally Non-Random Post

Originally posted to SOAPitStop.com, Jun 25, 2008.

I am sure this is covered somewhere on the great big Internet, but it’s new to me.  And it sucked until I discovered what the problem was.  Basically, I had random values being generated that were anything but random.  “Oh, you forgot to set a seed value to something pseudo-random like now.millisecond.”  No.  That didn’t help.  Check out this code and see what the results are:

    Private Sub UnRandom()
        Dim r As New Random(Now.Millisecond)

        Dim winCount, winCount2 As Integer

        For i As Integer = 1 To 100
            If Rndm() > 50 Then winCount += 1
        Next

        For i As Integer = 1 To 100
            If r.Next(1, 100) > 50 Then winCount2 += 1
        Next

        MsgBox(winCount & ":" & winCount2)

    End Sub

    Private Function Rndm() As Integer
        Dim r As New Random(Now.Millisecond)

        Return r.Next(1, 100)
    End Function

Assuming a 50/50 chance of win, you would expect that a Random.Next would return a random value.  Seemingly not when it gets continually instantiated like in the Rndm function.  So this apparently is not a case where you want all of your variables to be contained in their own methods.  You’d probably want to have one single random number generator for your whole application.

In Log We Trust (or why did you have to make it so difficult?)

I guess this might be kind of a belated rant… some will know what i’m talking about. At a previous job, we had a need to log stuff: errors, debug info, etc. We were given an API by the upper programmers and it was, shall we say, less than excellent. It was based on some MS application block, which had dependancies on a bunch of other application blocks and made your config files massive. But, boy was it configurable. Blah. The most difficult part was writing out the signature of the method and the parameters. We couldn’t consider doing the values passed in yet. Every time we changed the method signature, we had to alter all the logging statements. We usually ignored logging until late in development when the methods would stabilize.

So I proposed a more simple way. I mean, Reflection is supposed to know all this stuff, right? Why not have it interrogate the current method and pass it and its parameters somewhere to be logged, or at least generated. It was declined. I didn’t take it personally, but inside I was using an evil villain voice: "you fools!"

So when I left, I took my ideas with me and quickly built this into my new application suite. It’s not as wicked-cool as having a config file that can switch between text file and sql logging, in fact, it has NO config file. The simplicity of the logging is the key.

So let’s say you want to log an error. This is all you write:

Try

Catch ex As Exception
    Logging.LogError(ex, System.Reflection.MethodInfo.GetCurrentMethod)

End Try

And it’s the same for every method. That right there is worth its weight in code. But what about when you have parameters? You would need to capture them too. The code is almost unbearable at that point, right?

Try

Catch ex As Exception
    Dim errorParms As New System.Collections.Specialized.StringCollection
    With errorParms
        .Add(parm1)
        .Add(parm2.ToString)
    End With
    Logging.LogError(ex, System.Reflection.MethodInfo.GetCurrentMethod, errorParms)
End Try

So what’s the error log look like? It’s tab-delimited and has the basics of what you need:

DATE USER METHOD ERROR MESSAGE

4/16/2008 9:04:30 PM 700CB\anachostic WindowsApplication1.Form1.LogWithoutParameters(parm1,parm2) This is a generated error.

4/16/2008 9:04:30 PM 700CB\anachostic WindowsApplication1.Form1.LogWithParameters(parm1=This is parm 1,parm2=12345) This is a generated error.

Have I sold it yet? Or at least given you a starting point for your own logging "framework"? Ahem. Please, it’s just a simple class. Here it is for your plunder:

Imports System.Reflection
Imports System.Collections.Specialized

Public Class Logging

    Public Shared LOGFILE_NAME As String

    Shared Sub New()
        LOGFILE_NAME = "c:\" & My.Application.Info.ProductName & "-Errors.log"

    End Sub

    Shared Sub LogError(ByVal ex As Exception, ByVal method As MethodBase)
        LogError(ex, method, Nothing)
    End Sub

    Shared Sub LogError(ByVal ex As Exception, ByVal method As MethodBase, _
        ByVal parameterValues As StringCollection)

        Dim sb As New System.Text.StringBuilder

        Try
            With sb
                .Append(Now.ToString)
                .Append(vbTab)
                .Append(My.User.CurrentPrincipal.Identity.Name)
                .Append(vbTab)

                If method IsNot Nothing Then
                    .Append(method.ReflectedType.FullName & "." & method.Name)
                    .Append("(")


                    For i As Integer = 0 To method.GetParameters.Length - 1
                        .Append(method.GetParameters(i).Name)
                        If parameterValues IsNot Nothing _
                            AndAlso i < parameterValues.Count Then
                            .Append("=" & parameterValues(i).Replace(vbCrLf, "<CR>"))
                        End If

                        .Append(",")

                    Next

                    If sb.ToString.EndsWith(",") Then sb.Length -= 1

                    .Append(")")

                End If

                .Append(vbTab)

                If ex IsNot Nothing Then
                    .Append(ex.Message.Replace(vbCrLf, "<CR>"))
                End If

            End With

        Catch exc As Exception
            sb.Append("<ERROR WHILE PARSING: " & exc.Message & ">")
        End Try

        LogMessage(sb.ToString)

    End Sub

    Shared Sub LogMessage(ByVal msg As String)
        Try
            If Not IO.File.Exists(LOGFILE_NAME) Then
                IO.File.WriteAllText(LOGFILE_NAME, "DATE" _
                    & vbTab & "USER" _
                    & vbTab & "METHOD" _
                    & vbTab & "ERROR MESSAGE" _
                    & vbCrLf)

            End If

            IO.File.AppendAllText(LOGFILE_NAME, msg & vbCrLf)

        Catch ex As Exception

        End Try

    End Sub

End Class

In CASE you were wondering…

Here’s a thought I had a couple of days ago. I actually was almost done with this blog entry yesterday, but ended up closing the window and not saving any of it. Argh.

My thought was on sql sorting. There are times when you want to have your results sorted at the server based on a parameter. Those of you who like dynamic sql would be all over that. It’s so simple (in Northwind):

declare @SortType int
declare @sql varchar(2000)
-- SortTypes
-- 1- Lastname
-- 2- Firstname
-- 3- Employeeed
-- 4- Extension
-- 5- postalcode

set @sorttype=4

set @sql='select * from employees'
if @sorttype=1 set @sql=@sql + ' order by lastname'
if @sorttype=2 set @sql=@sql + ' order by firstname'
if @sorttype=3 set @sql=@sql + ' order by employeeid'
if @sorttype=4 set @sql=@sql + ' order by extension'
if @sorttype=5 set @sql=@sql + ' order by postalcode'

exec(@sql)

Ok. But as I’ve said before, I am not a fan of dynamic sql. And my environment doesn’t allow it because all security is done through the stored procedures. So, how do we do the same thing without dynamic sql? It’s a great use of the CASE function.

Let’s start with the first two sort types: lastname and firstname. you can put the CASE in the ORDER BY section:

select *
from employees
order by case @sorttype
    when 1 then lastname
    when 2 then firstname
    end 

That was simple. So let’s add the third type, employeeid.

order by case @sorttype
    when 1 then lastname
    when 2 then firstname
    when 3 then employeeid
    end

Works well. Wait, no it doesn’t. Sorting by type 3 is fine, but sorting bu type 1 or 2 gives:

Msg 245, Level 16, State 1, Line 12
Conversion failed when converting the nvarchar value 'Davolio' to data type int.

Ok, that’s not a problem, we’ll just convert everything to varchar. So now it’s like:

order by case @sorttype
    when 1 then lastname
    when 2 then firstname
    when 3 then cast(employeeid as varchar)
    end 

That works, kind of. You won’t notice the bug on Northwind, but you will when we do the next type, extension. We need to convert it to varchar also.

order by case @sorttype
    when 1 then lastname
    when 2 then firstname
    when 3 then cast(employeeid as varchar)
    when 4 then cast(extension as varchar)
    end 

Wow, that’s not right. 2344 doesn’t come before 428. But it does in the string world, which you forced your way into. There is a solution. we just need to pad the numbers with zeros. But how many zeros? All the values have to be the same length to be considered equivalent. so now we have:

order by case @sorttype
    when 1 then lastname
    when 2 then firstname
    when 3 then cast(employeeid as varchar)
    when 4 then replicate('0',10-len(extension))+ extension
    end 

That works fine. And we’ll have the same problem with postalcode, which is numeric and character, so we need to pad the beginning of those to be equal-length strings. The final version:

select *
from employees
order by case @sorttype
    when 1 then lastname
    when 2 then firstname
    when 3 then cast(employeeid as varchar)
    when 4 then replicate('0',10-len(extension))+ extension
    when 5 then replicate('0',20-len(postalcode))+ postalcode
    end 

You’d want to make the length of the postalcode be the max length of the field, in case it’s blank. But the point is CASE is a very powerful tool. It can be used anywhere a single value can be used. In ORDER BY, in JOIN, in SELECT, it’s universal. Now I’m going to save this before I lose it again.

It’s like the “Bag of Holding” for arrays

Here’s a little bit of code I find amusing. Haven’t you ever wanted an array that never ends? Or maybe better described as an array that loops? So if you have 10 items in your array and you ask for number 11, you get the first element again? I’m sure I have, but I don’t remember when or why.

Anyway, here’s a collection you can use for an (almost) infinite collection. It was actually a bit of trial and error to get the negative values to work properly.

Public Class ForeverCollection(Of T)
    Inherits Generic.List(Of T)

    Default Public Shadows ReadOnly Property Item(ByVal index As Integer) As T
        Get
            If Me.Count = 0 Then Throw New ArgumentOutOfRangeException("index")

            If index >= 0 Then
                Return MyBase.Item(Math.Abs(index Mod Me.Count))
            Else
                Return MyBase.Item(Me.Count - Math.Abs((index + 1) Mod Me.Count) - 1)
            End If

        End Get
    End Property
End Class

This uses the MOD operator which is a pretty fun bit of code. Its purpose is to return the leftover of any integer division. So 10 MOD 10 is 0, 12 MOD 10 is 2, 34 MOD 10 is 4. What good is that? Well, when you see the examples, it will make more sense.

You can use MOD for time calculations. You have 543 seconds on a timer. How many minutes and seconds is that? Which code is cooler?

allSecs = 543
min = 543 \ 60
sec = allSecs - (min * 60)
MsgBox(min & ":" & sec)or 

allSecs = 543
min = allSecs \ 60
sec = allSecs Mod 60
MsgBox(min & ":" & sec)

You can use MOD as a trigger. Lets say you have a counter and you want to annoy the user every X number of iterations.

For x As Integer = 1 To 1000
    If x Mod 15 = 0 Then
        MsgBox(String.Format("I've bugged you {0} time(s) " _
            & "and the counter is at {1}", (x / 15), x))
    End If
Next

You can use MOD for pseudo-random seed data in SQL. MOD in SQL is "%". Take the milliseconds of the current time and mod it by different numbers.

declare @i int
set @i=0
while @i<500
begin
    insert into userinfo(userid,createdate,logincount,
        lastwithdrawal,lastdeposit,currentbalance)
    select top 1 userid,getdate(),datepart(ms,getdate()),
        datepart(ms,getdate()) % 30,
        (datepart(ms,getdate()) % 15)*50,
        (datepart(ms,getdate()) % 25)*50,
        (datepart(ms,getdate()) % 10)*100
    from users
    order by newid()

    set @i=@i+1
end

I think of all the extra code I used to write before I discovered MOD and I think…