Mein UNDU Artikel

The Unofficial Newsletter of Delphi Users – by Robert Vivrette


Access Control System for a multiuser network application

By Thomas Kerkmann – thkerkmann at t-online.de

In large program systems, where the application is used in an organization with multiple departments and many users, you need a mechanism to control access to several groups of functions of the application, because not all employees may access all functions of your application. What you need now is a simple to manager access control system, that combines flexibility when your application grows with ease of manageabilty when your users change departments or new users need to be added or some removed.

The concept

Since the Delphi-library introduced tAction and tActionlist we have a nice component and mechanism to combine a central point of control over the programed functions and an automated way to update menus and buttons .enabled property. This will be one component of our access control system. Large applications are often database applications. So our second component is a database which stores the necessary information for the access control.

We’ll start with a look at the database.

The user-table

This table lists all users in our environment with some additional information. What we need for our access control system is only a user-id as a key to all other information. Additonal items may be stored for other purposes. My implementation looks like this.

tblUsers
Username Fullname Initials
Maier George Maier GM
Miller Hugo Miller HM
Smith John Smith JS

It doesn’t really matter which type of database you will use, but you should have an index to the keyitem – in this case the username. In a network environment you already have an authenticated user, and windows provides the name with the API function – GetUserName (…) –

The functions-table

In a similar way, we create a table of functions that the application supports. We need a unique key to this table to identify each function, a description and maybe some other information you like to have here.

tblFunctions
FunctionID Function
1 Supervisor
1000 Some function here
1001 Some function there
1002 Some other function
1003 Some special function
2000

As you can see, I use number areas to group functions together. This gives an easier overview for e.g. department dependent function groups of your application. By definition, I use FunctionID 1 to set Supervisor privileges.

The access-control-table

Now the access control table combines users to functions in a very easy way. For each user to function relation we’ll have a record in our table. This is very straight forward, easy to maintain and fast to retrieve.

tblUsersFunctions
Username FunctionID
Maier 1000
Maier 1001
Miller 1000
Miller 1001
Miller 1002
Smith 1000
Smith 2000

For maintenance and retrieval we need an index to each of the columns of this table. We will see later on, how we can minimize the size of this table by using user-groups. But for now, let this table grow a little bit in length, but still it is small because only two little columns are used. Let me tell you here already that the FunctionID should be in range of longint. You will see in the next chapter why.

The Application

Now we have our external datastructures, what about using them in our application.

Actions, like every Delphi component have a Tag property. This is of type longint and you will remember the function-id should be of the same type. For our application we will create an Actionlist with Actions for all the functions that need to be controlled. E.g. you will not need a controlled action for exiting the program, but it might be usefull to use an action anyway.

For this case we define that Actions with a Tag property of 0 are accessible by all users. All other actions will get the FunctionID as their Tag property value. So everytime you create an Action you will assign a function-id to it and later on create a database record in the function-table for it. Now you can control access to this function by assigning it to any user in the access-control table.

Now how do we implement this. I created an application wide datamodule with the user-database components and the actionlist. Yes, you can place the actionlist onto the datamodule and also place an imagelist component here, to assign images for toolbuttons that use the actions.

Figure 1: The datamodule

Having all this in mind, what will our program do on startup? First we need to find the username. Do this by using the API-function GetUserName.

Function GetNetworkUsername:string;
var
   buffer : array[0..128] of char;
   l      : dword;
begin
   l := SizeOf(buffer);
   GetUserName (buffer,l);
   if l>0 then
      result := StrPas(buffer)
   else
      result := 'GUEST';
end;

This is a utility function, you will find in the utilities unit.

procedure TDataModule2.DataModuleCreate(Sender: TObject);
begin
   FUsername := GetNetworkUsername;
   SetupSecurityUser (FuserName)
end;

Next we will set the enabled property of all functions this user may access. This is done by first reading the list of functions for this user. The tList class is perfect for doing this. Since it stores a list of Pointers which are compatible in size with LongInt we can typecast the function id to fit into tList. We have a private function for getting the users rights which will return TRUE if the list contains at least one executable function.

