Custom Security Extension - Authorization questions using Forms Authentication
Greetings. I have developed our own custom security extension, implemented forms based authentication, and can authenticate from report manager, report server and sql studio. So far so good.However, when it comes to Authorization, i'm unclear in a few areas and would appreciate if someone could help me out with the following questions. It should be noted that in the code I have granted an administrator user full access to all operations and permissions, and then tested against both an administrator user and a normal user. IAuthorizationExtension.GetPermissions summary says"Returns the set of permissions granted a specific user for an item in the report server database."Inparticular, the secDesc parameter is supposed to contain the security descriptor associated with the item.However, with our extension this parameter is always null, even if I have already granted access for a user, which is confirmed through logging in CreateSecurityDescriptor.Through the report manager or sql studio I can see that the permissions have been created, so I can't understand why I never see them in the GetPermissions method? This then (seems to) flow through to the various CheckAccess methods, where the users are authenticated, but are not authorized to perform any operations. i.e. in report manager a user has no folders or reports available. Is RS authorization designed around the concept that the details will always be stored in it's own database?Ideally, we'd like to have the various roles, users and function authorizations defined in our own security framework. This is working great for the authentication aspect of the extension, but unless there is a mechanism which exposes the details of the particular authorization process (e.g. the name of the folder being viewed or report being run), then I can't see a way we can implement it. Unless i'm missing something fundamental of course! Using Report Builder as the Administrator user (or any other user), I can see no data models available, even though I have created them via Report Manager, and I get the following exception trying to open up the list of reports:System.Web.Services.Protocols.SoapException: The permissions granted to user '' are insufficient for performing this operation. ---> Microsoft.ReportingServices.Diagnostics.Utilities.AccessDeniedException: The permissions granted to user '' are insufficient for performing this operation. at Microsoft.ReportingServices.Library.ListChildrenAction.PerformActionNow() at Microsoft.ReportingServices.Library.RSSoapAction.Execute() at Microsoft.ReportingServices.WebServer.ReportingService2005.ListChildren(String Item, Boolean Recursive, CatalogItem[]& CatalogItems)I have implemented a report server proxy (inherited from ReportService2005) as per the example, to pass through the authorization cookie. Any clues as to what could be wrong? Finally, I suspect part of my problem may be in assignment of users to System Roles ("System Administrator" and "System User"), I'm not sure if these are meant only for Windows Authentication? I can see no way of assigning these roles to any of my users using Forms Authentication.Thanks for any help or advice you can give!
November 9th, 2007 7:07am

I have resolved #4 in the list above. I didn't realise you could type the name of a user be assigned a role (Report Manager -> System Role Assignments -> New Role Assignment). So that mystery is solved, but it's interesting that I couldn't see a way to do this via SQL Studio?I'm still in the dark about #1 - #3, but #1 seems to indicate that it's my job to create the permissions, however, I still don't understand how I can, for example, determine which folder or report to define permissions to, if the relavent name of the folder or report is not passed through to the GetPermissions method? I know i'm missing something!
Free Windows Admin Tool Kit Click here and download it now
November 12th, 2007 9:56am

Ok, seem to be talking to myself here but I think I have grasped issues #1 & #2, and now only have solve #3 (see below).As I understand it, RS assumes that you will use it's authorization model and database, even when using a custom security extension. If you need to authorize RS outside of the standard approach, you have some heavy lifting to do.So...if any experts read this please correct any errors, but here's how I think it works:The IAuthenticationExtension.GetPermissions shows only what is granted for a particular item from the ReportServer database.If you want external authorization, this method is pretty much useless as there is no context to what item is being checked (e.g. Report Name, Folder Path), and I think I understand why, because as far as RS is concerned, there doesn't need to be, since CheckAccess methods are only concerned with the access control entities which come from the ReportServer database. i.e. They don't need to know what the object is, only what permissions it has been granted, so these can be checked.At first I thought this meant that our desires to have external authentication and authorization was impossible, but I found this message which showed how you can capture the input stream of the current web service request and parse out the necessary information. This takes some heavy lifting, but from my initial work, it seems possible, as i've now been able to externalize the authorization of viewing folders and reports, as well as executing reports. I'm happy to post some code but really it's just a variation of the example in the link above. Here's our workflow: User is authenticated GetPermissions method returns complete list (i.e. everything is permitted) CheckAccess methods are invoked, web service request is parsed based upon the required operation Authorization is applied by combining the required operation type (e.g. ReportOperation.Execute) and data extracted (e.g. //def:Report node inner text) with the true/false result returned to the invoked CheckAccess method.So the only thing I'm now stuck on is the error reported by Report Builder in item #3 in my first post, and i'm now getting the same error in BIDS when I try to deploy a report to the RS with the forms based custom security extension:Error rsAccessDenied : The permissions granted to user '' are insufficient for performing this operation.I'm going to go through my configuration setup again, but if anyone has seen this error or can give some clues i'd be extremely grateful!Cheerssi
November 13th, 2007 8:29am

