Pass User (ClaimsPrincipal) from ReportsController to Webservice Datasource controller

1 Answer 59 Views
DataSource WebService
Lennert
Top achievements
Rank 2
Iron
Iron
Lennert asked on 05 Jan 2024, 03:12 PM

Hi,

We are implementing Telerik Reporting in our ASP.NET hosted Blazor WASM application (using .NET 6).
So far we based our implementation on the following sample: https://github.com/telerik/reporting-samples/tree/master/BlazorViewerAndDesignerExample.

We are looking for a way to pass the User from HttpContext of the ReportsController (for viewer and designer controller) to the WebService datasource, so we can authorize the datasource request based on the user ClaimsPrincipal. The webservice datasource is an api controller that is part of the same ASP.NET project.

Is this possible?

Alternatively, if we could pass a certain cookie from the request to the ReportsController to the webservice datasource request, we could make the authorization work with that.

Kind regards,

Lennert

1 Answer, 1 is accepted

Sort by
1
Accepted
Momchil
Telerik team
answered on 10 Jan 2024, 01:13 PM

Hello Lennert,

The WebServiceDataSource Component can pass parameters as cookies along with the requests to the data source API and it supports cookie-based authentication, as well. Thus, I believe that the cookie alternative you mentioned should work.

Additionally, if you Implement a Custom ReportSource Resolver, you could access the information from HttpContext and modify the WebServiceDataSource programmatically (see Modify the Report Data Sources through codebut it might take extra effort to implement.

Best Regards,
Momchil
Progress Telerik

Stay tuned by visiting our roadmap and feedback portal pages, enjoy a smooth take-off with our Getting Started resources, or visit the free self-paced technical training at https://learn.telerik.com/.
Lennert
Top achievements
Rank 2
Iron
Iron
commented on 10 Jan 2024, 01:30 PM | edited

Hi Momchil,

Thanks for the suggestions. Will look into it and get back to you.

Currently we use the UriReportSourceResolver. Any chance of providing some example code on how to extend that specific ReportResolver to access the HttpContext?

Kind regards,

Lennert

Lennert
Top achievements
Rank 2
Iron
Iron
commented on 11 Jan 2024, 10:08 AM

Hi Momchil,
I am stuck on the SharedDataSource in the following custom ReportSourceResolver, which is based on the UriReportSourceResolver.

Our reports use a SharedDataSource, that defines a WebServiceDataSource. In the below code I can access the SharedDataSource, but not the WebServiceDataSource. The DataSourceReference property of the SharedDataSource is null.

public class CustomReportSourceResolver : ReportSourceResolverBase
{
    private readonly string repositoryDirectory;

    public CustomReportSourceResolver()
        : this(AppDomain.CurrentDomain.BaseDirectory)
    {
    }

    public CustomReportSourceResolver(string repositoryDirectory)
    {
        this.repositoryDirectory = repositoryDirectory;
    }

    protected override ReportSource ResolveReport(string reportStr)
    {
        if (IsValidReportPath(reportStr))
        {
            string reportPath = MapPath(reportStr);
            if (File.Exists(reportPath))
            {
                var cookies = (IRequestCookieCollection)UserIdentity.Current.Context["Cookies"];
                var cookieKey = "CustomAuhtenticationCookie";
                var parsedCookie = cookies.TryGetValue(cookieKey, out string cookie) ? cookie : null;
                var reportPackager = new ReportPackager();
                Report report = null;

                using (var sourceStream = System.IO.File.OpenRead(reportPath))
                {
                    report = (Report)reportPackager.UnpackageDocument(sourceStream);
                }

                var dataSources = report.GetDataSources().OfType<SharedDataSource>();
                var webServiceDataSources = dataSources.Where(x => x.DataSourceReference is WebServiceDataSource)
                    .Select(x => x.DataSourceReference)
                    .Cast<WebServiceDataSource>();

                foreach (var ds in webServiceDataSources)
                {
                    ds.Parameters.Add(cookieKey, WebServiceParameterType.Cookie, cookie);
                }

                var irs = new InstanceReportSource() { ReportDocument = report };
                return irs;
            }
        }

        return null;
    }

    private bool IsValidReportPath(string path)
    {
        return ReportDocumentUtils.IsSupportedReportDocument(path);
    }

    private string MapPath(string path)
    {
        if (!string.IsNullOrEmpty(repositoryDirectory))
        {
            return Path.Combine(repositoryDirectory, path);
        }

        return path;
    }
}

Is there a step missing to resolve the WebServiceDataSource, or is this approach incorrect?
Momchil
Telerik team
commented on 12 Jan 2024, 04:17 PM

Hello Lennert,

The general approach is valid when you are working with the common data source components but would not work with shared data sources. This is because the DataSourceReference property of the SharedDataSource is for internal use only.

You could use the Path property of the SharedDataSource to find the XML definition of the underlying data source and deserialize it to a DataSource with the help of the ReportXmlSerializer.

using (var fs = new FileStream(absolutePathToSharedDataSourceDefinition, FileMode.Open, FileAccess.Read, FileShare.Read))
{
    dataSource = (Telerik.Reporting.DataSource)new Telerik.Reporting.XmlSerialization.ReportXmlSerializer().Deserialize(fs);
}

Then you can modify this data source instance and assign it to the report.

Still, if you intend to use only shared data sources, I would suggest implementing a custom SharedDataSourceResolver that modifies WebServiceDataSources accordingly. The CustomSharedDataSourceResolver can access the information from the HttpContext in the same manner as your CustomReportSourceResolver.

You can find a runnable example of a custom shared data source resolver in one of the .NET integration demos located in the Examples folder of your Telerik Reporting installation directory. By default:

C:\Program Files (x86)\Progress\Telerik Reporting <YOUR VERSION>\Examples\CSharp\CSharp.ReportExamples.VS2022.sln

I hope this helps.

 

Lennert
Top achievements
Rank 2
Iron
Iron
commented on 24 Jan 2024, 11:44 AM

Hi Momchil,

Thanks for the additional feedback. I managed to get it mostly working with the CustomSharedDataSourceResolver for the preview of the designer and in the report viewer.
Still running into an issue when designing the report in the Telerik.WebReportDesigner.Blazor.WebReportDesigner:
The requests of that designer are handled by a TelerikReportDesignerController which inherits from ReportDesignerControllerBase, similar to the example project CSharp.Net6.BlazorIntegrationDemo.
When using the WebReportDesigner, the RenderRuntimeItem method is called from the ReportDesignerControllerBase.

The SharedDataSourceResolver that is created in that method is NOT the CustomSharedDataSourceResolver. Instead the SharedDataSourceResolver is created by this code:

SharedDataSourceResolverFactory.Create(new ResourceResolver(sharedDataSourceStorage, text), ReportingConfiguration.Default)

Unfortunately the RenderRuntimeItem method can not be overridden in the derived class, since it is private, and some of the methods/types used in it are also not accessible from the derived class.

I would expect that the SharedDataSourceResolver from the ReportServiceConfiguration is used, or that it can we explicitly set in the ReportDesignerServiceConfiguration.
I did come accross a CreateSharedDataSourceResolver method in the ReportDesignerControllerBase that can be overridden, but that function is not called from the RenderRuntimeItem anyway.

Any suggestions on how to use the CustomSharedDataSourceResolver for the WebReportDesigner?

Another issue i ran into:
I have tried to use the 'Use JSON inline as hard-coded data while designing the report' setting when configuring the shared datasource, but it looks like this is ignored and the data is request from the webservice anyway when designing a report through the WebReportDesigner.

Momchil
Telerik team
commented on 29 Jan 2024, 11:29 AM

Hi Lennert,

I tested the render methods of the ReportDesignerControllerBase with a custom SharedDataSourceResolver but the Resolve method of the custom resolver was called on my end.

Just in case, I examined the source code of the SharedDataSourceResolverFactory and it confirms that the resolver from the configuration takes precedence.

static partial class SharedDataSourceResolverFactory
{
    /// <summary>
    /// Creates a SharedDataSourceResolver, taking ReportingConfigurationSection with precedence over default implementation.
    /// Otherwise, returns the default file-based implementation.
    /// </summary>
    /// <param name="resourceResolver"></param>
    /// <returns></returns>
    public static ISharedDataSourceResolver Create(IResourceResolver resourceResolver, ReportingConfigurationSection configuration)
    {
        if (configuration == null)
        {
            return Create(resourceResolver);
        }

        return ConfigSharedDataSourceResolverFactory.CreateFromConfiguration(configuration.Processing.SharedDataSourceResolver)
                                                ?? Create(resourceResolver);
    }

    /// <summary>
    /// Creates a SharedDataSourceResolver based on the provided IResourceResolver.
    /// </summary>
    /// <param name="resourceResolver"></param>
    /// <returns></returns>
    public static ISharedDataSourceResolver Create(IResourceResolver resourceResolver)
    {
        return resourceResolver == null 
            ? null 
            : CreateDefaultSdsResolver(resourceResolver);
    }
}

Could you check whether the custom shared data source resolver is registered and initialized correctly?

Registering the resolver: see processing Element -> sharedDataSourceResolver.

Initializing the resolver: see Program.cs in one of the .NET integration demos.

Regarding the issue with inline data. I managed to reproduce it locally and it seems that it is specific to the Web Report Designer as the design time of the Standalone Report Designer works as expected.

We consider this a bug and I have logged it in our Pubilc Feedback Portal on your behalf. I am also updating your account's Telerik Points as a token of gratitude for helping us notice this.

Lennert
Top achievements
Rank 2
Iron
Iron
commented on 29 Jan 2024, 01:53 PM

Hi Momchil,

Thanks for the reply and for logging the bug.
I rechecked and the CustomSharedDataSourceResolver is being resolved in the WebReportDesigner now. Not sure why it wasn't working last time.

Still have an issue: At design-time of the report in the WebReportDesigner, whenever a render is triggered for a component in the report (e.g.: api/TelerikReportDesigner/render/Graph) the UserIdentity.Current is null.
The GetUserIdentity() method of the ReportDesignerController is not being invoked at design-time. Should this not be the case at design-time?

Kind regards,

Lennert

Momchil
Telerik team
commented on 31 Jan 2024, 03:58 PM

Hi Lennert,

You are correct, the GetUserIdentity method is called only during the Get Report Parameters, Resolve Report Instance, and Resolve Document requests currently.

Still, you can override the different render methods of the ReportDesignerControllerBase class and set the identity so that it can be used for the design-time preview. For example:

public override Task<IActionResult> RenderGraphAsync()
{
    UserIdentity.Current = this.GetUserIdentity();
    return base.RenderGraphAsync();
}
Alternatively, if you wish to assign the user identity for every request, you can use the constructor of the report designer controller.
public ReportDesignerController(IReportDesignerServiceConfiguration reportDesignerServiceConfiguration, IReportServiceConfiguration reportServiceConfiguration)
    : base(reportDesignerServiceConfiguration, reportServiceConfiguration)
{
    UserIdentity.Current = this.GetUserIdentity();
}

Would that work for you?

Best Regards,

Momchil

Lennert
Top achievements
Rank 2
Iron
Iron
commented on 01 Feb 2024, 01:55 PM

Hi Momchil,

Thanks again for your reply.

We can't set the UserIdentity.Current in the constructor, since we need to access the HttpContext to retrieve the cookie from it, but HttpContext is null when the constructor is executed.
Your other suggestion to override the Render endpoints does work fine. Thanks for that!

Besides the Render endpoints we also need to pass the cookie to the WebServiceDataSource for the following endpoints.

- /data/model (ReportDesignerControllerBase.GetDataModel(DataSourceInfo ds))

- /definitionresources/preview/webservice (ReportDesignerControllerBase.PreviewWebServiceData(DataSourceInfo dataSourceInfo)

We were able to override the last, but we can't override the GetDataModel method since it is not marked 'virtual'.
I have submitted a feature request for that. https://feedback.telerik.com/reporting/1639526-allow-to-override-public-reportdesignercontrollerbase-endpoints 

I don't have any other questions on this topic for now, so consider this post closed.

KR,

Lennert

Momchil
Telerik team
commented on 06 Feb 2024, 02:41 PM

Hi Lennert,

I am happy to hear that overriding the render endpoints worked.

I discussed your feature request with the team and we agree with the points you make which is why I have marked the request as approved.

Thank you for your feedback.

Best Regards,

Momchil

Tags
DataSource WebService
Asked by
Lennert
Top achievements
Rank 2
Iron
Iron
Answers by
Momchil
Telerik team
Share this question
or