Introduction
A few years back, a client asked me to implement a quick and dirty “security mechanism” to control what data the myriad of users were able to view within their reports. There were numerous tables with multiple columns and all departments (within the enterprise) had their data within these tables.
SQLShack Industries has tasked us with creating a similar quick and dirty “security mechanism”. We shall attack this challenge by creating the necessary stored procedures (to extract the required data) and then utilize these stored procedures to render and consume the data within our reports.
Getting started
Opening SQL Server Management Studio we find our ‘YearlySales1’ table. This table contains the total monthly sales figures for two sales teams (Team1 and Team2). For the first portion of this exercise Team1 is ONLY permitted to view THEIR OWN data and Team2 THEIR OWN data.
The raw data maybe seen below:
Our first task is to construct a small security table called “SQLShackSecurity” (see below).
The client has two department heads and both head major corporate teams (see below).
Now that our infrastructure has been created, we are ready to create the necessary stored procedures to service the corporate reports.
Going forward we shall be working with a Reporting Services system variable User!UserID and it is the value of this variable that will be passed to the stored procedure from the report body. This variable is the key component to ensuring that the pieces fit together and function correctly (see ADDENDA B).
User!UserID contains the Domain and user ID of the individual requesting access to the data.
Let us have a quick look at the contents of the security table.
We note that there are two main users. User “ATRION\ssimon” is entitled to the Team 1 data and user “ATRION\SQLDB” is entitled to view only the Team2 data.
Constructing our stored procedures
Having created the necessary infrastructure, let us look at a small query which is going to return the required data to the appropriate teams.
We see that (as is) the query returns all the data for Team1 as user “ATRION\ssimon” is permitted to see Team1 data.
Changing the user to “ATRION\SQLDB” the following data is returned.
Restricting data columns
After a meeting with SQLShack Industries management, we find that there is additional data that must be locked down.
Many SQLShack Industries reports contain a Total Sales per month field and management feels that Team1 should be able to view all corporate monthly sales, whilst the remaining teams may only view their own results.
The following piece of code will help us achieve the desired results. Once again permissions are based upon the user ID detected upon entry into the Reporting Services / SharePoint Environment.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
declare @UserID as Varchar(50) declare @WhatICanSee as varchar (50) set @UserID = 'ATRION\ssimon' set @WhatICanSee = (Select Min(team) from [dbo].[SQLShackSecurity] where UserID = rtrim(ltrim(@UserID))) if NOT @WhatICanSee is null Begin select YearMth ,case when @WhatIcanSee <> 'Team1' then 0 else SUM (Team1_Revenue) end as Team1_Revenue , SUM (Team2_Revenue) as Team2_Revenue, case when @WhatIcanSee = 'Team1' then (SUM (Team1_Revenue) + SUM (Team2_Revenue)) else sum(Team2_Revenue) end as TTotal from( SELECT YearMth, case when name = 'Team1' then sales else 0 end as Team1_Revenue ,case when name = 'Team2' then sales else 0 end as Team2_Revenue from [dbo].[YearlySales1])a Group by YearMTH end |
A note of explanation is required for the following line of the code:
1 2 3 |
set @WhatICanSee = (Select Min(team) from [dbo].[SQLShackSecurity] where UserID = rtrim(ltrim(@UserID))) |
As we see in the table below, we may find the condition where a user has access to the data from more than one team. In this case, the rights are assigned based upon the rights allocated to the lowest team number (i.e. Team1).
ATRION\ssimon | Team1 GLOBAL RIGHTS |
ATRION\ssimon | Team2 Team 2 ONLY |
Running our query for user ATRION\SQLDB we find that the figure for the Total Sales / Revenue are as follows:
Changing the user to ATRION\ssimon we find the following results.
The astute reader will note that in the case of user ATRION\SQLDB, he or she is only able to see the data for Team2 and the total column reflects that.
User ATRION\ssimon, on the other hand is able to see all the data and therefore his or her total field reflects the summary total of sales for Team1 and Team2. We make one final change to this code and that is to add a new calculated field called “Monthee” which will contain the first three letters of the calendar month name (e.g. ‘Jan’). We utilize the RIGHT() function “RIGHT(YEARMTH,2)”.
Note the usage of the RIGHT function to set the calendar month name (see above).
Data format conversion for matrix based reporting
Financial management at SQLShack Industries has also tasked us with showing these results in the form of a matrix. The thought behind this is, that it makes the results clearer to the decision maker. It should be remembered that the data format that we have just finished dealing with, is probably more conducive for a chart. This said, we are now going to structure a NEW stored procedure that will render the data in a format suitable for a matrix.
We begin by adding twelve variables @Month01 through @Month12. By using the code at the top of the screen dump below, we are able to populate these twelve variables. The complete code listing may be found in ADDENDA A.
The eagle–eyed reader will ask, why declare and populate twelve variable fields, when the year and month already exist within the “YearlySales1” database table.
In approaching the extract in this manner, i.e. utilizing the twelve variables, we are certain of obtaining the current twelve month period (once again, please see the code in ADDENDA A). Remember that the table could contain data from a plethora of years.
Adding the following pivot code to our original code (that we used for the chart), we now have a query that is more conducive to a matrix format.
–– The values for @month01 through @month12 are calculated on the fly. The code to do so
–– may be found in the ADDENDA at the end of this article.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
set @sql = ' select name, [' + @month01+ '],[' + @month02+ '],[' +@month03+ '],[' +@month04+ '],[' +@month05 +'],[' + @month06+ '],[' +@month07+ '],[' +@month08+ '],[' +@month09+ '],[' +@month10+ '],[' + @month11+ '],[' +@month12 + '] from ' + ' ( ' + ' select Yearmth, name, value ' + ' from #rawdata1 ' + ' unpivot ' + ' ( ' + ' value for name in ([Team1_Revenue],[Team2_Revenue],[TTotal]) ' + ' ) unpiv '+ ' ) src ' + ' pivot ' + ' ( ' + ' sum(value) ' + ' for YearMth in ([' + @month01+ '],[' + @month02+ '],[' +@month03+ '],[' +@month04+ '],[' +@month05 +'],[' + @month06+ '],[' +@month07+ '],[' +@month08+ '],[' +@month09+ '],[' +@month10+ '],[' + @month11+ '],[' +@month12 +'] )) piv ' + ‘order by name asc ' set @sql1 = @sql CREATE TABLE #rawdata55( [name] [varchar](75) NULL, [Month01] [decimal](10, 2) NULL, [Month02] [decimal](10, 2) NULL, [Month03] [decimal](10, 2) NULL, [Month04] [decimal](10, 2) NULL, [Month05] [decimal](10, 2) NULL, [Month06] [decimal](10, 2) NULL, [Month07] [decimal](10, 2) NULL, [Month08] [decimal](10, 2) NULL, [Month09] [decimal](10, 2) NULL, [Month10] [decimal](10, 2) NULL, [Month11] [decimal](10, 2) NULL, [Month12] [decimal](10, 2) NULL ) --select @sql insert #rawdata55 exec sp_executesql @SQL1 with recompile select * from #rawdata55 END |
Now executing the query for user ATRION\ssimon we find:
and for user ATRION\SQLDB we find:
These are the results that we would have expected to obtain (see above).
Creating our first production reports
Opening SQL Server Data Tools, we create a new Reporting Services Project and name it “SQLShackReportingSecurity”. Should you be unfamiliar with Reporting Services or how to create a project, please do have a look at my earlier article entitled: “Now you see it, now you don’t”
Within this article I describe in great detail how to get started creating a Reporting Services project.
We first create a shared “Data Source” (see below). This data source contains the necessary information to connect to the source data within the SQL Server database.
Creating our first report
Within our project we right click on the report folder, select “Add” and “New Item”.
We add a new report as shown below:
We click “Add” and we are returned to the report drawing surface.
We are now going to create a vertical bar chart graph (see below).
For this exercise we shall be utilizing a column chart (see above).
Creating our dataset
In order for us to access the raw data from the database table (i.e. the data which will be consumed by our bar chart), we must first create a dataset. Should you be unfamiliar with the process or not understand the concept of a “dataset”, then please do have a look at one of my earlier articles “Now you see it, now you don’t”
We click the “New” button (see above). The local data source dialog box is brought up (see below).
We note above that we opt to use as “Shared Data Source”. This is the shared data source that we created at the start of the project. We click “OK” to leave the local data source dialog box and we are returned to the “DataSet Properties” box (see below).
We opt for the “Stored Procedure” as the “Query type” and select “ServiceRevenue01” option as the procedure that we wish to utilize. We click OK to exit the “Dataset Properties” dialog box and we are returned to the report drawing surface (see below).
Note our new dataset appears on the left hand side of the screen shot (see above).
We now resize our chart control and assign the dataset (that we have just created) as the source of data for the chart (see below and to the right).
We also add a title to the chart and call it “Revenue”. We set the charts series, values and category groups (See below).
As we have done in past sessions together, we shall be using the “YearMth” field purely as a sorting field and use the “month names” column, “Monthee” as the ‘labels’ for the X axis. Note the way that we prevent the values of “YearMth” from appearing when the report is run (see below).
Highlighting “YearMth” and selecting the “Category Group Properties” tab (see above) we bring up the “Category Group Properties” dialog box.
We click the expression box for the “Label” option. The expression box opens.
We replace the value “=Fields!Yearmth.Value” with “=Nothing” and Click OK,OK to exit the “Properties” box.
Configuring the necessary parameters
While the chart and the necessary data fields are now in place, we must now complete the necessary “wiring” and obtain the user’s login ID and pass that through to the stored procedure thus ensuring that the correct data is extracted and rendered.
We define a Parameter called UserID (see below)
Note that our parameter is going to be “Hidden”.
On the “Available Values” tab, we leave the “Select from one of the following options:” radio button ‘as is’.
Moving to the “Default Values” tab, we are going to set a default.
We click on the “Specify values” radio button and click the “Add” and click the expressions box to open the “Expression” editor.
The expressions box opens and we add “ =User!UserID” (see above). We click OK and OK to exit the “Report Parameter Properties” box.
Moving to our dataset, we right click on the dataset name and open its properties dialog box.
We click on the “Parameters” tab.
Should our parameter @UserID not be there, then we must select the “Add” button to add the parameter @UserID (see above).
Let us now run our report to see the results.
The reader will note that I inserted a text box above the chart, to display the value of @UserID (see above).
This obviously would NOT be there within the production environment and going forward, I have removed this text box from further screen shots.
“Re-arranging the furniture”
Prior to adding our matrix based report, we should really re-arrange the controls that we currently show upon our screen.
I have taken the liberty of adding a text box above the chart and I have given my report the title “SQLShack Industries Team Revenue Report”.
Further, I have labelled the Y axis as “Revenue” and set the property to currency (see above).
Adding a matrix to the mix
As you will remember, we created two store procedures. The second one was in the format that could be utilized with a matrix. We are now going to add that matrix to our report.
We drag a “Matrix Report Item” onto the drawing surface.
In a similar manner to which we created the chart dataset (above), we create an additional dataset for the matrix. The question that DOES arise is, “Could we have created one data set for both?” The answer is yes HOWEVER, I prefer to keep the processes as simple as possible as this gives you added flexibility to handle ‘change’.
The screen shot above shows the dataset for the Matrix.
Prior to configuring the matrix, we remove the “Column Grouping” as shown above (middle bottom).
Further we are ONLY removing the Grouping and NOT the data (see above).
Note that the left most column of the matrix is the “grouping“column. We are going to use the column “Name” (which contains the Team names) as the grouping field (see above). Further, we add eleven more columns to ensure that we have enough columns for the twelve months of the year.
We are now going change the column headers to something more meaningful. We shall also right orient the revenue fields, in addition to adding fill to the matrices’ cells (see below).
When we run our report for “ATRION\ssimon”, we find the following (see below).
When we run our report for “ATRION\SQLDB”, we find the following (see below).
Conclusions
We have completed the work that SQLShack Industries’ management had requested. The data rendered within the reports reflected the data permissions that each group/team had.
Whilst this technique is aimed for internal business consumption, it is far from the type of security that one would implement for users coming in through the firewall.
As always, should you have any questions or concerns, please feel free to contact me.
In the interim, Happy Programming!!
ADDENDA A
The code sample for the matrix may be seen below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
/****** Script for SelectTopNRows command from SSMS ******/ use [SQLShackFinancial] go --IF OBJECT_ID(N'tempdb..#rawdata1') IS NOT NULL --BEGIN -- DROP TABLE #rawdata1 --END --IF OBJECT_ID(N'tempdb..#rawdata2') IS NOT NULL --BEGIN -- DROP TABLE #rawdata2 --END --IF OBJECT_ID(N'tempdb..#rawdata3') IS NOT NULL --BEGIN -- DROP TABLE #rawdata3 --END --IF OBJECT_ID(N'tempdb..#rawdata55') IS NOT NULL --BEGIN -- DROP TABLE #rawdata55 --END --go Alter procedure SalesRevenue01Matrix ( @UserID as Varchar(20) ) as declare @Yearr varchar(4) declare @LowYearr varchar(4) declare @SQL varchar(2000) declare @SQL1 nvarchar(2000) declare @decider int declare @month01 varchar(6) declare @month02 varchar(6) declare @month03 varchar(6) declare @month04 varchar(6) declare @month05 varchar(6) declare @month06 varchar(6) declare @month07 varchar(6) declare @month08 varchar(6) declare @month09 varchar(6) declare @month10 varchar(6) declare @month11 varchar(6) declare @month12 varchar(6) declare @beginFiscal date declare @endFiscal date --declare @YearIncoming as varchar(4) set @decider = datepart(Month,convert(date,getdate())) set @Yearr = datepart(YEAR,Convert(date,Getdate())) set @Lowyearr = @Yearr set @Lowyearr = case when @decider > 6 then datepart(YEAR,Convert(date,Getdate())) else @LowYearr end set @Yearr = case when @decider >= 7 then datepart(YEAR,Convert(date,Getdate())) + 1 else @Yearr +1 end set @Beginfiscal = convert(varchar(4),@LowYearr) + '0701' set @Endfiscal = convert(varchar(4),@Yearr) + '0630' set @month01 = convert(varchar(4),datepart(Year,@beginFiscal)) + case when len(convert(varchar(2),datepart(Month,@beginFiscal))) = 1 then convert(varchar(2),'0' + convert(varchar(2),datepart(Month,@beginFiscal))) else convert(varchar(2),datepart(Month,@beginFiscal)) end set @month02 = convert(varchar(4),datepart(Year,@beginFiscal)) + case when len(convert(varchar(2),datepart(Month,dateadd(mm,1,@beginFiscal)))) = 1 then convert(varchar(2),'0' + convert(varchar(2),datepart(Month,dateadd(mm,1,@beginFiscal)))) else convert(varchar(2),datepart(Month,dateadd(mm,1,@beginFiscal))) end set @month03 = convert(varchar(4),datepart(Year,@beginFiscal)) + case when len(convert(varchar(2),datepart(Month,dateadd(mm,2,@beginFiscal)))) = 1 then convert(varchar(2),'0' + convert(varchar(2),datepart(Month,dateadd(mm,2,@beginFiscal)))) else convert(varchar(2),datepart(Month,dateadd(mm,2,@beginFiscal))) end set @month04 = convert(varchar(4),datepart(Year,@beginFiscal)) + case when len(convert(varchar(2),datepart(Month,dateadd(mm,3,@beginFiscal)))) = 1 then convert(varchar(2),'0' + convert(varchar(2),datepart(Month,dateadd(mm,3,@beginFiscal)))) else convert(varchar(2),datepart(Month,dateadd(mm,3,@beginFiscal))) end set @month05 = convert(varchar(4),datepart(Year,@beginFiscal)) + case when len(convert(varchar(2),datepart(Month,dateadd(mm,4,@beginFiscal)))) = 1 then convert(varchar(2),'0' + convert(varchar(2),datepart(Month,dateadd(mm,4,@beginFiscal)))) else convert(varchar(2),datepart(Month,dateadd(mm,4,@beginFiscal))) end set @month06 = convert(varchar(4),datepart(Year,@beginFiscal)) + case when len(convert(varchar(2),datepart(Month,dateadd(mm,5,@beginFiscal)))) = 1 then convert(varchar(2),'0' + convert(varchar(2),datepart(Month,dateadd(mm,5,@beginFiscal)))) else convert(varchar(2),datepart(Month,dateadd(mm,5,@beginFiscal))) end set @month07 = convert(varchar(4),datepart(Year,@endFiscal)) + case when len(convert(varchar(2),datepart(Month,dateadd(mm,-5,@endFiscal)))) = 1 then convert(varchar(2),'0' + convert(varchar(2),datepart(Month,dateadd(mm,-5,@endFiscal)))) else convert(varchar(2),datepart(Month,dateadd(mm,-5,@endFiscal))) end set @month08 = convert(varchar(4),datepart(Year,@endFiscal)) + case when len(convert(varchar(2),datepart(Month,dateadd(mm,-4,@endFiscal)))) = 1 then convert(varchar(2),'0' + convert(varchar(2),datepart(Month,dateadd(mm,-4,@endFiscal)))) else convert(varchar(2),datepart(Month,dateadd(mm,-4,@endFiscal))) end set @month09 = convert(varchar(4),datepart(Year,@endFiscal)) + case when len(convert(varchar(2),datepart(Month,dateadd(mm,-3,@endFiscal)))) = 1 then convert(varchar(2),'0' + convert(varchar(2),datepart(Month,dateadd(mm,-3,@endFiscal)))) else convert(varchar(2),datepart(Month,dateadd(mm,-3,@endFiscal))) end set @month10 = convert(varchar(4),datepart(Year,@endFiscal)) + case when len(convert(varchar(2),datepart(Month,dateadd(mm,-2,@endFiscal)))) = 1 then convert(varchar(2),'0' + convert(varchar(2),datepart(Month,dateadd(mm,-2,@endFiscal)))) else convert(varchar(2),datepart(Month,dateadd(mm,-2,@endFiscal))) end set @month11 = convert(varchar(4),datepart(Year,@endFiscal)) + case when len(convert(varchar(2),datepart(Month,dateadd(mm,-1,@endFiscal)))) = 1 then convert(varchar(2),'0' + convert(varchar(2),datepart(Month,dateadd(mm,-1,@endFiscal)))) else convert(varchar(2),datepart(Month,dateadd(mm,-1,@endFiscal))) end set @month12 = convert(varchar(4),datepart(Year,@endFiscal)) + case when len(convert(varchar(2),datepart(Month,dateadd(mm,0,@endFiscal)))) = 1 then convert(varchar(2),'0' + convert(varchar(2),datepart(Month,dateadd(mm, 0,@endFiscal)))) else convert(varchar(2),datepart(Month,dateadd(mm,0,@endFiscal))) end -- select @Month01,@Month02,@Month03,@Month04,@Month05,@Month06 --select @Month07,@Month08,@Month09,@Month10,@Month11,@Month12 --declare @UserID as Varchar(50) declare @WhatICanSee as varchar (50) --set @UserID = 'ATRION\ssimon' Set @UserID = rtrim(ltrim(@UserID)) set @WhatICanSee = (Select Min(team) from [dbo].[SQLShackSecurity] where UserID = rtrim(ltrim(@UserID))) --set @WhatICanSee='Team1' If Not @WhatICanSee is null BEGIN select YearMth ,case when @WhatIcanSee <> 'Team1' then 0 else SUM (Team1_Revenue) end as Team1_Revenue , SUM (Team2_Revenue) as Team2_Revenue, case when @WhatIcanSee = 'Team1' then (SUM (Team1_Revenue) + SUM (Team2_Revenue)) else sum(Team2_Revenue) end as TTotal into #rawdata1 from( SELECT YearMth, case when name = 'Team1' then sales else 0 end as Team1_Revenue ,case when name = 'Team2' then sales else 0 end as Team2_Revenue from [dbo].[YearlySales1])a Group by YearMTH set @sql = ' select name, [' + @month01+ '],[' + @month02+ '],[' +@month03+ '],[' +@month04+ '],[' +@month05 +'],[' + @month06+ '],[' +@month07+ '],[' +@month08+ '],[' +@month09+ '],[' +@month10+ '],[' + @month11+ '],[' +@month12 + '] from ' + ' ( ' + ' select Yearmth, name, value ' + ' from #rawdata1 ' + ' unpivot ' + ' ( ' + ' value for name in ([Team1_Revenue],[Team2_Revenue],[TTotal]) ' + ' ) unpiv '+ ' ) src ' + ' pivot ' + ' ( ' + ' sum(value) ' + ' for YearMth in ([' + @month01+ '],[' + @month02+ '],[' +@month03+ '],[' +@month04+ '],[' +@month05 +'],[' + @month06+ '],[' +@month07+ '],[' +@month08+ '],[' +@month09+ '],[' +@month10+ '],[' + @month11+ '],[' +@month12 +'] )) piv ' + ' order by name asc ' set @sql1 = @sql CREATE TABLE #rawdata55( [name] [varchar](75) NULL, [Month01] [decimal](10, 2) NULL, [Month02] [decimal](10, 2) NULL, [Month03] [decimal](10, 2) NULL, [Month04] [decimal](10, 2) NULL, [Month05] [decimal](10, 2) NULL, [Month06] [decimal](10, 2) NULL, [Month07] [decimal](10, 2) NULL, [Month08] [decimal](10, 2) NULL, [Month09] [decimal](10, 2) NULL, [Month10] [decimal](10, 2) NULL, [Month11] [decimal](10, 2) NULL, [Month12] [decimal](10, 2) NULL) insert #rawdata55 exec sp_executesql @SQL1 with recompile select * from #rawdata55 END go |
ADDENDA B
User ATRION\ssimon observes the following when a report is pushed to SharePoint
User ATRION\SQLDB observes the following when a report is pushed to SharePoint
- Reporting in SQL Server – Using calculated Expressions within reports - December 19, 2016
- How to use Expressions within SQL Server Reporting Services to create efficient reports - December 9, 2016
- How to use SQL Server Data Quality Services to ensure the correct aggregation of data - November 9, 2016