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.
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.
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.
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.