Function TDataModule2.GetRightsForUser (aUser:string; VAR aList:tList):boolean;
var
  cf     : LongInt;
  i      : integer;
  groups : tStringlist;
  procedure ReadRightList (const aUser:string);
  begin
     if tblUsersFunctions.Findkey([aUser]) then
     begin
       while NOT tblUsersFunctions.Eof
        and (tblUsersFunctionsUSERID.value=aUser) do
         begin
           cf := Round(tblUsersFunctionsFUNCTIONID.value);
           if aList.IndexOf(Pointer(cf))<0 then // only add when not there
              aList.Add(Pointer(cf));
           tblUsersFunctions.Next;
         end;
      end;
  end;

begin // function TDatamodule2.GetRightsForUser
  aUser := AnsiUpperCase(aUser);
  aList.Clear;
  tblUsers.Open;
  tblUsersFunctions.Open;

  if tblUsers.FindKey([aUser]) then
   begin
     FFullName := tblUsersFULL_NAME.value;
     FInitials := tblUsersINITIALS.value;
     tblUsersFunctions.IndexName := 'USERID';
     ReadRightList (aUser);
     // This is an enhencement for supporting usergroups
     Groups := tStringList.Create;
     if GetUsersMemberGroups(aUser,Groups)>0 then
      for i:=0 to Groups.Count-1 do
       ReadRightList (UpperCase(Groups.Strings[i]));  // add group rights

     Groups.Free;
     // end of usergroups enhencement
   end;
  tblUsersFunctions.Close;
  tblUsers.Close;
  result := aList.Count>0;
  bSupervisor := aList.IndexOf(Pointer(1))>=0;
end; // function TDatamodule2.GetRightsForUser

And we also have a function for combining the rights with the Actions used. This function is called from our datamodule.create implementation. It’s called SetupSecurityUser.

Procedure TDatamodule2.SetupSecurityUser(const aUser:string);
var
  RightList : tList;
  i         : integer;
begin
  // /////////////////////////////////////////////////
  // Read Rights for User and set the Actionlist Items
  // /////////////////////////////////////////////////
  RightList := tList.Create;
  GetRightsForUser (AnsiUpperCase(aUser),RightList);
  with ActionList1 do
   for i:=0 to ActionCount-1 do
    if Actions[i] is tAction then
     begin
       TAction(Actions[i]).enabled := bSupervisor
          or (Actions[i].Tag=0)
          or (RightList.IndexOf(Pointer(Actions[i].Tag))>=0 );
     end;
  RightList.Free;
end;

Now our Actions are enabled / disabled as defined in the user-database. What do you think, easy eh…?

The Usermanager

The Usermanager is an application that will maintain the tables discussed above. I just want to show how it looks like, you will directly understand how it works if you remember the first chapter.

Figure 2: Usermanager (users view)
Figure 2: Usermanager (users view)

In the left pane you see every user from our Usertable. The right pane shows all functions from the function table. Each checked function is a record in our UsersFunction table. When removing the checkmark the record is deleted, when setting the checkmark a record with Userid and FunctionID is created.

In my real application, I do not really delete the record, because I use a dBase database. This will grow with every change, since records are not removed on delete. What I do is set a special keyvalue ‚***‘ that marks deleted records. When appending, I first look for one of these and reuse them. If no previously deleted record is available I do a real append.

Figure 3: Usermanager (functions view)
Figure 3: Usermanager (functions view)

In the functios view, you can add functions to the table and you can switch users for a function as allowed or not.

There are also functions to copy and paste a profile to another user (in the User-view), or to allow or disallow a special function for all or no users at all (in the Functions-view).

Conclusion

This system now is very easy to manage, easy to use in your applications and you will find many more features you could include to make it more comfortable and robust.

In the project I did for my company, there are additional functions available, that can read the userdatabase of our NT-Network. So it is easy to add users to the programs user-control. You can add a usergroup to the userlist which will have rights for all users in this group. These are combined to the individual rights for a user.

The complete sample code can be downloaded in the download section.