<commit_before>﻿//******************************************************************************************************
//  GrafanaAuthProxyController.cs - Gbtc
//
//  Copyright © 2017, Grid Protection Alliance.  All Rights Reserved.
//
//  Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
//  the NOTICE file distributed with this work for additional information regarding copyright ownership.
//  The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
//  not use this file except in compliance with the License. You may obtain a copy of the License at:
//
//      http://opensource.org/licenses/MIT
//
//  Unless agreed to in writing, the subject software distributed under the License is distributed on an
//  "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
//  License for the specific language governing permissions and limitations.
//
//  Code Modification History:
//  ----------------------------------------------------------------------------------------------------
//  12/02/2017 - <NAME>
//       Generated original version of source code.
//
//******************************************************************************************************

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using GSF;
using GSF.Configuration;
using GSF.Data;
using GSF.Data.Model;
using GSF.Security;
using GSF.Security.Cryptography;
using GSF.Security.Model;
using GSF.Threading;
using Newtonsoft.Json.Linq;
using CancellationToken = System.Threading.CancellationToken;
using Http = System.Net.WebRequestMethods.Http;

#pragma warning disable 169, 414, 649
// ReSharper disable ClassNeverInstantiated.Local
// ReSharper disable InconsistentNaming
namespace openHistorian.Adapters
{
    /// <summary>
    /// Creates a reverse proxy to a hosted Grafana instance that handles integrated authentication.
    /// </summary>
    public class GrafanaAuthProxyController : ApiController
    {
        #region [ Members ]

        // Nested Types
        private class UserDetail
        {
            public int id;
            public string email;
            public string name;
            public string login;
            public string theme;
            public int orgId;
            public bool isGrafanaAdmin;
        }

        private class OrgUserDetail
        {
            public int orgId;
            public int userId;
            public string email;
            public string login;
            public string role;
        }

        // Constants

        /// <summary>
        /// Defines the default installation server path for Grafana.
        /// </summary>
        public const string DefaultServerPath = "Grafana\\bin\\grafana-server.exe";

        /// <summary>
        /// Default URL for the hosted Grafana process.
        /// </summary>
        public const string DefaultHostedURL = "http://localhost:8185";

        /// <summary>
        /// Default timeout, in seconds, for system initialization.
        /// </summary>
        public const int DefaultInitializationTimeout = 15;

        /// <summary>
        /// Default Grafana organization ID.
        /// </summary>
        public const int DefaultOrganizationID = 1;

        /// <summary>
        /// Grafana admin role name.
        /// </summary>
        public const string GrafanaAdminRoleName = "GrafanaAdmin";

        #endregion

        #region [ Methods ]

        /// <summary>
        /// Handle proxy of the root Grafana URL, e.g., http://localhost:8180/grafana.
        /// </summary>
        /// <param name="cancellationToken">Cancellation token.</param>
        /// <returns>Proxied response.</returns>
        [AcceptVerbs(Http.Get, Http.Head, Http.Post, Http.Put, Http.MkCol), HttpDelete, HttpPatch]
        [SuppressMessage("Security", "SG0016", Justification = "CSRF vulnerability handled by Grafana")]
        public async Task<HttpResponseMessage> ProxyRoot(CancellationToken cancellationToken)
        {
            return await ProxyPage("", cancellationToken);
        }