Issue #3 resolved, details in this thread.
Free Windows Admin Tool Kit Click here and download it now
November 14th, 2007 5:49am

Hello Si618,I am in the planning phase of building a similar project. The database contains the Role information for each report and for each user. The objective is to authenicate and authorize user so that only the authorized reports are available for users to view.Do you have any suggestions on my following design steps?1. Deploy Reports onto the Report Server2. Put Reports in folders to group different types of report (I am not sure is it possible to only the display authorized reports for the current user)3. Design the custom Security extension to authenicate and authorize using the databaseThanks
November 28th, 2007 9:44am

Hey there,Sorry for the reply lag, I've been at home all week looking after my sick family (rotavirus' are not much fun!).Anyway, #1 & #2 on your list are done as per normal, you just have to provide the necessary implementation in the Authorization extension based upon the operation being executed, such as FolderOperation.CreateFolder.For #3, we now have a working implementation for external database-driven authentication and authorization, so it's definitely possible, but I have to stress that it seems Microsoft seems to assume you will be using their authorization database, so you have to jump through a few hoops to do this. Authentication is trivial compared to authorization.Since my last post I've made a few changes, namely When a path-based operation is performed (e.g. create folder or execute report) a check is made to see if the path starts with "/My Reports", if so, this is remapped to "/Users Folders/<username>/My Reports" so you only have to authorize one path, e.g.// Hardcoded alias used to re-map to single path for authorizationconst string _myReports = "/My Reports";// {0} will be replaced with the current identity nameconst string _userReports = "/Users Folders/{0}/My Reports";...// result is the path or item extracted from web serviceif (result.StartsWith(_myReports)){ userReports = string.Format(CultureInfo.InvariantCulture, _userReports, HttpContext.Current.User.Identity.Name); result = result.Replace(_myReports, userReports); _log.DebugFormat(CultureInfo.InvariantCulture, "Remapped {0} to {1}", _myReports, result);} Instead of relying on the CheckAccess operation type to determine what the web service soap structure will look like (so you can get the necessary info to authorize the operation), I just now grab the xml in the ctor, and for each CheckAccess invoked, grab the first node in the soap body and determine what to get based upon the node name, i.e.XmlDocument _request;static XmlNamespaceManager _nsMgr;...public AuthorizationExtension(){ _request = getWebServiceRequest(); if (_request == null) { _nsMgr = null; } else { _nsMgr = getNamespaceManager(_request); }}...XmlDocument getWebServiceRequest(){ StreamReader inputStream; XmlDocument result; string xml; result = new XmlDocument(); HttpContext.Current.Request.InputStream.Position = 0; inputStream = new StreamReader(HttpContext.Current.Request.InputStream); xml = inputStream.ReadToEnd(); if (string.IsNullOrEmpty(xml)) { _log.Debug("HttpContext.Current.Request.InputStream is empty"); return null; } result.LoadXml(xml); HttpContext.Current.Request.InputStream.Position = 0; return result;}...XmlNamespaceManager getNamespaceManager(XmlDocument xml){ XmlNamespaceManager result; result = new XmlNamespaceManager(xml.NameTable); result.AddNamespace("def", "http://schemas.microsoft.com/sqlserver/2005/06/30/reporting/reportingservices"); result.AddNamespace("soap", "http://schemas.xmlsoap.org/soap/envelope/"); return result;}... and then in each CheckAccess, something like...public bool CheckAccess(string userName, IntPtr userToken, byte[] secDesc, ReportOperation requiredOperation){ string operationName; operationName = _reportOperationNames[requiredOperation].ToString(); return checkAccess(userName, requiredOperation, operationName);}...bool checkAccess(string userName, object requiredOperation, string operationName){ bool result; string context, operation, operationType, path; UserToken user; result = false; context = string.Empty; operation = requiredOperation.ToString(); operationType = requiredOperation.GetType().Name; if (isAdministator(userName)) { _log.DebugFormat(CultureInfo.InvariantCulture, "User {0} automatically granted administrator access for {1} ({2}.{3})", userName, operationName, operationType, operation); return true; } user = getUserTokenFromCache(userName); if (user == null) { return false; } path = getPathFromRequest(); if (string.IsNullOrEmpty(path)) { _log.Error("No path found"); return false; } context = string.Format(CultureInfo.InvariantCulture, "{0} : {1}", operationName, path); result = user.Authorize(context); _log.DebugFormat(CultureInfo.InvariantCulture, "User {0} {1} access to {2} ({3}.{4})", userName, result ? "granted" : "denied", context, operationType, operation); return result;}...string getPathFromRequest(){ string result, userReports, nodeName; result = string.Empty; if (_request == null) { return result; } try { nodeName = _request.SelectSingleNode("//soap:Body", _nsMgr).ChildNodes[0].Name; if (nodeName == "ListChildren" || nodeName == "GetProperties" || nodeName == "GetPolicies" || nodeName == "SetPolicies" || nodeName == "GetItemDataSources") { result = _request.SelectSingleNode("//def:Item", _nsMgr).InnerText; } else if (nodeName == "LoadReport" || nodeName == "GetReportDefinition") { result = _request.SelectSingleNode("//def:Report", _nsMgr).InnerText; }...etc for each possible operationWe have a fairly standard role-based authorization structure in our database, and use the OperationNames class along with the path to define the context of the authorization, e.g. Execute and View : /Users Folders/userBlah/My Reports/reportBlahHope that helps!
Free Windows Admin Tool Kit Click here and download it now
December 7th, 2007 11:23am

