1279 lines · 64.2 KB · C#
RawDownload
//******************************************************************************************************//  AdoSecurityProvider.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://  ----------------------------------------------------------------------------------------------------//  11/24/2010 - Mehulbhai P Thakkar//       Generated original version of source code.//  07/18/2011 - Stephen C. Wills//       Modified a SELECT statement in RefreshData to explicitly alias columns to//       fix an error when using SQLite databases.//  09/01/2011 - Stephen C. Wills//       Modified references to views in the database whose names have changed.//  09/28/2011 - Stephen C. Wills//       Modified database queries to work with Oracle database.//  12/20/2012 - Starlynn Danyelle Gilliam//       Modified Header.//  01/16/2014 - J. Ritchie Carroll//       Added security context caching so security system can load without database access//       using last cached security state.////****************************************************************************************************** using System;using System.Collections.Generic;using System.Configuration;using System.Data;using System.Diagnostics;using System.Linq;using System.Security;using System.Text.RegularExpressions;using System.Threading;using GSF.Collections;using GSF.Configuration;using GSF.Data;using GSF.Diagnostics;using GSF.Identity; #pragma warning disable S2068 namespace GSF.Security{    /// <summary>    /// Represents an <see cref="ISecurityProvider"/> that uses ADO.NET data source (SQL Server, MySQL, Oracle, etc.) for its    /// backend data store and authenticates internal users against Active Directory and external users against the database.    /// </summary>    /// <example>    /// Required config file entries (automatically added):    /// <code>    /// <![CDATA[    /// <?xml version="1.0"?>    /// <configuration>    ///   <configSections>    ///     <section name="categorizedSettings" type="GSF.Configuration.CategorizedSettingsSection, GSF.Core" />    ///   </configSections>    ///   <categorizedSettings>    ///     <securityProvider>    ///       <add name="ProviderType" value="GSF.Security.AdoSecurityProvider, GSF.Security" description="The type to be used for enforcing security."    ///         encrypted="false" />    ///       <add name="UserCacheTimeout" value="5" description="Defines the timeout, in whole minutes, for a user's provider cache. Any value less than 1 will cause cache reset every minute."    ///         encrypted="false" />    ///       <add name="ConnectionString" value="Eval(systemSettings.ConnectionString)" description="Configuration database connection string"    ///         encrypted="false"/>    ///       <add name="DataProviderString" value="Eval(systemSettings.DataProviderString)" description="Configuration database ADO.NET data provider assembly type creation string"    ///         encrypted="false"/>        ///       <add name="LdapPath" value="" description="Specifies the LDAP path used to initialize the security provider."    ///         encrypted="false" />    ///       <add name="ApplicationName" value="SEC_APP" description="Name of the application being secured."    ///         encrypted="false" />    ///       <add name="IncludedResources" value="*=*" description="Semicolon delimited list of resources to be secured along with role names."    ///         encrypted="false" />    ///       <add name="ExcludedResources" value="" description="Semicolon delimited list of resources to be excluded from being secured."    ///         encrypted="false" />        ///       <add name="NotificationSmtpServer" value="localhost" description="SMTP server to be used for sending out email notification messages."    ///         encrypted="false" />    ///       <add name="NotificationSenderEmail" value="sender@company.com" description="Email address of the sender of email notification messages."     ///         encrypted="false" />    ///       <add name="CacheRetryDelayInterval" value="200" description="Wait interval, in milliseconds, before retrying load of user data cache."    ///         encrypted="false"/>    ///       <add name="CacheMaximumRetryAttempts" value="10" description="Maximum retry attempts allowed for loading user data cache."    ///         encrypted="false"/>    ///       <add name="EnableOfflineCaching" value="True" description="True to enable caching of user information for authentication in offline state, otherwise False."    ///         encrypted="false"/>    ///       <add name="PasswordRequirementsRegex" value="^.*(?=.{8,})(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).*$" description="Regular expression used to validate new passwords for database users."    ///         encrypted="false" />    ///       <add name="PasswordRequirementsError" value="Invalid Password: Password must be at least 8 characters; must contain at least 1 number, 1 upper case letter, and 1 lower case letter" description="Error message to be displayed when new database user password fails regular expression test."    ///         encrypted="false" />    ///       <add name="DefaultRoles" value="Viewer" description="If set this is a list of Roles assigned to a user that has no defined Roles."    ///         encrypted="false" />    ///     </securityProvider>    ///     <activeDirectory>    ///       <add name="PrivilegedDomain" value="" description="Domain of privileged domain user account."    ///         encrypted="false" />    ///       <add name="PrivilegedUserName" value="" description="Username of privileged domain user account."    ///         encrypted="false" />    ///       <add name="PrivilegedPassword" value="" description="Password of privileged domain user account."    ///         encrypted="true" />    ///     </activeDirectory>    ///   </categorizedSettings>    /// </configuration>    /// ]]>    /// </code>    /// </example>    /// <remarks>    /// Minimum expected table schema for ADO Security Provider:    /// <code>    /// <![CDATA[    /// CREATE TABLE UserAccount    /// (    ///     ID UNIQUEINDENTIFIER NOT NULL DEFAULT NEWID(),    ///     Name VARCHAR(200) NOT NULL,    ///     Password VARCHAR(200) DEFAULT NULL,    ///     FirstName VARCHAR(200) DEFAULT NULL,    ///     LastName VARCHAR(200) DEFAULT NULL,    ///     Phone VARCHAR(200) DEFAULT NULL,    ///     Email VARCHAR(200) DEFAULT NULL,    ///     LockedOut TINYINT NOT NULL DEFAULT 0,    ///     UseADAuthentication TINYINT NOT NULL DEFAULT 1,    ///     ChangePasswordOn DATETIME DEFAULT NULL,    ///     CONSTRAINT PK_UserAccount PRIMARY KEY (ID ASC),    ///     CONSTRAINT IX_UserAccount UNIQUE KEY (Name)    /// );    ///     /// CREATE TABLE SecurityGroup    /// (    ///     ID UNIQUEINDENTIFIER NOT NULL DEFAULT NEWID(),    ///     Name VARCHAR(200) NOT NULL,    ///     CONSTRAINT PK_SecurityGroup PRIMARY KEY (ID ASC),    ///     CONSTRAINT IX_SecurityGroup UNIQUE KEY (Name)    /// );    ///     /// CREATE TABLE SecurityGroupUserAccount    /// (    ///     SecurityGroupID UNIQUEINDENTIFIER NOT NULL,    ///     UserAccountID UNIQUEINDENTIFIER NOT NULL    /// );    ///     /// CREATE TABLE ApplicationRole    /// (    ///     ID UNIQUEINDENTIFIER NOT NULL DEFAULT NEWID(),    ///     Name VARCHAR(200) NOT NULL,    ///     NodeID UNIQUEINDENTIFIER NOT NULL,    ///     CONSTRAINT PK_ApplicationRole PRIMARY KEY (ID ASC),    ///     CONSTRAINT IX_ApplicationRole UNIQUE KEY (NodeID, Name)    /// );    ///     /// CREATE TABLE ApplicationRoleUserAccount    /// (    ///     ApplicationRoleID UNIQUEINDENTIFIER NOT NULL,    ///     UserAccountID UNIQUEINDENTIFIER NOT NULL      /// );    ///     /// CREATE TABLE ApplicationRoleSecurityGroup    /// (    ///     ApplicationRoleID UNIQUEINDENTIFIER NOT NULL,    ///     SecurityGroupID UNIQUEINDENTIFIER NOT NULL      /// );    /// ]]>    /// </code>    /// </remarks>    public class AdoSecurityProvider : LdapSecurityProvider    {        #region [ Members ]         // Constants        private const string UserAccountTable = "UserAccount";                                      // Table name for user accounts        private const string SecurityGroupTable = "SecurityGroup";                                  // Table name for security groups        private const string SecurityGroupUserAccountTable = "SecurityGroupUserAccount";            // Table name for security group user accounts        private const string ApplicationRoleTable = "ApplicationRole";                              // Table name for application roles        private const string ApplicationRoleUserAccountTable = "ApplicationRoleUserAccount";        // Table name for application role assignments for user accounts        private const string ApplicationRoleSecurityGroupTable = "ApplicationRoleSecurityGroup";    // Table name for application role assignments for security groups         /// <summary>        /// Default Roles to be used if no Roles are supplied for a user.        /// </summary>        private const string DefaultDefaultRoles = "";         /// <summary>        /// Default regular expression used to validate new database user passwords.        /// </summary>        public const string DefaultPasswordRequirementsRegex = "^.*(?=.{8,})(?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).*$";         /// <summary>        /// Default error message displayed when databases users fail regular expression test.        /// </summary>        public const string DefaultPasswordRequirementsError = "Invalid Password: Password must be at least 8 characters; must contain at least 1 number, 1 upper case letter, and 1 lower case letter";         /// <summary>        /// Default value for <see cref="UseDatabaseLogging"/>.        /// </summary>        public const bool DefaultUseDatabaseLogging = true;         /// <summary>        /// Defines the provider ID for the <see cref="AdoSecurityProvider"/>.        /// </summary>        public new const int ProviderID = 1;         private bool m_successfulPassThroughAuthentication;        private string m_passwordRequirementsRegex;        private string m_passwordRequirementsError;        private bool? m_lastLoggedLoginResult;         #endregion         #region [ Constructor ]         /// <summary>        /// Initializes a new instance of the <see cref="AdoSecurityProvider"/> class.        /// </summary>        /// <param name="username">Name that uniquely identifies the user.</param>        public AdoSecurityProvider(string username)            : this(username, true, false, true)        {        }         /// <summary>        /// Initializes a new instance of the <see cref="AdoSecurityProvider"/> class.        /// </summary>        /// <param name="username">Name that uniquely identifies the user.</param>        /// <param name="canRefreshData">true if the security provider can refresh <see cref="UserData"/> from the backend data store, otherwise false.</param>        /// <param name="canResetPassword">true if the security provider can reset user password, otherwise false.</param>        /// <param name="canChangePassword">true if the security provider can change user password, otherwise false.</param>        protected AdoSecurityProvider(string username, bool canRefreshData, bool canResetPassword, bool canChangePassword)            : base(username, canRefreshData, canResetPassword, canChangePassword)        {            base.ConnectionString = "Eval(systemSettings.ConnectionString)";             m_passwordRequirementsRegex = DefaultPasswordRequirementsRegex;            m_passwordRequirementsError = DefaultPasswordRequirementsError;            UseDatabaseLogging = DefaultUseDatabaseLogging;        }         #endregion         #region [ Properties ]         /// <summary>        /// Gets last exception reported by the <see cref="AdoSecurityProvider"/>.        /// </summary>        public Exception LastException        {            get;            set;        }         /// <summary>        /// Gets or sets flag that determines if <see cref="LogAuthenticationAttempt"/> and <see cref="LogError"/> should        /// write to the database. Defaults to <c>true</c>.        /// </summary>        /// <remarks>        /// Setting this flag to <c>false</c> may be necessary in cases where a database has been setup to use authentication        /// but does not include an "AccessLog" or "ErrorLog" table.        /// </remarks>        public bool UseDatabaseLogging        {            get;            set;        }          /// <summary>        /// Gets or sets the Default Roles used when a user does not have a role defined.        /// The user still needs to exist but they won't require a Role and will be assigned the DefaultRoles.        /// It is a comma separate list for multiple Roles. If an empty String is supplied a Role is required for the user.        /// </summary>        public string DefaultRoles        {            get;            set;        }         #endregion         #region [ Methods ]         /// <summary>        /// Loads saved security provider settings from the config file if the <see cref="SecurityProviderBase.PersistSettings"/> property is set to true.        /// </summary>        /// <exception cref="ConfigurationErrorsException"><see cref="SecurityProviderBase.SettingsCategory"/> has a value of null or empty string.</exception>        public override void LoadSettings()        {            base.LoadSettings();             // Make sure default settings exist            ConfigurationFile config = ConfigurationFile.Current;            CategorizedSettingsElementCollection settings = config.Settings[SettingsCategory];             settings.Add("DataProviderString", "Eval(systemSettings.DataProviderString)", "Configuration database ADO.NET data provider assembly type creation string to be used for connection to the backend security data store.");            settings.Add("LdapPath", "", "Specifies the LDAP path used to initialize the security provider.");            settings.Add("PasswordRequirementsRegex", DefaultPasswordRequirementsRegex, "Regular expression used to validate new passwords for database users.");            settings.Add("PasswordRequirementsError", DefaultPasswordRequirementsError, "Error message to be displayed when new database user password fails regular expression test.");            settings.Add("UseDatabaseLogging", DefaultUseDatabaseLogging, "Flag that determines if provider should write logs to the database.");            settings.Add("DefaultRoles", DefaultDefaultRoles, "If set this is a list of Roles assigned to a user that has no defined Roles.");             DefaultRoles = settings["DefaultRoles"].ValueAs(DefaultRoles);            m_passwordRequirementsRegex = settings["PasswordRequirementsRegex"].ValueAs(m_passwordRequirementsRegex);            m_passwordRequirementsError = settings["PasswordRequirementsError"].ValueAs(m_passwordRequirementsError);        }         /// <summary>        /// Refreshes the <see cref="UserData"/>.        /// </summary>        /// <returns>true if <see cref="SecurityProviderBase.UserData"/> is refreshed, otherwise false.</returns>        public override bool RefreshData()        {            if (string.IsNullOrEmpty(UserData.Username))                return false;             try            {                UserData userData = new UserData(UserData.Username);                 // Initialize user data.                userData.Initialize();                 // We'll retrieve all the data we need about a user.                //   Table1: Information about the user.                //   Table2: Groups the user is a member of.                //   Table3: Roles that are assigned to the user either implicitly (NT groups) or explicitly (database) or through a group.                 DataSet securityContext;                 try                {                    // Attempt to extract current security context from the database                    using (AdoDataConnection database = new AdoDataConnection(SettingsCategory))                    {                        securityContext = ExtractSecurityContext(database.Connection, ex => LogError(ex.Source, ex.ToString()), UserData.Username);                    }                }                catch (InvalidOperationException)                {                    // Failed to open ADO connection, attempt to fall back on using security context from last known cached information                    using (AdoSecurityCache cache = AdoSecurityCache.GetCurrentCache())                        securityContext = cache.DataSet;                }                 try                {                    // Make sure primary keys are defined on tables that we use "Find" function on later...                    UpdatePrimaryKey(securityContext.Tables[SecurityGroupTable], "SecurityGroupID");                    UpdatePrimaryKey(securityContext.Tables[ApplicationRoleTable], "ApplicationRoleID");                }                catch (Exception ex)                {                    // Just going to fall back on slower "Select" function if this fails...                    Logger.SwallowException(ex, "Failed to update primary key from cached security context, falling back on slower select function.", additionalFlags: MessageFlags.PerformanceIssue);                }                 // Validate that needed security tables exist in the data set                if ((object)securityContext == null)                    throw new SecurityException("Failed to load a valid security context. Cannot proceed with user authentication.");                 foreach (string securityTable in s_securityTables)                {                    if (!securityContext.Tables.Contains(securityTable))                        throw new SecurityException($"Failed to load a valid security context - missing table '{securityTable}'. Cannot proceed with user authentication.");                }                 if (securityContext.Tables[ApplicationRoleTable].Rows.Count == 0 && string.IsNullOrEmpty(DefaultRoles))                    throw new SecurityException($"Failed to load a valid security context - no application roles were found for node ID '{DefaultNodeID}', verify the node ID in the config file '{ConfigurationFile.Current.Configuration.FilePath}'. Cannot proceed with user authentication.");                 DataRow userAccount = null;                Guid userAccountID = Guid.Empty;                string userSID = UserInfo.UserNameToSID(userData.Username);                 // Filter user account data for the current user.                DataRow[] userAccounts = securityContext.Tables[UserAccountTable].Select($"Name = '{EncodeEscapeSequences(userSID)}'");                 // If SID based lookup failed, try lookup by user name.  Note that is critical that SID based lookup                // take precedence over name based lookup for proper cross-platform authentication.                if (userAccounts.Length == 0)                    userAccounts = securityContext.Tables[UserAccountTable].Select($"Name = '{EncodeEscapeSequences(userData.Username)}'");                 if (userAccounts.Length == 0)                {                    // User doesn't exist in the database, however, user may exist in an NT authentication group which                    // may have an explicit role assignment. To test for this case we make the assumption that this is                    // a Windows authenticated user and test for rights within groups                    userData.IsDefined = true;                    userData.IsExternal = false;                }                else                {                    userAccount = userAccounts[0];                    userData.IsDefined = true;                    userData.IsExternal = !Convert.ToBoolean(userAccount["UseADAuthentication"]);                    userAccountID = Guid.Parse(Convert.ToString(userAccount["ID"]));                }                 if (userData.IsExternal && (object)userAccount != null)                {                    // Load database user details                    if (string.IsNullOrEmpty(userData.LoginID))                        userData.LoginID = userData.Username;                     if (!Convert.IsDBNull(userAccount["Password"]))                        userData.Password = Convert.ToString(userAccount["Password"]);                     if (!Convert.IsDBNull(userAccount["FirstName"]))                        userData.FirstName = Convert.ToString(userAccount["FirstName"]);                     if (!Convert.IsDBNull(userAccount["LastName"]))                        userData.LastName = Convert.ToString(userAccount["LastName"]);                     if (!Convert.IsDBNull(userAccount["Phone"]))                        userData.PhoneNumber = Convert.ToString(userAccount["Phone"]);                     if (!Convert.IsDBNull(userAccount["Email"]))                        userData.EmailAddress = Convert.ToString(userAccount["Email"]);                     if (!Convert.IsDBNull(userAccount["ChangePasswordOn"]))                        userData.PasswordChangeDateTime = Convert.ToDateTime(userAccount["ChangePasswordOn"]);                     if (!Convert.IsDBNull(userAccount["CreatedOn"]))                        userData.AccountCreatedDateTime = Convert.ToDateTime(userAccount["CreatedOn"]);                     // For possible future use:                    //if (!Convert.IsDBNull(userDataRow["UserCompanyName"]))                    //    userData.CompanyName = Convert.ToString(userDataRow["UserCompanyName"]);                    //if (!Convert.IsDBNull(userDataRow["UserSecurityQuestion"]))                    //    userData.SecurityQuestion = Convert.ToString(userDataRow["UserSecurityQuestion"]);                    //if (!Convert.IsDBNull(userDataRow["UserSecurityAnswer"]))                    //    userData.SecurityAnswer = Convert.ToString(userDataRow["UserSecurityAnswer"]);                }                else                {                    // Load implicitly assigned groups - this happens via user's NT/AD groups that get loaded into the                    // user data group collection. When database group definitions are defined with the same name as                    // their NT/AD equivalents, this will allow automatic external group management.                    base.RefreshData(userData, userData.Groups, ProviderID);                }                 // Administrator can lock out NT/AD user as well as database-only user via database                if (!userData.IsLockedOut && (object)userAccount != null && !Convert.IsDBNull(userAccount["LockedOut"]))                    userData.IsLockedOut = Convert.ToBoolean(userAccount["LockedOut"]);                 // At this point an NT/AD based user will have a list of groups that is known to be available to the user. A database                // user will have no groups defined yet. The next step will be to load any explicitly assigned groups the user is a                // member of. Users can be explicitly assigned to any group, database or NT/AD group. This allows flexibility for                // local database users to be assigned to an NT/AD group (in the database) as well as NT/AD users to be a member of                // local database groups - allowing customizable role assignments to be fully managed via groups.                 // Note: some groups, such as "Everyone" (S-1-1-0), will not be automatically returned as valid groups by the base                // LdapSecurityProvider (see UserInfo class for details). If the security provider needs to support such groups,                // these groups will need to be manually added to the groups list here before testing to see if the user is a                // member of these groups. Since these types of groups could be considered a security risk by some applications,                // it is recommended that if this type of functionality is enabled, that it can be disabled in the configuration.                 if (userAccountID != Guid.Empty)                {                    // Filter explicitly assigned security groups for current user                    DataRow[] userGroups = securityContext.Tables[SecurityGroupUserAccountTable].Select($"UserAccountID = '{EncodeEscapeSequences(userAccountID.ToString())}'");                     foreach (DataRow row in userGroups)                    {                        // Locate associated security group record                        DataRow securityGroup = null;                         if (securityContext.Tables[SecurityGroupTable].PrimaryKey.Length > 0)                        {                            securityGroup = securityContext.Tables[SecurityGroupTable].Rows.Find(row["SecurityGroupID"]);                        }                        else                        {                            DataRow[] securityGroups = securityContext.Tables[SecurityGroupTable].Select($"ID = '{EncodeEscapeSequences(row["SecurityGroupID"].ToString())}'");                             if (securityGroups.Length > 0)                                securityGroup = securityGroups[0];                        }                         if ((object)securityGroup == null || Convert.IsDBNull(securityGroup["Name"]))                            continue;                         // Just in case a database user was manually assigned to an NT/AD group, make sure to convert                        // the group name back to it's human readable name (from a SID). The UserInfo.SIDToAccountName                        // function will return the original parameter value if name cannot be converted - this allows                        // the function to work for database group names as well.                        string groupName = UserInfo.SIDToAccountName(Convert.ToString(securityGroup["Name"]));                         if (!userData.Groups.Contains(groupName, StringComparer.OrdinalIgnoreCase))                            userData.Groups.Add(groupName);                    }                }                 // Explicitly assigned user roles will take precedence over any roles that may be implicitly available to the                // user based on what group they may be a member of. In practice this means even though a user may be a member                // of a group with role assignments, if there are any roles directly assigned to the user these will be their

Lines 1–500 of 1279.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t