        /// <summary>
        /// Handle proxy of specified Grafana URL.
        /// </summary>
        /// <param name="url">URL to proxy.</param>
        /// <param name="cancellationToken">Cancellation token.</param>
        /// <returns>Proxied response.</returns>
        [AcceptVerbs(Http.Get, Http.Head, Http.Post, Http.Put, Http.MkCol), HttpDelete, HttpPatch]
        [SuppressMessage("Security", "SG0016", Justification = "CSRF vulnerability handled by Grafana")]
        public async Task<HttpResponseMessage> ProxyPage(string url, CancellationToken cancellationToken)
        {
            // Handle special URL commands
            switch (url.ToLowerInvariant())
            {
                case "syncusers":
                    return HandleSynchronizeUsersRequest(Request, RequestContext.Principal as SecurityPrincipal);
                case "servertime":
                    return HandleServerTimeRequest(Request);
                case "logout":
                    return HandleGrafanaLogoutRequest(Request);
                case "api/login/ping":
                    return HandleGrafanaLoginPingRequest(Request, RequestContext.Principal as SecurityPrincipal);
            }

            if (url.StartsWith("avatar/", StringComparison.OrdinalIgnoreCase))
                return HandleGrafanaAvatarRequest(Request);

            // Proxy all other requests
            SecurityPrincipal securityPrincipal = RequestContext.Principal as SecurityPrincipal;

            if ((object)securityPrincipal == null || (object)securityPrincipal.Identity == null)
                throw new SecurityException($"User \"{RequestContext.Principal?.Identity.Name}\" is unauthorized.");

            Request.Headers.Add(s_authProxyHeaderName, securityPrincipal.Identity.Name);
            Request.RequestUri = new Uri($"{s_baseUrl}/{url}{Request.RequestUri.Query}");

            if (Request.Method == HttpMethod.Get)
                Request.Content = null;

            HttpResponseMessage response = await s_http.SendAsync(Request, cancellationToken);
            HttpStatusCode statusCode = response.StatusCode;

            if (statusCode == HttpStatusCode.NotFound || statusCode == HttpStatusCode.Unauthorized)
            {
                ThreadPool.QueueUserWorkItem(state =>
                {
                    string userName = (state as SecurityPrincipal)?.Identity?.Name;

                    if (string.IsNullOrEmpty(userName))
                        return;

                    try
                    {
                        // Validate user has a role defined in latest security context
                        Dictionary<string, string[]> securityContext = s_latestSecurityContext;

                        if ((object)securityContext == null)
                            return;

                        Dictionary<string, string[]> userRoles = StartUserSynchronization(userName);
                        string newUserMessage = securityContext.ContainsKey(userName) ? "" : $"New user \"{userName}\" encountered. ";

                        OnStatusMessage($"{newUserMessage}Security context with {userRoles.Count} users and associated roles queued for Grafana user synchronization.");
                    }
                    catch (Exception ex)
                    {
                        OnStatusMessage($"ERROR: Failed while queuing Grafana user synchronization for new user \"{userName}\": {ex.Message}");
                    }
                },
                RequestContext.Principal);
            }

            return response;
        }<commit_msg>Added hacks to GrafanaAuthProxyController to retry failed requests without the authorization header to work around Internet Explorer bug.<commit_after>﻿//******************************************************************************************************
//  GrafanaAuthProxyController.cs - Gbtc
//
//  Copyright © 2017, Grid Protection Alliance.  All Rights Reserved.
//
//  Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
//  the NOTICE file distributed with this work for additional information regarding copyright ownership.
//  The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
//  not use this file except in compliance with the License. You may obtain a copy of the License at:
//
//      http://opensource.org/licenses/MIT
//
//  Unless agreed to in writing, the subject software distributed under the License is distributed on an
//  "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
//  License for the specific language governing permissions and limitations.
//
//  Code Modification History:
//  ----------------------------------------------------------------------------------------------------
//  12/02/2017 - <NAME>
//       Generated original version of source code.
//
//******************************************************************************************************

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using GSF;
using GSF.Configuration;
using GSF.Data;
using GSF.Data.Model;
using GSF.Security;
using GSF.Security.Cryptography;
using GSF.Security.Model;
using GSF.Threading;
using Newtonsoft.Json.Linq;
using CancellationToken = System.Threading.CancellationToken;
using Http = System.Net.WebRequestMethods.Http;

#pragma warning disable 169, 414, 649
// ReSharper disable ClassNeverInstantiated.Local
// ReSharper disable InconsistentNaming
namespace openHistorian.Adapters
{
    /// <summary>
    /// Creates a reverse proxy to a hosted Grafana instance that handles integrated authentication.
    /// </summary>
    public class GrafanaAuthProxyController : ApiController
    {
        #region [ Members ]

        // Nested Types
        private class UserDetail
        {
            public int id;
            public string email;
            public string name;
            public string login;
            public string theme;
            public int orgId;
            public bool isGrafanaAdmin;
        }

        private class OrgUserDetail
        {
            public int orgId;
            public int userId;
            public string email;
            public string login;
            public string role;
        }

        // Constants

        /// <summary>
        /// Defines the default installation server path for Grafana.
        /// </summary>
        public const string DefaultServerPath = "Grafana\\bin\\grafana-server.exe";

        /// <summary>
        /// Default URL for the hosted Grafana process.
        /// </summary>
        public const string DefaultHostedURL = "http://localhost:8185";

        /// <summary>
        /// Default timeout, in seconds, for system initialization.
        /// </summary>
        public const int DefaultInitializationTimeout = 15;

        /// <summary>
        /// Default Grafana organization ID.
        /// </summary>
        public const int DefaultOrganizationID = 1;

        /// <summary>
        /// Grafana admin role name.
        /// </summary>
        public const string GrafanaAdminRoleName = "GrafanaAdmin";

        #endregion

        #region [ Methods ]

        /// <summary>
        /// Handle proxy of the root Grafana URL, e.g., http://localhost:8180/grafana.
        /// </summary>
        /// <param name="cancellationToken">Cancellation token.</param>
        /// <returns>Proxied response.</returns>
        [AcceptVerbs(Http.Get, Http.Head, Http.Post, Http.Put, Http.MkCol), HttpDelete, HttpPatch]
        [SuppressMessage("Security", "SG0016", Justification = "CSRF vulnerability handled by Grafana")]
        public async Task<HttpResponseMessage> ProxyRoot(CancellationToken cancellationToken)
        {
            return await ProxyPage("", cancellationToken);
        }