Wow I hope your family is doing better.I am configuring the database table relationships and I have deployed a few Reports onto the ReportServer. You mentioned "Microsoft seems to assume you will be using their authorization database" Does this mean that you should have certain tables and their relationships should be a certain way as expected by the Security Extension?I will be starting to develop the security extension next week after I have finished configured the database. I should really look into what is the best way to setup my database for the Security Extension. Do you have any suggestions?Thanks a lot
December 8th, 2007 4:00am

Wow I hope your family is doing better.Much better, thanks!Does this mean that you should have certain tables and their relationships should be a certain way as expected by the Security Extension?No, what I mean is the hoops you have to jump through to perform external authorization. That is, parsing the web service request instead of getting the necessary info from GetPermissions.As far as your database schema for authorization goes, there are plenty of patterns and examples available on-line, it shouldn't matter which one you adopt, just so long as it serves your needs.Hope that helps.
Free Windows Admin Tool Kit Click here and download it now
December 10th, 2007 6:14am

Happy Holidays si618,I have successfully compiled the custom security extension. Currently I am having some runtime error when using the added membershipProvider.dll for authentication & authorization. This dll is signed with a strong name key.Is there some sort of special way to include your own class library?I also tried to edit the web.config under ReportManager to input the connection string, but never got it running because of the class library.Any ideas?http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2633482&SiteID=1
December 28th, 2007 3:34am

I am finally at where you were and I understand why you use soap in order to get the report or folder name. Is there a way to not show the folder or report the user doesn't have access to?By using soap grabing the path you will list items that the user can NOT access, only when (s)he clicks on it then soap is able to pick it up. Here is my post: http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2717891&SiteID=1
Free Windows Admin Tool Kit Click here and download it now
January 22nd, 2008 4:09am

