Saturday, August 2, 2008

Using ReportViewer control with SQL Server 2005 Reporting Services Custom Security Extension

Russell's Blog

I've been playing around with using different forms of authentication / impersonation with the Report Viewer controls, and I thought I'd post the fruits of my efforts. Here we go:

Using Forms Authentication with the Winform Report Viewer control is easy -- Just pass in the creds and you're all done:

reportViewer1.ServerReport.ReportServerCredentials.SetFormsCredentials(null, "userName", "password", "domainName");
this.reportViewer1.RefreshReport();

Doing the same thing with the Webform Report Viewer control is kind of a pain in the tail. You actually have to implement an interface called IReportServerCredentials and write/borrow another subclass that handles all the cookie related stuff when dealing with Forms Auth.

I took most of the following code from the following help topic, btw:

http://msdn2.microsoft.com/en-us/library/microsoft.reporting.webforms.ireportservercredentials.aspx

Anyway, first create the code for your Forms Auth Login page:

MyReportingService svc = new MyReportingService();
svc.Url = "
http://myServer/reportserver/reportexecution2005.asmx";
try
{
svc.LogonUser("myUserName", "MyPassword", null);
Cookie myAuthCookie = svc.AuthCookie;
if (myAuthCookie == null)
{
Message.Text = "Logon failed";
}
else
{
HttpCookie cookie = new HttpCookie(myAuthCookie.Name, myAuthCookie.Value);
Response.Cookies.Add(cookie);
string returnUrl = Request.QueryString["ReturnUrl"];
if (returnUrl == null !returnUrl.StartsWith("/"))
Message.Text = "Return url is missing or invalid!";
else
Response.Redirect("
http://myServer/appFolder/default.aspx");
}
}
catch (Exception ex)
{
Message.Text = "Logon failed: " + ex.Message;
}

The code above calls LogonUser() against the SSRS web service so that we can get the Forms Auth cookie back from SSRS itself. Then, we stick the cookie into Response.Cookies, and forward the user to a page which has a ReportViewer control on it (in this case, http://myServer/appFolder/default.aspx)

OK, so now we're sitting on the page which has the Report Viewer control itself.

In Page_Load, we first see if the previous cookie exists...if it doesn't, we send you right back to the logon form:


HttpCookie cookie = Request.Cookies["sqlAuthCookie"];
if (cookie == null)
{
Response.Redirect("/appFolder/logon.aspx?ReturnUrl=" + HttpUtility.UrlEncode(Request.RawUrl));

}
else
{

ReportViewer1.ProcessingMode = ProcessingMode.Remote;
ReportViewer1.ServerReport.ReportServerUrl = new Uri("
http://myServer/reportserver");
ReportViewer1.ServerReport.ReportPath = "/Report Project2/Report1";

Cookie authCookie = new Cookie(cookie.Name, cookie.Value);
authCookie.Domain = "myServer";
ReportViewer1.ServerReport.ReportServerCredentials =
new MyReportServerCredentials(authCookie);
}

If the cookie IS there, we set a few properties on the Report Viewer control, then set the ReportServerCredentials property of the control equal to our authCookie. Here is where the implementation of IReportServerCredentials and that other "cookie-handling class" come in.

First, here's the implementation of IReportServerCredentials. It allows us to create a MyReportServerCredentials object:

class MyReportServerCredentials : IReportServerCredentials
{
private Cookie m_authCookie;

public MyReportServerCredentials(Cookie authCookie)
{
m_authCookie = authCookie;
}

public WindowsIdentity ImpersonationUser
{
get
{
return null; // Use default identity.
}
}

public ICredentials NetworkCredentials
{
get
{
return null; // Not using NetworkCredentials to authenticate.
}
}

public bool GetFormsCredentials(out Cookie authCookie,
out string user, out string password, out string authority)
{
authCookie = m_authCookie;
user = password = authority = null;
return true; // Use forms credentials to authenticate.
}
}


As I mentioned earlier, we also have to subclass the myServer.ReportExecutionService class and override a few methods in order to do the cookie-related work:

public class MyReportingService : myServer.ReportExecutionService
{
private Cookie m_authCookie;

public Cookie AuthCookie
{
get
{
return m_authCookie;
}
}

protected override WebRequest GetWebRequest(Uri uri)
{
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(uri);
request.Credentials = base.Credentials;
request.CookieContainer = new CookieContainer();
if (m_authCookie != null)
request.CookieContainer.Add(m_authCookie);
return request;
}

protected override WebResponse GetWebResponse(WebRequest request)
{
WebResponse response = base.GetWebResponse(request);
string cookieName = response.Headers["RSAuthenticationHeader"];
if (cookieName != null)
{
HttpWebResponse webResponse = (HttpWebResponse)response;
m_authCookie = webResponse.Cookies[cookieName];
}
return response;
}
}

Parameters can also be passed to ReportViewer and we can also Show/Hide them as needed:


Cookie authCookie = new Cookie(cookie.Name, cookie.Value);
authCookie.Domain = ConfigurationManager.AppSettings["serverName"].ToString();
reportViewer1.ServerReport.ReportServerCredentials = new MyReportServerCredentials(authCookie);
//Checking available parameter list
ReportParameterInfoCollection rparams = reportViewer1.ServerReport.GetParameters();
List paramList = new List();
//Hide parameters if user is External user i.e not Internal
if (isClientID != "false")
{
foreach (ReportParameterInfo rpinfo in rparams)
{
switch (rpinfo.Name)
{
case "ClientID":
paramList.Add(new Microsoft.Reporting.WebForms.ReportParameter("ClientID", isClientID, false));
break;
case "IsExported":
paramList.Add(new Microsoft.Reporting.WebForms.ReportParameter("IsExported","true", false));
break;
case "ServerType":
paramList.Add(new Microsoft.Reporting.WebForms.ReportParameter("ServerType", "true", false));
break;
case "IsReleased":
paramList.Add(new Microsoft.Reporting.WebForms.ReportParameter("IsReleased", "true", false));
break;
}
}
this.reportViewer1.ServerReport.SetParameters(paramList);
}

...and that's it. You may actually have better luck using the URL at the top of the page for your code copying and pasting as it contains HTML you can use for you logon page, too.

My biggest problem with this whole scenario was actually *finding* the help topic I needed...It would have nice if I could have searched on "forms authentication report viewer control" and been directed to the topic in question (grump, grump).

Forms Authentication in SQL Server 2005 Reporting Services

Form authentication extension can be easily implemented. Here's how...
http://msdn.microsoft.com/en-us/library/ms160724.aspx


You can find all the extensions for RS2005 at http://msdn.microsoft.com/en-us/library/ms160911.aspx


So once you have sample projects then you need to compile it and get the required custom security dll.
Above output dll needs to be placed at two places.
1. <system-drive>:/<path to reportingservices>/reportserver/bin
2. <system-drive>:/<path to reportingservices>/reportmanager/bin
you also need to place two .aspx files for creating cutom interface in reporting services. These two files should be placed as following:
1. <system-drive>:/<path to reportingservices>/reportserver/logon.aspx
2. <system-drive>:/<path to reportingservices>/reportmanager/pages/UILogon.aspx
After placing above files at desired locations. We need to edit RS configuration files to make them start using our custom dll extension. These editings should be done at both ReportServer and ReportManager as follows:
Important : Dont forget to take backup of all config files before making changes. Otherwise in case of errors, RS will be corrupt and you need to re-install reporting services
- Report Server
1. rsreportserver.config
- your custom report server url should be given with instead of 'localhost':
http://EXTREME-MACHINE/ReportServer
- Authentication and Authorization should be modified as :
<Security>
<Extension Name="Forms" Type="ReportingServices.CustomSecurity.Authorization,ReportingServices.CustomSecurity" >
<Configuration>
<AdminConfiguration>
<UserName>testuser
</AdminConfiguration>
</Configuration>
</Extension>
</Security>


<Authentication>

<Extension Name="Forms" Type="ReportingServices.CustomSecurity.AuthenticationExtension, ReportingServices.CustomSecurity">
<Configuration>
<ConnectionString>server=localhost;user id=sa;password=123;database=BenmarkIS
</Configuration>
</Extension>
</Authentication>

2. rssrvpolicy.config
- Trust levels should be defined as highlighted:
class="FirstMatchCodeGroup"
version="1"
PermissionSetName="FullTrust"
Description="This code group grants MyComputer code Execution permission. ">
<IMembershipCondition
class="ZoneMembershipCondition"
version="1"
Zone="MyComputer" />
<CodeGroup
class="UnionCodeGroup"
version="1"
PermissionSetName="FullTrust"
Name="Microsoft_Strong_Name"
Description="This code group grants code signed with the Microsoft strong name full trust. ">
- A new Code Group should be inserted for security extenstion as given:

<CodeGroup
class="UnionCodeGroup"
version="1"
Name="SecurityExtensionCodeGroup"
Description="Code group for the sample security extension"
PermissionSetName="FullTrust">
<IMembershipCondition
class="UrlMembershipCondition"
version="1"
Url="C:\Program Files\Microsoft SQL Server\MSSQL.4\Reporting Services\ReportServer\bin\ReportingServices.CustomSecurity.dll"
/>
</CodeGroup>
<CodeGroup
class="UnionCodeGroup"
version="1"
PermissionSetName="FullTrust"
Name="SharePoint_Server_Strong_Name"
3. Web.config
- Following highlighted changes should be made:
<authentication mode="Forms">
<forms loginUrl="logon.aspx" name="sqlAuthCookie" timeout="60" slidingExpiration="true" path="/"></forms>
</authentication>
<authorization>
<deny users="?" />
</authorization>
<identity impersonate="false" />


- Report Manager
1. rsmgrpolicy.config
- Trust levels should be changed in rsmgrpolicy.config as highlighted
class="FirstMatchCodeGroup"
version="1"
PermissionSetName="FullTrust"
Description="This code group grants MyComputer code Execution permission. ">
<IMembershipCondition
class="ZoneMembershipCondition"
version="1"
Zone="MyComputer" />
<CodeGroup
class="UnionCodeGroup"
version="1"
PermissionSetName="FullTrust"
Name="Microsoft_Strong_Name"
Description="This code group grants code signed with the Microsoft strong name full trust. ">

2. RSWebApplication.config
- Basic report manager redirection to the custome made .aspx page
*Note: Use instead of 'localhost' in the ReportServerUrl
<CustomAuthenticationUI>
<loginUrl>/Pages/UILogon.aspx</loginUrl>
<UseSSL>False</UseSSL>
</CustomAuthenticationUI>
<ReportServerUrl>http://extreme-machine/ReportServer</ReportServerUrl>
</UI>

Once you are done with above changes then you need to setup IIS for anonymous access. So in the 'security' tab of 'reportserver' and 'reports' web folders, set security level as 'anonymous'.
Here you go, you are done with the settings.
:) Happy Reporting!!