        /// <summary>
        /// Handle proxy of specified Grafana URL.
        /// </summary>
        /// <param name="url">URL to proxy.</param>
        /// <param name="cancellationToken">Cancellation token.</param>
        /// <returns>Proxied response.</returns>
        [AcceptVerbs(Http.Get, Http.Head, Http.Post, Http.Put, Http.MkCol), HttpDelete, HttpPatch]
        [SuppressMessage("Security", "SG0016", Justification = "CSRF vulnerability handled by Grafana")]
        public async Task<HttpResponseMessage> ProxyPage(string url, CancellationToken cancellationToken)
        {
            // Handle special URL commands
            switch (url.ToLowerInvariant())
            {
                case "syncusers":
                    return HandleSynchronizeUsersRequest(Request, RequestContext.Principal as SecurityPrincipal);
                case "servertime":
                    return HandleServerTimeRequest(Request);
                case "logout":
                    return HandleGrafanaLogoutRequest(Request);
                case "api/login/ping":
                    return HandleGrafanaLoginPingRequest(Request, RequestContext.Principal as SecurityPrincipal);
            }

            if (url.StartsWith("avatar/", StringComparison.OrdinalIgnoreCase))
                return HandleGrafanaAvatarRequest(Request);

            // Proxy all other requests
            SecurityPrincipal securityPrincipal = RequestContext.Principal as SecurityPrincipal;

            if ((object)securityPrincipal == null || (object)securityPrincipal.Identity == null)
                throw new SecurityException($"User \"{RequestContext.Principal?.Identity.Name}\" is unauthorized.");

            Request.Headers.Add(s_authProxyHeaderName, securityPrincipal.Identity.Name);
            Request.RequestUri = new Uri($"{s_baseUrl}/{url}{Request.RequestUri.Query}");

            if (Request.Method == HttpMethod.Get)
                Request.Content = null;

            // HACK: Cannot reuse request objects, but we may need to tweak and resend the same request
            Func<Task<HttpRequestMessage>> requestFactory = MakeRequestFactory();
            HttpRequestMessage request = await requestFactory();
            HttpResponseMessage response = await s_http.SendAsync(request, cancellationToken);
            HttpStatusCode statusCode = response.StatusCode;

            if (statusCode == HttpStatusCode.NotFound || statusCode == HttpStatusCode.Unauthorized)
            {
                // HACK: Internet Explorer sometimes applies cached authorization headers to concurrent AJAX requests
                if ((object)Request.Headers.Authorization != null)
                {
                    HttpRequestMessage retryRequest = await requestFactory();
                    retryRequest.Headers.Authorization = null;

                    HttpResponseMessage retryResponse = await s_http.SendAsync(retryRequest, cancellationToken);
                    HttpStatusCode retryStatusCode = retryResponse.StatusCode;

                    if (retryStatusCode != HttpStatusCode.NotFound && retryStatusCode != HttpStatusCode.Unauthorized)
                    {
                        request = retryRequest;
                        response = retryResponse;
                        statusCode = retryStatusCode;
                    }
                }
            }

            if (statusCode == HttpStatusCode.NotFound || statusCode == HttpStatusCode.Unauthorized)
            {
                ThreadPool.QueueUserWorkItem(state =>
                {
                    string userName = (state as SecurityPrincipal)?.Identity?.Name;

                    if (string.IsNullOrEmpty(userName))
                        return;

                    try
                    {
                        // Validate user has a role defined in latest security context
                        Dictionary<string, string[]> securityContext = s_latestSecurityContext;

                        if ((object)securityContext == null)
                            return;

                        Dictionary<string, string[]> userRoles = StartUserSynchronization(userName);
                        string newUserMessage = securityContext.ContainsKey(userName) ? "" : $"New user \"{userName}\" encountered. ";

                        OnStatusMessage($"{newUserMessage}Security context with {userRoles.Count} users and associated roles queued for Grafana user synchronization.");
                    }
                    catch (Exception ex)
                    {
                        OnStatusMessage($"ERROR: Failed while queuing Grafana user synchronization for new user \"{userName}\": {ex.Message}");
                    }
                },
                RequestContext.Principal);
            }

            return response;
        }

        private Func<Task<HttpRequestMessage>> MakeRequestFactory()
        {
            MemoryStream content = null;

            return async () =>
            {
                HttpRequestMessage clone = new HttpRequestMessage(Request.Method, Request.RequestUri);

                clone.Version = Request.Version;

                foreach (KeyValuePair<string, object> property in Request.Properties)
                    clone.Properties.Add(property);

                foreach (KeyValuePair<string, IEnumerable<string>> header in Request.Headers)
                    clone.Headers.TryAddWithoutValidation(header.Key, header.Value);

                if ((object)content == null && (object)Request.Content != null)
                {
                    content = new MemoryStream();
                    await Request.Content.CopyToAsync(content);
                    content.Position = 0;
                }

                if ((object)content != null)
                {
                    MemoryStream cloneContent = new MemoryStream();
                    await content.CopyToAsync(cloneContent);
                    content.Position = 0;
                    cloneContent.Position = 0;
                    clone.Content = new StreamContent(cloneContent);
                }

                return clone;
            };
        }
{'fraction_non_alphanumeric': 0.11973641915782707, 'fraction_numerical': 0.004446587378120647, 'mean_word_length': 2.047673469387755, 'pattern_counts': {'":': 10, '<': 72, '<?xml version=': 0, '>': 75, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2373407', 'n_tokens_mistral': 4397, 'n_tokens_neox': 4206, 'n_words': 1248}