Yes, that's a problem. It's okay inside your own code as you could filter these items, but for Report Manager, etc you'd need some mechanism to intercept the output.If this is a must have requirement, then one suggestion I have is something my boss suggested; instead of using a custom security extension, apply the same logic you have to getting the report data from the soap message in the security extension, but implement it in a new HttpModule and hook into that (i.e. filter the soap response based on authorization). I can't say that I like this approach because you removing the authentication aspect that works very nicely in the security extension. i.e. Your authentication will need to be completely separate from your authorization. But it may be a way.Finally, if it's possible to take a more directory based approach instead of a file based approach then this problem can also be avoided (i.e. a user can see everthing or nothing in a directory), but that depends very much on your requirements.Hope that helps!
January 23rd, 2008 8:33am

si618,I came up with a solution that utilize the built-in Role base permission and your own database Role schema. Let me know what you think.Here is my post: http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2717891&SiteID=1 DC
Free Windows Admin Tool Kit Click here and download it now
January 24th, 2008 9:45pm

The solution proposed above (getting the xml request) will notwork reliably. Report Servercan make many calls to the CheckAccess methods justto filla singleuser request. My experience is that at some point one of those calls will throw an XML related exception while trying to load the request into a xml document. The exception will say "no root node". I don't want to handle this exception by catching it and returning true in the CheckAccess method. So I catch the exception and send back false. Just one false return value will cause the Report Server to send back a failed authorization message to the user even if all of the previous calls to CheckAccess() returned true.Any thoughts on a way toimprove this approachwithout compromising security?
January 31st, 2008 7:06pm

You could use my solution.http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2717891&SiteID=1It is a very nice and clean implementation and it has been working fine for couple weeks now. I notice a lot of people have this issue regarding getting the item level permission. Please let me know if i can clarify anything.
Free Windows Admin Tool Kit Click here and download it now
February 1st, 2008 2:43am

mjs wrote: The solution proposed above (getting the xml request) will notwork reliably. Report Servercan make many calls to the CheckAccess methods justto filla singleuser request. My experience is that at some point one of those calls will throw an XML related exception while trying to load the request into a xml document. The exception will say "no root node". I don't want to handle this exception by catching it and returning true in the CheckAccess method. So I catch the exception and send back false. Just one false return value will cause the Report Server to send back a failed authorization message to the user even if all of the previous calls to CheckAccess() returned true.Any thoughts on a way toimprove this approachwithout compromising security? Interesting. Do you have any soap message at all? Do you store the soap message once per IAuthorizationExtension instance? We parse it into a private field in our ctor.I haven't finished completing the map for each CheckAccess method and it's equivalent soap body, but one example that I have come across is when _request.SelectSingleNode("//soap:Body", _nsMgr).ChildNodes[0].Name == "GetExecutionInfo".So far we haven't needed to authorize against this (LoadReport does the job), so it's gets logged and ignored (return true), perhaps as we complete the map this may be an issue. I'll let you know, I was going to post the complete map once it's finished, but it currently does enough for us now.Also, I think I understand what docchang has done; he's syncing his external authorization database with the internal reporting services authorization database.We did think of that, but maintaining two databases for authorization seemed more risky, especially if you have to provide a two-way sync. i.e. If permissions change in reporting services (say from Report Manager or SQL Studio) you have to push the changes out to your external authorization database. If permissions change in your external authorization database, you have to push the changes to reporting services. This would have to be transaction controlled and rock-solid, otherwise they would get out of sync and cause much confusion. e.g. The permissions you think you have in your external authorization database are not the permissions stored in the reporting services internal database. A bad situation.Don't get me wrong, I think this approach is possible, but IMHO it carries more risk, so we will only try it if using an external system on it's own proves unreliable. Will post results to this thread.
February 11th, 2008 3:52am

Thanks for your input. I agree with you with respect to maintaining two databases for the sake of authorization. We would really like to avoid this solution. With regards to evaluating xml requests, some requests don't contain soap messages but these are URL requests so I can evaluate for that andcan pluck the requested item off the end of the URL string. More concerning to me iswhen I am getting a Soap request but it does not contain any reference to the item or report being requested. Instead it contains a <ExecutionID> nested in a <ExecutionHeader> followed bya <Parameters> which includes all of the parameters the rdl is expecting to run the query. The first few soap requests follow the format that uses and item or report node and then this one comes. I hesitate to return true blindly to the request that contains only an ExecutionID since I have no way of reconciling that ID with a report name. The inner workings of the Report Server are murky here.From observation I know that therequest is related to some of the ones that have come before it but I don't see a way of drawing a definite link between the request with parameters and the one that contains the name of the report. Any thoughts would be much appreciated.
Free Windows Admin Tool Kit Click here and download it now
February 12th, 2008 8:18pm

