Of late I have been getting down and dirty with the Database Development tools in Visual Studio 2010. You may know this feature set by one of the plethora of other names it has had over recent years such as:
- Visual Studio Team System for Database Professionals
For the rest of this post I’ll stick with the colloquial name that most people seem to recognise – datadude.
Regardless of which moniker you prefer I’m hoping you recognise the feature set that I am referring to here which includes:
- Schema Compare
- Data Compare
- Offline database development
- T-SQL refactoring
- Database unit testing/Data generation
- MSBuild integration
- various other things…
I plan to write a detailed blog post in the coming weeks talking about my overall experiences with datadude but in the meantime I’m writing this blog post to focus on how I have chosen to manage security; by which I mean:
- role membership
- user and role permissions
Security in Post-Deployment scripts
Datadude has built-in support for managing all of this stuff as we can see from this screenshot:
Notice that we have created a user, a login for that user and a role into which we later want to add the user. They exist as explicit objects within our project which means that datadude “knows” about them and that has a number of advantages including:
- Datadude can include these objects in a schema compare
- Datadude can do dependency tracking thus enabling offline development (e.g. If the login that is referenced in the CREATE USER statement does not exist then a design-time error will be raised)
That should be enough to justify using datadude to manage users and roles however I have come across a limitation that prohibits us from doing so; we do not deploy the same security model to each of our environments. The reasons for this are very familiar to you I’m sure, we have people that require different permissions on different environments (e.g. our developers will want to be able to issue DDL and UPDATE/INSERT/DELETE on our development environment but we definitely don’t want them to have the same on production).
As far as I can discern datadude does not have a good solution for dealing with this situation. I have not found a way of conditionally deploying objects depending on which environment we are deploying to. Actually that is not altogether true, we could have a Visual Studio configuration for each environment and include/exclude objects as we choose but that doesn’t play well with the build and deploy model of msbuild which requires that you build each one of those configurations; each of our builds takes about 10 minutes which, when multiplied by the number of environments, results in a total build time that takes too long for our continuous integration (CI) usage.
Hence we needed a better way to deploy our security model – we chose to use PostDeployment scripts. PostDeployment scripts do exactly what they say on the tin – they are scripts that get run after datadude has deployed the objects that it knows about. They get called (using SQLCMD syntax) from the Script.PostDeployment.sql file that exists in every datadude project:
My chosen approach is to have a script called SecurityAdditionsWrapper.sql that gets called from Script.PostDeployment.sql. SecurityAdditionsWrapper.sql checks a SQLCMD variable to determine which environment is being deployed to and then calls the appropriate SecurityAdditionsXXX.sql script that is responsible for deploying the security model for that environment (where XXX is the environment).
I then have 3 subfolders that contain all the individual CREATE USER, role membership and permissions scripts that get called from SecurityAdditionsXXX.sql as and when required.
The screenshot below shows the contents of Script.PostDeployment.sql & SecurityAdditionsWrapper.sql and also the files as they exist in Solution Explorer:
Some things to notice:
- I have a SQLCMD variable called $(DeployType) that will contain the value “DEV”, “UAT” or “PRD”
- All of my security scripts exist in Post-Deployment -> SecurityAdditions
- In the commented-out code you can see that I was trying to be clever and dynamically build the name of the file to be executed using the $(DeployType) variable – the thinking being that if we later added a new environment type I wouldn’t need to change this script. Unfortunately even though this is valid SQLCMD syntax it does not work in datadude; I have reported that issue on Connect as a bug (with repro) at [Datadude] Valid SQLCMD syntax throws error at build time.
You may be wondering how we get a value into that $(DeployType) variable when using msbuild. Well, its pretty simple when you know how. First you need to include the variable in the sqlcmdvars file for your datadude project:
You then need to pass in the appropriate value to that variable at deploy-time. We are using VSDBCMD.exe to run our deployments and the syntax to pass in a value for a variable is /p:PropertyName=PropertyValue. We are calling VSDBCMD.exe from inside an msbuild script which therefore looks like this:
<Exec Command="..\Tools\VSDBCMD\vsdbcmd.exe /Action:Deploy /ConnectionString:'Data Source=$(DatabaseServer);Integrated Security=True;Pooling=False' /DeployToDatabase:+ /ManifestFile:.\MyDB.deploymanifest /p:DeployType="$(DeployType)" " />
$(DeployType) is of course also a property within our msbuild file, which I am drawing attention to in the bold section above.
In conclusion, I am not saying that this is the correct way to deploy a SQL Server security model using datadude, it just happens to be the method that I have chosen. I would really like to how people have solved this using datadude so if you have any anecdotes to share please put them in the comments.
One more thing…
Cranking all of those files out by hand is a laborious undertaking, especially if you have lots of users with lots of permissions. In our case we introduced datadude to be used with an existing system hence we already had all our security setup on existing servers but we had no good way of getting it into the structure that I explained above. Powershell and SQLCMD to the rescue!
I have produced a series of scripts that interrogate a named instance and produce all of the files that I explain above. Namely:
- A file for each user which gets dropped into %SolutionRoot%\%ProjectName%\Scripts\Post-Deployment\Users\
- A file for each principle that issues all the GRANT/DENY EXECUTE/SELECT/INSERT/UPDATE/DELETE for that principle, it gets dropped into %SolutionRoot%\%ProjectName%\Scripts\Post-Deployment\PermissionsSets\
- A file that specifies all of the role memberships, for all database principles.
Creating the CREATE USER file and the file containing the permissions for each principle is quite involved. For that we need some nested loops that loop over (a) all the databases for which you want to scripts users and (b) all the principles in each database. It then dumps the appropriate files into the appropriate folders and then you have the simple task of adding them to your project and calling them from your Post Deployment script.
The files are stored in a zip folder on my OneDrive at http://1drv.ms/11tdPML
To run the Powershell script simply unzip all the files into the root folder of your solution and issue:
>GeneratePermissions.ps1 -SQLInstance instanceName -Environment XXX
changing SQLInstance & XXX accordingly (the $Environment variable is simply a suffix that is added to the names of some of the generated files). The Powershell script contains more detailed notes within it in the comments section at the top.
Hope this helps!
Disclaimer: I’m giving this away for free and I won’t be supporting it so use at your own peril (in other words, don’t come here complaining that it doesn’t work on your environment). On the other hand if you find a problem and are able to fix it – please let me know
UPDATE: I did actually find some problems with the scripts and have now uploaded a new set. I'm confident that they'll work this time.
UPDATE 2: I've updated the scripts again. They now create the SecurityAdditionsXXX.sql file as well as the individual CREATE USER & permissions files; SecurityAdditionsXXX.sql contains all of the calls to the relevant scripts for environment XXX.
SecurityAdditionsXXX.sql also issues all of the role membership statements as well. So just to clarify - you now no longer have a need to run the script that generates the role memberships - GeneratePermissions.ps1 takes care of everything.
UPDATE 3: Peter Schott has taken this, adapted it for hiw own needs, and sent it back to me so that I can share it here. His modified version of the scripts is at http://1drv.ms/1uimCKV and includes these changes (mainly to deal with roles):
1. Changed the Powershell script:
* Added a section to handle role permissions. (really don't care for the way VS does it)
* Tweaked the Database Principle section - for some reason, the variable in the PS script I extracted wasn't set properly so I just changed the names around
* Added a bunch of lines of code to add to the Array. My Powershell's not too strong so wasn't sure about the best way to add new DBs/Projects to the DB Array. I ended up doing a bunch of sets. I know it's not ideal, but I just wanted to get it done one time.
* Tweaked the section that creates the Users. If one of those logins doesn't exist, the grant permissions script would stop. Now I check to make sure that the login exists before the script runs. I sometimes get empty (Login = ) sections. No ideas on that yet. I fix or delete that file as appropriate.
2. Added a "GetDatabaseRoleList.sql" file. Gets the roles for the DB so we can run it through the Permissions file
3. Permissions SQL file. I tweaked this with a query that UNIONs the object permissions with the Schema permissions. I didn't break it down further
like you did, but our permissions are so messed up right now that it doesn't matter much to me. (we have far too many permissions granted to
individual users instead of roles because the prior DBA didn't understand the point of DB Roles for permissions and even then far too many permissions because we had a job that nightly granted all permissions on all objects to certain usernames :-P )
And one more change that needs to be made which Peter informed me of later:
Ended up changing the GeneratePermissions.ps1 file to change all of the Out-File
to "Out-File -width 500 ".
After doing that, the permissions weren't truncated at 80 characters. Also tweaked the Invoke-SQLCmd functions to use -MaxCharLength 500 so the output line would be long enough. That may have been overkill or there may be a better way to handle, but some of my permission lines were definitely longer than the allowed line lengths.
P.S. I have used Powershell in the past but this is the first time I have really got into the inner workings of it and I’ve got to tell you – its just fantastic. The productivity you get is just incredible – if you don’t believe me have a look at the script herein and see how easy it is to generate a dataset from SQL Server and then loop over it inside your script. Its two lines of code - really fantastic stuff – I recommend any SQL Server pro go out and spend some time getting to know Powershell.