Marcos Vainer Loeff

ASP.NET MVC 5 with PI AF SDK: Part 2 - Security

Blog Post created by Marcos Vainer Loeff Employee on Nov 13, 2014

On my post ASP.NET MVC 5 with PI AF SDK: Part 1 - Introduction, I have shown how to develop a web application using ASP.NET MVC 5 and PI AF SDK that will show the snapshot of some PI Points after making a search. On this blog post, we will talk a little about securing this web application.

 

I recommend you to download the source code package that you can refer to in case you are having problems.

How does a controller work?

In order to know where to organize our PI objects within the controller, we need to understand how an ASP.NET MVC Controller works. To do so, we will create a test controller like the one below:

 
 public class ControllerTestController : Controller
    {
        private int pClassCount = 0;
        private static int pStaticCount = 0;
        public ActionResult DisplayLocalVariable()
        {
            int pLocalCount = 0;
            pLocalCount++;
            return View("DisplayVariable",pLocalCount);
        } 

        public ActionResult DisplayPrivateClassVariable()
        {
            pClassCount++;
            return View("DisplayVariable",pClassCount);
        } 

        public ActionResult DisplayPrivateStaticVariable()
        {
            pStaticCount++;
            return View("DisplayVariable",pStaticCount);
        }
    }

On this controller named TestController, we have created 3 actions:

  • DisplayLocalVariable: this action will check what happen with the local variables.
  • DisplayPrivateClassVariable: this action will check what happen with private variables from objects instantiated from the ControllerTest class.
  •  DisplayPrivateStaticVariable: this action will check what happen with private static variables ControllerTest class.

All 3 actions will result on the same view DisplayVariable.cshtml located under the Folder Views\ControllerTest:

 

 

 
@model int
@{
    Layout = null;
} 

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>DisplayVariable</title>
</head>
<body>
    <div>
        <p>Value: @Model.ToString()</p>
    </div>
</body>
</html>

 

