//******************************************************************************************************
// SecurityProviderUtility.cs - Gbtc
//
// Copyright © 2012, 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://www.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:
// ----------------------------------------------------------------------------------------------------
// 06/25/2010 - Pinal C. Patel
// Generated original version of source code.
// 01/05/2011 - Pinal C. Patel
// Added NotificationSmtpServer and NotificationSenderEmail settings to the config file along with
// GeneratePassword() and SendNotification() utility methods.
// 01/24/2011 - Pinal C. Patel
// Updated the logic in IsResourceAccessible() to stop looking at other included resources once a
// match is found for the resource being evaluated.
// 02/03/2011 - Pinal C. Patel
// Updated the logic in IsResourceSecurable() and IsResourceAccessible() to allow for multiple
// resources to be specified delimited by ',' with the same role requirements in the config file.
// 08/02/2011 - Pinal C. Patel
// Modified IsResourceAccessible() to skip security check if no role is specified for a resource
// in the config to allow security to be setup when accessing the resource but not enforced and
// leave it to the resource to enforce it.
// 12/20/2012 - Starlynn Danyelle Gilliam
// Modified Header.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using System.Security.Principal;
using System.Text.RegularExpressions;
using System.Threading;
using GSF.Configuration;
using GSF.Net.Smtp;
using GSF.Security.Cryptography;
namespace GSF.Security
{
///
/// A helper class containing methods used in the implementation of role-based security.
///
public static class SecurityProviderUtility
{
#region [ Members ]
//Constants
private const string SettingsCategory = "SecurityProvider";
private const string DefaultProviderType = "GSF.Security.LdapSecurityProvider, GSF.Security";
private const string DefaultIncludedResources = "*=*";
private const string DefaultExcludedResources = "";
private const string DefaultNotificationSmtpServer = Mail.DefaultSmtpServer;
private const string DefaultNotificationSenderEmail = "sender@company.com";
#endregion
#region [ Static ]
// Static Fields
private static readonly ICollection s_excludedResources;
private static readonly IDictionary s_includedResources;
private static readonly string s_notificationSmtpServer;
private static readonly string s_notificationSenderEmail;
private static Dictionary> s_providerFactory;
private static readonly object s_providerFactoryLock = new object();
// Static Constructor
static SecurityProviderUtility()
{
// Load settings from config file.
ConfigurationFile config = ConfigurationFile.Current;
CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory];
settings.Add("IncludedResources", DefaultIncludedResources, "Semicolon delimited list of resources to be secured along with role names.");
settings.Add("ExcludedResources", DefaultExcludedResources, "Semicolon delimited list of resources to be excluded from being secured.");
settings.Add("NotificationSmtpServer", DefaultNotificationSmtpServer, "SMTP server to be used for sending out email notification messages.");
settings.Add("NotificationSenderEmail", DefaultNotificationSenderEmail, "Email address of the sender of email notification messages.");
s_includedResources = settings["IncludedResources"].ValueAsString().ParseKeyValuePairs();
s_excludedResources = settings["ExcludedResources"].ValueAsString().Split(';');
s_notificationSmtpServer = settings["NotificationSmtpServer"].ValueAsString();
s_notificationSenderEmail = settings["NotificationSenderEmail"].ValueAsString();
}
// Static Properties
// Static Methods
///
/// Creates a new based on the settings in the config file.
///
/// Username of the user for whom the is to be created.
/// The category used to store configuration settings for the provider.
/// An object that implements .
public static ISecurityProvider CreateProvider(string username, string settingsCategory = null) =>
CreateProvider(username, null, settingsCategory);
///
/// Creates a new based on the settings in the config file.
///
/// Username of the user for whom the is to be created.
/// obtained through alternative authentication mechanisms to provide authentication for the .
/// The category used to store configuration settings for the provider.
/// An object that implements .
public static ISecurityProvider CreateProvider(string username, IPrincipal passthroughPrincipal, string settingsCategory = null)
{
// Initialize the username
if (string.IsNullOrEmpty(username))
username = Thread.CurrentPrincipal.Identity.Name;
// If an application is being launched from an installer it will have the NT AUTHORITY\System Identity which
// will not have available user information - so we pickup username from Environment instead
if (username.StartsWith("NT AUTHORITY\\", StringComparison.OrdinalIgnoreCase))
username = Environment.UserDomainName + "\\" + Environment.UserName;
// Instantiate the provider
// ReSharper disable once AssignNullToNotNullAttribute
ISecurityProvider provider = ProviderFactory(settingsCategory)(username);
if (!string.IsNullOrEmpty(settingsCategory))
provider.SettingsCategory = settingsCategory;
// Initialize the provider
provider.LoadSettings();
provider.PassthroughPrincipal = passthroughPrincipal;
if (provider.CanRefreshData)
provider.RefreshData();
// Return initialized provider
return provider;
}
///
/// Creates a new based on the settings in the config file.
///
/// Object that contains data about the user to be used by the security provider.
/// The category used to store configuration settings for the provider.
/// An object that implements .
public static ISecurityProvider CreateProvider(UserData userData, string settingsCategory = null)
{
// Initialize the username
string username = userData.Username;
// Instantiate the provider
// ReSharper disable once AssignNullToNotNullAttribute
ISecurityProvider provider = ProviderFactory(settingsCategory)(username);
// Initialize the provider
provider.LoadSettings();
provider.UserData.Clone(userData);
// Return initialized provider
return provider;
}
///
/// Determines if the specified is to be secured based on settings in the config file.
///
/// Name of the resource to be checked.
/// true if the is to be secured; otherwise false/
public static bool IsResourceSecurable(string resource)
{
// Check if resource is excluded explicitly.
foreach (string exclusion in s_excludedResources)
{
if (IsRegexMatch(exclusion, resource))
return false;
}
// Check if resource is included explicitly.
foreach (KeyValuePair inclusion in s_includedResources)
{
foreach (string item in inclusion.Key.Split(','))
{
if (IsRegexMatch(item.Trim(), resource))
return true;
}
}
return false;
}
///
/// Determines if the current user, as defined by the , has permission to access
/// the specified based on settings in the config file.
///
/// Name of the resource to be checked.
/// The principal providing the security context for the user.
/// true if the current user has permission to access the ; otherwise false.
public static bool IsResourceAccessible(string resource, IPrincipal principal)
{
// Check if the resource has a role-based access restriction on it.
foreach (KeyValuePair inclusion in s_includedResources)
{
foreach (string item in inclusion.Key.Split(','))
{
if (IsRegexMatch(item.Trim(), resource))
{
// Allow security to be implemented inside the resource.
if (string.IsNullOrEmpty(inclusion.Value))
return true;
// Check resource role requirements against user's role subscription.
return principal.IsInRole(inclusion.Value);
}
}
}
return false;
}
///
/// Determines if the specified matches the specified .
///
/// Spec string that can include wildcards ('*'). For example, *.txt
/// Target string to be compared with the .
/// true if the matches the , otherwise false.
public static bool IsRegexMatch(string spec, string target)
{
spec = spec.Replace(".", "\\."); // Escape special regex character '.'.
spec = spec.Replace("?", "\\?"); // Escape special regex character '?'.
spec = spec.Replace("*", ".*"); // Convert '*' to its regex equivalent.
// Perform a case-insensitive regex match.
return Regex.IsMatch(target, string.Format("^{0}$", spec), RegexOptions.IgnoreCase);
}
///
/// Encrypts the password to a one-way hash using the SHA1 hash algorithm.
///
/// Password to be encrypted.
/// Encrypted password.
public static string EncryptPassword(string password)
{
return Cipher.GetPasswordHash(password);
}
///
/// Generates a random password of the specified with at least one uppercase letter, one lowercase letter, one special character and one digit.
///
/// Length of the password to generate.
/// Randomly generated password of the specified .
/// A value of less than 8 is specified for the .
public static string GeneratePassword(int length)
{
if (length < 8)
throw new ArgumentException("Value must be at least 8", nameof(length));
return PasswordGenerator.Default.GeneratePassword(length);
}
///
/// Sends email notification message to the specified using settings specified in the config file.
///
/// Email address of the notification recipient.
/// Subject of the notification.
/// Content of the notification.
public static void SendNotification(string recipient, string subject, string body)
{
Mail.Send(s_notificationSenderEmail, recipient, subject, body, false, s_notificationSmtpServer);
}
private static Func CreateSecurityProviderFactory(string settingsCategory)
{
ConfigurationFile config = ConfigurationFile.Current;
CategorizedSettingsElementCollection settings = config.Settings[settingsCategory];
settings.Add("ProviderType", DefaultProviderType, "The type to be used for enforcing security.");
string providerTypeSetting = settings["ProviderType"].ValueAsString(DefaultProviderType);
Type providerType = Type.GetType(providerTypeSetting);
if ((object)providerType == null)
throw new InvalidOperationException("The default security provider type defined by the system does not exist.");
ConstructorInfo constructor = providerType.GetConstructor(new[] { typeof(string) });
if ((object)constructor == null)
throw new InvalidOperationException("The default security provider type does not define a constructor with the appropriate signature.");
ParameterExpression parameterExpression = Expression.Parameter(typeof(string));
NewExpression newExpression = Expression.New(constructor, parameterExpression);
LambdaExpression lambdaExpression = Expression.Lambda(typeof(Func), newExpression, parameterExpression);
return (Func)lambdaExpression.Compile();
}
private static Func ProviderFactory(string settingsCategory)
{
if (settingsCategory == null)
settingsCategory = SettingsCategory;
lock (s_providerFactoryLock)
{
if (s_providerFactory is null)
s_providerFactory = new Dictionary>();
if (s_providerFactory.ContainsKey(settingsCategory))
return s_providerFactory[settingsCategory];
s_providerFactory.Add(settingsCategory, CreateSecurityProviderFactory(settingsCategory));
return s_providerFactory[settingsCategory];
}
}
#endregion
}
}