mjs wrote: With regards to evaluating xml requests, some requests don't contain soap messages but these are URL requests so I can evaluate for that andcan pluck the requested item off the end of the URL string. That's interesting, as I thought everything was handled via web services. I've added some logging to capture the URL for each IAuthorizationExtension instance created...actually, on reviewing the code I've got a test for null HttpContext.Current.Request.InputStream, so I may have already run into it and not realised what was going on and just coded around the problem. So the logging should fill in the gaps, thanks! mjs wrote: More concerning to me iswhen I am getting a Soap request but it does not contain any reference to the item or report being requested. Instead it contains a <ExecutionID> nested in a <ExecutionHeader> followed bya <Parameters> which includes all of the parameters the rdl is expecting to run the query. The first few soap requests follow the format that uses and item or report node and then this one comes. I hesitate to return true blindly to the request that contains only an ExecutionID since I have no way of reconciling that ID with a report name. The inner workings of the Report Server are murky here.From observation I know that therequest is related to some of the ones that have come before it but I don't see a way of drawing a definite link between the request with parameters and the one that contains the name of the report. Okay, my immediate thought on reading the first paragraph was there should be a way to cache the exection id and then map that back to a report or whatever...but the second paragraph leaves me uncertain now I know if you turn the logging up to verbose in reporting services you get a lot more information, so there may be some clues there.I guess I'm still not sure our code needs to worry about these execution related requests as our authorization level is simply "is this user allowed to load the report", and my testing indicated that authorization worked under all the circumstances I tried ...but there may be edge cases we haven't tested, so I can understand your concern.Anyway, I've just started getting back into this extension again after a break to do other work, so will keep this thread updated.
February 13th, 2008 3:28am

It seems like which ever direction you take to do this you will run into some kind of wall. Right now I am developing a trigger off my ReportsInRoles table if any changes it'll update the report server policy database table. Well at least there are two working solutions to Security Extensions that we know of.I personally think there should be a cleaner customization. Almost seemed like its a hack instead of a the correct way of doing things. After I have develop my solution I like to write up an aritcle with both directions so probably need you guy's help on the SOAP route.Happy CodingDC
Free Windows Admin Tool Kit Click here and download it now
February 14th, 2008 10:06pm

For anybody interested, here is my design to handle authorization requests in Reporting Services. I believe I have a working design for our environment. For the time being this design focuses on the CheckAccess() method for a report request (e.g. those requests from a report viewer control in our web application). The other overloaded CheckAccess() methods seem to support Report Manager functionality or the RS utility. In the case of those methods I do a simple check to make sure the user is in an administrative role. If they are than those methods will return true. I have found that for any given request from a report viewer control the CheckAccess method will be called several times. There are basically three types of requests that come through. The first is an XML request with the item or report name in the XML body. The second is an XML request with an ExecutionID element and report parameters but no item or report name in it. The third request is a URL request with report viewer environment variables and instructions to the Report Server. Nested in that request is item name that can be parsed out. Forreport requests, I first check to see if the request is via URL or XML. If there is no SOAP argument in the header than it must be a URL request. I then parse out the request to get the item being requested and send that along with the users name to our database for authorization. if (HttpContext.Current.Request.Headers["SOAPAction"] == null) { string url = HttpContext.Current.Request.Url.ToString(); int indexStart = url.IndexOf(@"?"); int indexEnd = url.IndexOf(@"&"); int diff = indexEnd - indexStart; string path = url.Substring(indexStart, diff); string[] parsePath = path.Split('/'); requestedItem = parsePath[parsePath.Length - 1]; } If there is a SOAP argument in the header than I move on to evaluate for the other two types of requests. I check for an item or report element in the body of the XML. If there is one I get the name of the item and send that along with the users name on for authorization. The logic I use here is similar to what is already included in previous posts to this thread. If there is no item or report in the XML I check for a <ExecutionHeader> element. If there is a <ExecutionHeader> element I know that this request is related to a request that I have already authorized (or rejected) so I pass a true back to Report Server. Below result is an xml node. The executionheader string is used in my code later to evaluate the request to be true. if (result == null) { result = SOAPDocument.SelectSingleNode("//def:ExecutionHeader", ns); if (result != null) { requestedItem = "executionheader"; } } All of this logic is handled in two methods called by the CheckAccess() method: ParseRequest() and GetAuthorization(). In the future we will build an administrative application to do what the RS utility and Report Manager provide. When we do we will be adding to our database and authorization logic to accommodate the extra functionality.
February 19th, 2008 10:25pm

