Back in those DOS and Win9x days you could pretty much dump files anywhere since there was no real filesystem security. On Windows 2000 and then on its minor update Windows XP, people carried on working in "DOS Mentality" by just making all users members of an elevated rights group such as Administrators or Power Users.
This gave Microsoft a black eye because users logged on with such accounts who used the Internet had opened a gaping hole for malware to come in and wreak havoc. A lot of email spam comes from zombied Windows XP machines even today.
In response this was modified beginning in Windows Vista through user Account Control (UAC). With UAC even if you were silly enough to have users log on as an Administrators account (Power Users was removed entirely) your session was no longer elevated. Instead elevated rights require special actions that raise a dialog on a Secure Desktop that malware can't just hijack and blat messages at to "click approval."
However when combined with NT security (pretty much the same model since at least NT 4.0) users can't just dump files and folders willy-nilly anymore. Lots of secured filesystem locations became off limits. This meant installed programs (in Program Files) began to either fail or run afoul of appcompat filesystem virtualization.
What To Do?
Well, there are lots of writeable locations. Each user has a Desktop, a Documents, and even AppData locations in which he can create, modify, delete, and do other things with folders and files. These work fine if the programmer takes any time to make use of them. But these don't work well for files "shared" among different users of the same PC.
Instead Windows has a CommonAppData special folder with special security on it. The DOS-visible name of this file can vary: on recent versions of Windows an English-language system calls this ProgramData.
The security on CommonAppData/ProgramData is such that a folder or file created within it has a special "Owner Access" applied to it. If user Joe creates a folder there he has full access, and all other users have basically read access. However since Joe "owns" it he can change the folder's security without elevation, and this altered security will be inherited by any folders or files created within it.
In order to avoid collisions between applications using ProgramData the convention is to create a "company" subfolder there, and within that create "product" or "application" subfolders to contain the folders and files of a given application that all users need access to.
How To Do It?
Windows Explorer, also known as Shell32, knows how to locate ProgramData by invariant code and can return the path to your programs. These "codes" are numeric values, assigned names prefixed ssf/CSIDL_ such as ssfCOMMONAPPDATA.
That gets you to the folder's path, and you can use MkDir to create subfolders, so you're nearly there!
To alter the security I've posted SetSec.bas before, but nobody seems to be using it. In an attempt to simplify this I have written ProgramData.bas as a "wrapper" for it. This gets things down to a simple function call.
Demo
The attached archive contains a simple program AnyUserUpdate.vbp, which doesn't do much.
The program looks for an application common data path and creates it as required, altering security at each created level to "Full Access" for members of the "Users" group. This is very liberal access and not correct for all situations, but it emulates the Wild West of those DOS/Win9x days to simplify programming for people. "Users" differs from "Everyone" in the post-XP era (starting in Windows Vista, "Everyone" no longer includes "Guest" accounts).
Once the demo program has this common folder and its path, it loads Text.txt into a TextBox for possible user editing.
When the program ends it checks for changes to the TextBox. If changes have been made it writes the altered Text.txt back out to disk, then logs the change to Log.txt (timestamp and user name) and exits.
All of the work required is now down to a one-liner:
A ProgramData subfolder is a good place for data common to all users. That might be program settings INI files, Jet MDB databases, or any other files common to all users that your application's program(s) need to be able to create, alter, or delete.
However you often have per-user settings and such too. These should go into a similar folder structure underneath ssfLOCALAPPDATA, but that's easy enough since there is no need to use SetSec.bas to alter security there. However you might want to add a new function to ProgramData.bas to look-up/create such a path too.
Installed Programs
If you use an installer to put your program(s) into Program Files you can still use ProgramData. For example maybe your program ships with an initial empty Jet MDB that all users will be updating.
Just have your program use ProgramData.Path() as above. Then check to see whether your XXX.MDB exists there, and if not copy XXX.MSB from App.Path to this ProgramData path. Then open the database as you normally would, but in the ProgramData path instead of in App.Path.
It's as easy as that!
Last edited by dilettante; Mar 19th, 2016 at 10:26 AM.
Compile the Project. Move the EXE to some location all users can run it from (the special folder Public is handy for this in Vista and later).
Then run it once, type some text, close the program. "Switch User" to another account. Run it again. Both users should be able to make changes and have their changes logged. Nobody needs to be an admin, no elevation is required.
You can also examine the folders' and files' Properties to see how security got set on things.
There is a distinct possibility that the code posted above could fail on a localized Windows OS. The built-in group "Users" might well have another name.
To help make the code more universal, the modified ProgramData.bas posted here tries looking the name up based on invariant information (i.e. the DOMAIN_ALIAS_RID_USERS constant). I haven't tested this on a localized OS yet, so feedback would be appreciated. Perhaps we can weed out any remaining flaws together.
The second version worked fine and the two text files were created. Even after deleting them the fist version works without error from both the IDE and the exe.
EDIT: The third version works fine as well
Last edited by Carlos Rocha; Mar 20th, 2016 at 12:16 PM.
Are you using a non-English Windows? I can't tell whether these group names change based on the language settings of the user running the program, or based on the "system language" of the installed Windows OS.
Hmm. I might have to try changing my language settings and displaying the fetched group name.
I'm running Windows 10 in Portuguese. The "Users" folder is renamed to "Utilizadores", but "ProgramData" remains unchanged. With Windows 7 (VirtualBox) the behavior is the same.
The "AMD" is your machine name. At the point where you are checking this (Properties dialog) the local machine name replaces the fixed keyword BUILTIN.
As far as I can tell from looking through TechNet and MSDN the BUILTIN keyword never gets localized, even though the well-known group names under it do get localized.
However none of that matters for version 2 (post #3) or version 3 (post #7 above) which will work in spite of localization because they look up the local names from invariant SID values.
Are you using a non-English Windows? I can't tell whether these group names change based on the language settings of the user running the program, or based on the "system language" of the installed Windows OS.
Hmm. I might have to try changing my language settings and displaying the fetched group name.
Version 3 works perfect. I've tried under Windows 7 Spanish version.
The LookupAliasFromRid function returns me "BUILTIN\Usuarios" for
DOMAIN_ALIAS_RID_USERS.
Full privileges are OK in each folder and files.
The change of users from Administrator to another limited, is OK.
Also others Spanish VersiĂłn tried under:
-Windows XP SP3 (VirtualBox), same, is OK: C:\Usuarios\All Users\Datos de programa\DemoLand Concepts\ProgramDataDemo
-Windows Vista x32 (VirtualBox) is OK but LookupAliasFromRid returns: "BUILTIN\Users" ... yes in english.
If the input text is Chinese there will be an error.
It sounds like you are talking about the file I/O being used in the demo Project. If you are working cross-locale then yes, of course there can be problems using ANSI/DBCS I/O.
However that has nothing to do with the topic here. The ProgramData class shouldn't have such issues. If it does this would be something to address. So was this just about the demonstration file? If so I'm not sure what you expect the ProgramData class to do about that.
Otherwise if you insist on working cross-locale you'll just have to use Unicode text I/O. Again, that has nothing to do with the topic at hand. Perhaps you need to start a question thread.
This version avoids even late-binding any of the Shell Automation objects and calls Shell32's flat API instead: SHGetFolderPath() to find the ProgramData folder.
Seems to work fine but as always test for yourself.
Otherwise it should operate identically to version 3 posted above. The change was only made because some people are uncomfortable with the Automation API.
Worked for me on both Windows 10 and Windows XP. I suspect your IDE is set to run as an administrator (hence getting access to the folder), but for some reason your user account doesn't have the proper access to the CSIDL_COMMON_APPDATA folder.
Worked for me on both Windows 10 and Windows XP. I suspect your IDE is set to run as an administrator (hence getting access to the folder), but for some reason your user account doesn't have the proper access to the CSIDL_COMMON_APPDATA folder.
Yes, I guess it's something like that, thought I believe the idea is to work for all users?
EDIT: It works after deleting the folder inside ProgramData
The error 75 is exactly what this is meant to prevent, so I'm curious since I am not seeing this occur.
Originally Posted by Carlos Rocha
I probably found the issue (if it's an issue at all). Just moved End If to the line before GiveFullControlTo Path, Group within the Function Path
So here is "before":
Code:
Path = Space$(MAX_PATH)
HRESULT = SHGetFolderPath(WIN32_NULL, _
CSIDL_COMMON_APPDATA, _
WIN32_NULL, _
SHGFP_TYPE_CURRENT, _
StrPtr(Path))
If HRESULT <> S_OK Then
Err.Raise &H80047200, _
"ProgramData", _
"SHGetFolderPath error &H" & Hex$(HRESULT)
End If
Path = Left$(Path, InStr(Path, vbNullChar) - 1)
Nodes = PathNodes(SubDirectoryPath)
'We are looking up the built-in "well known group" Users here:
Group = LookupAliasFromRid(vbNullString, DOMAIN_ALIAS_RID_USERS)
For NodeIndex = 0 To UBound(Nodes)
Path = Path & "\" & Nodes(NodeIndex)
If Not Exists(Path) Then
If CreateDirectory(StrPtr(Path), WIN32_NULL) = 0 Then
Err.Raise &H80047204, _
"ProgramData", _
"CreateDirectory system error " & CStr(Err.LastDllError)
End If
'Give full access rights to all normal logged-on users (i.e.
'not the Guest user if one even exists):
GiveFullControlTo Path, Group
End If
Next
Could you show the "after" code? I'm not sure what End If you are moving.
Path = Space$(MAX_PATH)
HRESULT = SHGetFolderPath(WIN32_NULL, _
CSIDL_COMMON_APPDATA, _
WIN32_NULL, _
SHGFP_TYPE_CURRENT, _
StrPtr(Path))
If HRESULT <> S_OK Then
Err.Raise &H80047200, _
"ProgramData", _
"SHGetFolderPath error &H" & Hex$(HRESULT)
End If
Path = Left$(Path, InStr(Path, vbNullChar) - 1)
Nodes = PathNodes(SubDirectoryPath)
'We are looking up the built-in "well known group" Users here:
Group = LookupAliasFromRid(vbNullString, DOMAIN_ALIAS_RID_USERS)
For NodeIndex = 0 To UBound(Nodes)
Path = Path & "\" & Nodes(NodeIndex)
If Not Exists(Path) Then
If CreateDirectory(StrPtr(Path), WIN32_NULL) = 0 Then
Err.Raise &H80047204, _
"ProgramData", _
"CreateDirectory system error " & CStr(Err.LastDllError)
End If
'Give full access rights to all normal logged-on users (i.e.
'not the Guest user if one even exists):
End If
GiveFullControlTo Path, Group
Next
Because if a directory level exists that means somebody created it. If somebody created it, the owner might be another user and then we don't have rights to set the security on it without being elevated.
The entire point of this is to create the path below CommonAppData, folder by folder, setting security at each level as we go. If any of the intermediate folders aready exist and we don't already have Full Access then it should fail.
Worked for me on both Windows 10 and Windows XP. I suspect your IDE is set to run as an administrator (hence getting access to the folder), but for some reason your user account doesn't have the proper access to the CSIDL_COMMON_APPDATA folder.
If the folder path and files do not already exist from a previous run the program should create them. As it creates the folder path it sets security to Full Access for all Users.
This should work the same way whether that first run is elevated or not.
After this first run, the only users who should be denied access are Guest (anonymous) users. I'm not trying to provide them access anyway.
Note that if you really wanted to do such a thing instead you could do something like:
Code:
Group1 = LookupAliasFromRid(vbNullString, DOMAIN_ALIAS_RID_USERS)
Group2 = LookupAliasFromRid(vbNullString, DOMAIN_ALIAS_RID_GUESTS)
For NodeIndex = 0 To UBound(Nodes)
Path = Path & "\" & Nodes(NodeIndex)
If Not Exists(Path) Then
If CreateDirectory(StrPtr(Path), WIN32_NULL) = 0 Then
Err.Raise &H80047204, _
"ProgramData", _
"CreateDirectory system error " & CStr(Err.LastDllError)
End If
GiveFullControlTo Path, Group1
GiveFullControlTo Path, Group2
End If
Next
The error 75 is exactly what this is meant to prevent, so I'm curious since I am not seeing this occur.
Could you show the "after" code? I'm not sure what End If you are moving.
Code:
Path = Left$(Path, InStr(Path, vbNullChar) - 1)
Nodes = PathNodes(SubDirectoryPath)
'We are looking up the built-in "well known group" Users here:
Group = LookupAliasFromRid(vbNullString, DOMAIN_ALIAS_RID_USERS)
For NodeIndex = 0 To UBound(Nodes)
Path = Path & "\" & Nodes(NodeIndex)
If Not Exists(Path) Then
If CreateDirectory(StrPtr(Path), WIN32_NULL) = 0 Then
Err.Raise &H80047204, _
"ProgramData", _
"CreateDirectory system error " & CStr(Err.LastDllError)
End If
'Give full access rights to all normal logged-on users (i.e.
'not the Guest user if one even exists):
End If
GiveFullControlTo Path, Group
Next
As I see it, the original code should grant access for every user in Users group at the time of path creation. What if another user is created after that? (It's not my case, anyway).
With the change the access is granted every time the program is executed even if not necessary, so probably not the perfect solution.
The folder was already there from previous runs, probably since first version. Could that be the reason for error 75?
As I see it, the original code should grant access for every user in Users group at the time of path creation.
That's what the code does before your modification. You modification breaks that and tries to change the security even if the folder already exists. You don't want to do that. While it may succeed in this case due to prior "Full Control" being granted, ideally you'd actually give these folders something somewhat less than that.
So your change is not needed, and in a more general case it would be wrong.
Originally Posted by Carlos Rocha
What if another user is created after that? (It's not my case, anyway).
The group is being given access, not a specific user.
Originally Posted by Carlos Rocha
The folder was already there from previous runs, probably since first version. Could that be the reason for error 75?
If a previous version ran to normal completion the folder should have been there and the security already changed, so no error 75. Since you got an error 75 something else must have happened, for example a previous run failing partway through.
I'd just delete the entire directory tree and start a new test from scratch. You can also examine the folder security using Explorer's Properties dialog.
Deleting the Path and executing 1st version I get the error 1332, the first level of the Path is created but with the wrong rights for Users (Utilizadores), I think: No Edit (Alterar), no Write (Escrever), and with Special permissions (Permissões especiais).
Executing then any other version (2, 3, or 4 original) everything seems to work. Yesterday there was no Special permissions for any Path level or the two text files, what I guess led to the error 75.
The strange is that I changed nothing within UAC since March (the time of first version).
Deleting the Path and recreating it with any version since 2 the Special permissions is only set to C:\ProgramData\DemoLand Concepts,(the one created with version 1 before the error 1332) so it seems windows didn't reset everything during path deletion.
This one builds on the previous ideas, adding another twist.
Often you will have multiple "common" files used by your program. But if you are going to use a first-run action to copy initial files to the [ProgramData] special folder it can be clunky to include them into your setup/installer one by one and they are still there later, taking up more disk than necessary.
So the idea here is to install these files "bundled up" as a Microsoft CAB file. This offers compression as well, so the CAB archive saves us some space, some fiddling, and we have an API we can use to extract the files as part of our first-run action.
First-run action: Create [ProgramData] path and extract CAB into it.
There is an additional ExtractCab.bas module here that uses the setupapi.dll in Windows to extract our bundled data files. This API cannot create, add, or delete CAB files but extraction isn't too hard. We only need the SetupIterateCabinet entrypoint to do this. You could also use a ZIP archive but we don't have good API support for those.
To create the CAB archive we'll use the MakeCAB.exe utility included in Windows. A directive file (.DDF) and batch command file are used to help automate this for us, making it easy to rebuild the CAB archive if we have to change the data files for a new version of our application.
See the ReadMe.txt for a rundown on the process to follow in testing this demo.