It is time to test and see what happens with each of this action. If we call the DisplayLocalVariable (http://localhost:XXXX/ControllerTest/DisplayLocalVariable), all the returned pages will show (Value: 1) as expected. The same would happen if we call the DisplayPrivateClassVariable action. Nevertheless, the value increase each time we call DisplayPrivateStaticVariable action.

 

The conclusion is simple. Each time we call an action, an object of the Controller class is instantiated. Therefore, if we want to use the same objects to connect to our PISystem and PIServer they should be static variables of the controller. If this is not the case, you would have to instantiate them.

Securing your web application with no impersonation

If you are developing in ASP.NET MVC 5, you will have to deploy your application in IIS. Your web site or application will use an application pool which will be linked to an account.

 

In case you are not making any impersonation, this account of the application pool will be used to authenticate in the PI Data Archive or PI Data Archive if you call PISystem.Connect() or PIServer.Connect().

 

Having the sample application developed on my previous blog post as a starting point, we will:

  • Rename the HomeController to SecurityNoImpersonationController.
  • Create a property called GetPIServer() that will return the PIServer object.
  • Create GetPIPointSnapshotValueList and GetPIDisplayConnectionInfo protected methods on the SecurityNoImpersonationController in order to separate the code snippets related to the PI System.
  • Create a new action called DisplayPIConnectionInfo() as its view on the Shared folder which will show the security and connection information. 

The code snippet for the modified controller is:

 

 

 
using OSIsoft.AF;
using OSIsoft.AF.PI;
using PIAFSDK_WebApplication.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;


namespace PIAFSDK_WebApplication.Controllers
{
    public class SecurityNoImpersonationController : Controller
    {

        public PISystem GetPISystem()
        {
            PISystems piSystems = new PISystems();
            return piSystems["MARC-PI2014"];
        }

        public PIServer GetPIServer()
        {
            PIServers piServers = new PIServers();
            return piServers["MARC-PI2014"];
        }

        public ActionResult Index()
        {
            return View("Query");
        }

        public ActionResult Query()
        {
            return View();
        }

        protected virtual List<PIPointSnapshotModelView> GetPIPointSnapshotValueList(string piPointNameQuery, string pointSourceNameQuery)
        {
            PIPointQuery query1 = new PIPointQuery(PICommonPointAttributes.Tag, OSIsoft.AF.Search.AFSearchOperator.Equal, piPointNameQuery);
            PIPointQuery query2 = new PIPointQuery(PICommonPointAttributes.PointSource, OSIsoft.AF.Search.AFSearchOperator.Equal, pointSourceNameQuery);
            IEnumerable<PIPoint> foundPoints = PIPoint.FindPIPoints(GetPIServer(), new PIPointQuery[] { query1, query2 });
            PIPointList pointlist = new PIPointList(foundPoints.Take(1000));
            var snapshots = pointlist.Snapshot();
            List<PIPointSnapshotModelView> PIPointSnapshotValueList = new List<PIPointSnapshotModelView>();
            for (int i = 0; i < pointlist.Count; i++)
            {
                PIPointSnapshotValueList.Add(new PIPointSnapshotModelView(pointlist
.Name.ToString(), snapshots
));
            }
            return PIPointSnapshotValueList;
        }

        public ActionResult Result(string piPointNameQuery, string pointSourceNameQuery)
        {
            List<PIPointSnapshotModelView> PIPointSnapshotValueList = GetPIPointSnapshotValueList(piPointNameQuery, pointSourceNameQuery);
            return View(PIPointSnapshotValueList);
        }

        protected virtual Dictionary<string, string> GetPIConnectionInfo()
        {
            Dictionary<string, string> connectionData = new Dictionary<string, string>();
            PIServer piServer = GetPIServer();
            piServer.Connect();
            connectionData.Add("ServerHost", piServer.ConnectionInfo.Host);
            connectionData.Add("ServerType", "PI Data Archive");
            connectionData.Add("CurrentUserName", piServer.CurrentUserName);
            connectionData.Add("IsConnected", piServer.ConnectionInfo.IsConnected.ToString());
            connectionData.Add("TimeOut", piServer.ConnectionInfo.OperationTimeOut.TotalSeconds.ToString());
            connectionData.Add("Port", piServer.ConnectionInfo.Port.ToString());
            connectionData.Add("WindowsIdentityName", System.Security.Principal.WindowsIdentity.GetCurrent().Name);
            return connectionData;
        }

        public ActionResult DisplayPIConnectionInfo()
        {
            Dictionary<string, string> connectionData = GetPIConnectionInfo();
            return View("DisplayConnectionInfo", connectionData);
        }
    }
}

 

 

Concerning the Views, move all the Views from the Home folder to the Shared folder, since all the controllers should have access to those Views.

 

Create a new View called DisplayConnectionInfo.cshtml on Views\Shared folder with the following content:

 

 

 
@model Dictionary<string, string>
@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>DisplayConnectionInfo</title>
</head>
<body>
    <div>
        <p>Server Host Name: @Model["ServerHost"]</p>
        <p>Server Type: @Model["ServerType"]</p>
        <p>Current User Name: @Model["CurrentUserName"]</p>
        <p>Is Connected: @Model["IsConnected"]</p>
        <p>TimeOut: @Model["TimeOut"]</p>
        <p>Port: @Model["Port"]</p>
        <p>Windows identity name: @Model["WindowsIdentityName"]</p>
    </div>
</body>
</html>

 

 

We also need to change the Query.cshtml view since its form will send data to the Result method from the controller who called the View. The new version of the Query.cshtml is:

 
@{
    Layout = null;
    string controllerName = ViewContext.RouteData.Values["controller"].ToString();
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <link href="~/Content/bootstrap.css" rel="stylesheet" />
    <link href="~/Content/bootstrap-theme.css" rel="stylesheet" />
    <script src="~/Scripts/jquery-1.9.0.js"></script>
    <script src="~/Scripts/bootstrap.js"></script>
    <title>Query PI Data Archive</title>
</head>
<body>
    <div class="col-sm-8 col-sm-offset-2">
        <h1>My first ASP.NET MVC with PI AF SDK</h1>
        <br /><br />
        <h4>Search for PI Points and check its snapshots.</h4>
        <br /><br />
        <div class="container">
            <div class="row">
                @using (Html.BeginForm("Result",controllerName , FormMethod.Get, new { @class = "form-inline", role = "form" }))
                {
                    <div class="form-group">
                        <label class="sr-only" for="name">Search PI Point: </label>
                        @Html.TextBox("piPointNameQuery", string.Empty, new { @class = "form-control", placeholder = "PI Point" })
                    </div>
                    <div class="form-group">
                        <label class="sr-only" for="inputfile">With this Point Source: </label>
                        @Html.TextBox("pointSourceNameQuery", string.Empty, new { @class = "form-control", placeholder = "Point Source" })
                    </div>
                    <button type="submit" class="btn btn-default">Submit</button>
                }
            </div>
        </div>
    </div>
</body>
</html>

 Start your solution with VisualStudio and browser to http//localhost:XXXX/SecurityNoImpersonation/Query making sure that the results of the snapshots are still working as before. Then go to  /SecurityNoImpersonation/DisplayPIConnectionInfo.

 

As I am using Visual Studio logged as the user name marc.adm on MARC domain, the web server renders the HTML page shown on Figure 1.

 

2313.fig1.png

 

Figure 1 – DisplayPIConnectionInfo HTML page when running from Visual Studio with no impersonation.

 

After deploying the web application to a web server whose hostname is MARC-WEB-SQL, I can access a a similar URI resulting on the HTML page shown on Figure 2.

 

4087.fig2.png

 

Figure 2 – DisplayPIConnectionInfo HTML page when running on IIS with no impersonation.

 

The account displayed is MARC\MARC-WEB-SQL$ instead of MARC\marc.adm since the application pool of this application in IIS is running with the Network Service account used by the web application to connect to the PI Data Archive.

 

Note that the results of the web application might be different when using IIS and Visual Studio since both domain accounts used to connect to the PI Data Archive are different. This means that the list of PI Points found will depend on the permissions configured on the PI Data Archive for each domain account.

 

In this example all users will receive the same results since the all connections to the PI Data Archive use the same domain account for authentication.

Securing your web application with impersonation

A more secure approach would be to use the end-user credentials to authenticate the PI Data Archive. This way even if there are many different users accessing the web application each one would receive different results for the same query inputs according to the security settings on PI Data Archive.

 

In this case, impersonation is required which can make things a little more complicated since there is the need of configuring Active Directory for Kerberos delegation, so the impersonated identity is allowed to flow from the web server to either the PI Server or the AF Server. Please refer to this article for more details..

 

The good part is that PI AF SDK would make all the hard work for us by storing in its cache the security information from each domain account.

 

The structure of the impersonation is:

 
            System.Security.Principal.WindowsImpersonationContext impersonationContext;
            impersonationContext = ((System.Security.Principal.WindowsIdentity)User.Identity).Impersonate();         

            //Impersonation section
            impersonationContext.Undo();

 If you call PIServers myPIServers = new PIServers(); on the Impersonation section, the web application will authenticate with its domain account on the PI System and not with the application pool account. That is why in this case, the PIServer and the PISystem objects cannot be private static members but local variables.

 

With this concept in mind, we have created the SecurityWithImpersonationController which is inherited from the SecurityNoImpersonationController. On the SecurityWithImpersonationController we are using the same Actions and Views (since they are stored on the Shared folder) from the SecurityNoImpersonationController. The only difference is that we are overriding the GetPIPointSnapshotValueList and GetPIConnectionInfo methods in order to add impersonation. Simple like that!

 

 

 
using OSIsoft.AF.PI;
using PIAFSDK_WebApplication.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace PIAFSDK_WebApplication.Controllers
{
    public class SecurityWithImpersonationController : SecurityNoImpersonationController
    {
        protected override List<PIPointSnapshotModelView> GetPIPointSnapshotValueList(string piPointNameQuery, string pointSourceNameQuery)
        {
            System.Security.Principal.WindowsImpersonationContext impersonationContext = null;
            try
            {
                impersonationContext = ((System.Security.Principal.WindowsIdentity)User.Identity).Impersonate();
                List<PIPointSnapshotModelView> PIPointSnapshotValueList = base.GetPIPointSnapshotValueList(piPointNameQuery, pointSourceNameQuery);
                return PIPointSnapshotValueList;
            }
            finally
            {
                impersonationContext.Undo();
            }  
        }

        protected override Dictionary<string, string> GetPIConnectionInfo()
        {
            System.Security.Principal.WindowsImpersonationContext impersonationContext = null;
            try
            {
                impersonationContext = ((System.Security.Principal.WindowsIdentity)User.Identity).Impersonate();
                Dictionary<string, string> connectionData = base.GetPIConnectionInfo();
                return connectionData;
            }
            finally
            {
                impersonationContext.Undo();
            }
           
        }      
    }
}

 

 

After deploying again in IIS, we will receive the HTML page on Figure 3 using impersonation.

 

6012.fig3.png

 

Figure 3 – DisplayPIConnectionInfo HTML page when running on IIS with impersonation logged with marc.adm.

 

Although I am still using the computer MARC-WEB-SQL$ domain account for the Application Pool of this web application, I can see that the currentUserName is marc.adm since MARC-WEB-SQL$ impersonated the marc.adm user as already explained.  The same would happen for the Result action.

 

If we log in with another user (MARC\marc.user), we will see a different page with different security settings to access the PI Data Archive. Note that marc.user is not piadmin or piadmins.

 

6505.fig4.png

 

Figure 4 – DisplayPIConnectionInfo HTML page when running on IIS with impersonation logged with marc.user.

 


 

If you are using the PISystems object instead of the PIServers, the logic is exactly the same. On the download source package we have implemented the action DisplayAFConnectionInfo() and GetAFConnectionInfo() method on the SecurityNoImpersonation controller. For the SecurityWithImpersonation, we have overriden the  GetAFConnectionInfo() to implement the impersonation. Refer to the source code package for more details.

 

A good test would be to set up different security settings for some PI Points and log with these users and check if the results are returned correctly according to the user domain account permissions on the PI Data Archive. Nevertheless, this post is becoming really long.  Instead, let’s focus on what is important: a final recommendation list.

Final recommendations

Recommendations for using AF SDK on ASP.NET MVC 5:

  • Avoid using the Connect(NetworkCredential networkCredential) overrides since the AF SDK will disconnect the previous connection, even if it is the same credential.  This can cause errors to occur if other threads are using or have cached AF SDK objects.  Exceptions with a message “Database 'XXX' has been disconnected” are typical.  Rather passing credentials, use the identity of your application or the impersonated user to connect. Please refer to this thread for more details.
  • With the impersonation, we respect the user security for each web client. But the IIS running the process will have higher memory footprint as AFSDK will have to maintain a different object hierarchy for each different user. You can also use impersonation with PISystems object in order to respect user security on the PI AF Server.
  • The Connect() method is not typically needed because AF will connect automatically.
  • Do not to call PISystem.Disconnect() or PIServer.Disconnect() unless you have finished accessing this server and will not be using it again for a period time. Do not call it after every web-request:
    • Re-connecting is relatively expensive. 
    • Any existing objects from that connection will become invalid.
    • Web applications are multi-threaded.  Disconnecting could destroy other another thread’s work-in-progress. 
    • AF SDK will clean up idle connections for you.

Conclusion

 I hope you many ASP.NET MVC developers will refer to this blog post as one of our materials when developing their web application with PI AF SDK. I will try to keep the final recommendations list updated.

 

Hope you have enjoyed!

Outcomes