Thanks for sharing! Some useful content there. mjs wrote: In the future we will build an administrative application to do what the RS utility and Report Manager provide. When we do we will be adding to our database and authorization logic to accommodate the extra functionality.Could you hook into the web service methods which are related to administration? That way you only have to worry about coding the business and persistence layers, rather than the presentation layer, which seems like a lot of work, especially when Microsoft have already written them (in RS, Report Manager and SQL Server Studio).You would probably end up with content in the RS database, but would that matter if you only authorize from an external database?
Free Windows Admin Tool Kit Click here and download it now
February 20th, 2008 9:11am

I haven't done a lot of research onour second phaseyet. My first task is to replace the RS functionality we are losing as a result of going with forms authentication. The RS utility only supports basic and windows authentication. I believe that I can hook my own console or windowsapplication (that supports forms-based authentication)up to use the web services. I will be starting on it soon.We can hold on toReport Manager longer so I haven't thought about replacing that yet. It may be that we upgrade or integrate with Sharepoint before I even get to that piece.
February 21st, 2008 12:30am

Can you please elaborate how you solved point #2. I have written a similar custom security extension. My Problem is when I debug various CheckAccess methods I could not find a way to get the item name or report name the report server is checking access for. Could you please help me in this. I know this post was made long time back, but I hope I could get a answer to this. Thanks, Nilesh
Free Windows Admin Tool Kit Click here and download it now
March 13th, 2008 7:35am

Hope all is well!A logout feature is requested by the customer. My approach is to create a "Logout" folder in the home folder so that when the user clicks on it, i'll clear cookies and clear authentication session. I grab the item name and insert into cache Code Snippet public string GetItemName() { XmlDocument SOAPDocument = getWebServiceRequest(); XmlNamespaceManager ns = getNamespaceManager(SOAPDocument); XmlNode result = SOAPDocument.SelectSingleNode("//def:Item", ns); if (result == null) { return null; } String ItemPath = result.InnerText; string[] ItemDescription = ItemPath.Split(new char[] { '/' }); HttpContext.Current.Cache.Insert("ItemName", ItemDescription[ItemDescription.Length - 1], null, DateTime.Now.AddMinutes(5), TimeSpan.Zero); return ItemDescription[ItemDescription.Length - 1]; } In my authorization if I found "Logout" folder I will run the function below Code Snippet public void Logout() { HttpContext.Current.Response.Cookies.Clear(); HttpContext.Current.Response.Clear(); FormsAuthentication.SignOut(); } I double check in the GetUserInfo and make sure it will return NULL if Cache["ItemName"] == "Logout" Code Snippet public void GetUserInfo(out IIdentity userIdentity, out IntPtr userId) { // If the current user identity is not null, // set the userIdentity parameter to that of the current user if (HttpContext.Current != null && HttpContext.Current.User != null) { if (HttpContext.Current.Cache != null && HttpContext.Current.Cache["ItemName"] != null && (0 == String.Compare(HttpContext.Current.Cache["ItemName"].ToString(), "Logout", true))) { HttpContext.Current.Response.Cookies.Clear(); HttpContext.Current.Response.Clear(); FormsAuthentication.SignOut(); userIdentity = null; } else userIdentity = HttpContext.Current.User.Identity; } else { userIdentity = null; } // initialize a pointer to the current user id to zero userId = IntPtr.Zero; } Right now when I clicks on the Logout folder the Report Server just error out. Solved! http://forums.microsoft.com/msdn/ShowPost.aspx?PostID=3516664&SiteID=1
June 24th, 2008 10:50pm

D E F
Free Windows Admin Tool Kit Click here and download it now
March 13th, 2011 11:40am

This topic is archived. No further replies will be accepted.

Other recent topics Other recent topics