From a99287e86555f6700cac2ae3478db62819449abb Mon Sep 17 00:00:00 2001 From: Shreyas Zare Date: Sun, 18 Sep 2022 17:45:38 +0530 Subject: [PATCH] Added AuthManager implementation --- DnsServerCore/Auth/AuthManager.cs | 837 ++++++++++++++++++++++++++++++ 1 file changed, 837 insertions(+) create mode 100644 DnsServerCore/Auth/AuthManager.cs diff --git a/DnsServerCore/Auth/AuthManager.cs b/DnsServerCore/Auth/AuthManager.cs new file mode 100644 index 00000000..9caa9120 --- /dev/null +++ b/DnsServerCore/Auth/AuthManager.cs @@ -0,0 +1,837 @@ +/* +Technitium DNS Server +Copyright (C) 2022 Shreyas Zare (shreyas@technitium.com) + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace DnsServerCore.Auth +{ + sealed class AuthManager : IDisposable + { + #region variables + + readonly ConcurrentDictionary _groups = new ConcurrentDictionary(1, 4); + readonly ConcurrentDictionary _users = new ConcurrentDictionary(1, 4); + + readonly ConcurrentDictionary _permissions = new ConcurrentDictionary(1, 11); + + readonly ConcurrentDictionary _sessions = new ConcurrentDictionary(1, 10); + + readonly ConcurrentDictionary _failedLoginAttempts = new ConcurrentDictionary(1, 10); + const int MAX_LOGIN_ATTEMPTS = 5; + + readonly ConcurrentDictionary _blockedAddresses = new ConcurrentDictionary(1, 10); + const int BLOCK_ADDRESS_INTERVAL = 5 * 60 * 1000; + + readonly string _configFolder; + readonly LogManager _log; + + readonly object _lockObj = new object(); + bool _pendingSave; + readonly Timer _saveTimer; + const int SAVE_TIMER_INITIAL_INTERVAL = 10000; + + #endregion + + #region constructor + + public AuthManager(string configFolder, LogManager log) + { + _configFolder = configFolder; + _log = log; + + _saveTimer = new Timer(SaveTimerCallback, null, Timeout.Infinite, Timeout.Infinite); + } + + #endregion + + #region IDisposable + + bool _disposed; + + public void Dispose() + { + if (_disposed) + return; + + if (_saveTimer is not null) + _saveTimer.Dispose(); + + lock (_lockObj) + { + SaveConfigFileInternal(); + } + + _disposed = true; + } + + #endregion + + #region private + + private void SaveTimerCallback(object state) + { + try + { + lock (_lockObj) + { + _pendingSave = false; + SaveConfigFileInternal(); + } + } + catch (Exception ex) + { + _log.Write(ex); + } + } + + private void CreateDefaultConfig() + { + Group adminGroup = CreateGroup(Group.ADMINISTRATORS, "Super administrators"); + Group dnsAdminGroup = CreateGroup(Group.DNS_ADMINISTRATORS, "DNS service administrators"); + Group dhcpAdminGroup = CreateGroup(Group.DHCP_ADMINISTRATORS, "DHCP service administrators"); + Group everyoneGroup = CreateGroup(Group.EVERYONE, "All users"); + + SetPermission(PermissionSection.Dashboard, adminGroup, PermissionFlag.ViewModifyDelete); + SetPermission(PermissionSection.Zones, adminGroup, PermissionFlag.ViewModifyDelete); + SetPermission(PermissionSection.Cache, adminGroup, PermissionFlag.ViewModifyDelete); + SetPermission(PermissionSection.Allowed, adminGroup, PermissionFlag.ViewModifyDelete); + SetPermission(PermissionSection.Blocked, adminGroup, PermissionFlag.ViewModifyDelete); + SetPermission(PermissionSection.Apps, adminGroup, PermissionFlag.ViewModifyDelete); + SetPermission(PermissionSection.DnsClient, adminGroup, PermissionFlag.ViewModifyDelete); + SetPermission(PermissionSection.Settings, adminGroup, PermissionFlag.ViewModifyDelete); + SetPermission(PermissionSection.DhcpServer, adminGroup, PermissionFlag.ViewModifyDelete); + SetPermission(PermissionSection.Administration, adminGroup, PermissionFlag.ViewModifyDelete); + SetPermission(PermissionSection.Logs, adminGroup, PermissionFlag.ViewModifyDelete); + + SetPermission(PermissionSection.Zones, dnsAdminGroup, PermissionFlag.ViewModifyDelete); + SetPermission(PermissionSection.Cache, dnsAdminGroup, PermissionFlag.ViewModifyDelete); + SetPermission(PermissionSection.Allowed, dnsAdminGroup, PermissionFlag.ViewModifyDelete); + SetPermission(PermissionSection.Blocked, dnsAdminGroup, PermissionFlag.ViewModifyDelete); + SetPermission(PermissionSection.Apps, dnsAdminGroup, PermissionFlag.ViewModifyDelete); + SetPermission(PermissionSection.DnsClient, dnsAdminGroup, PermissionFlag.ViewModifyDelete); + SetPermission(PermissionSection.Settings, dnsAdminGroup, PermissionFlag.ViewModifyDelete); + SetPermission(PermissionSection.Logs, dnsAdminGroup, PermissionFlag.View); + + SetPermission(PermissionSection.Zones, dhcpAdminGroup, PermissionFlag.View); + SetPermission(PermissionSection.DnsClient, dhcpAdminGroup, PermissionFlag.View); + SetPermission(PermissionSection.DhcpServer, dhcpAdminGroup, PermissionFlag.ViewModifyDelete); + SetPermission(PermissionSection.Logs, dhcpAdminGroup, PermissionFlag.View); + + SetPermission(PermissionSection.Dashboard, everyoneGroup, PermissionFlag.View); + SetPermission(PermissionSection.Zones, everyoneGroup, PermissionFlag.View); + SetPermission(PermissionSection.Cache, everyoneGroup, PermissionFlag.View); + SetPermission(PermissionSection.Allowed, everyoneGroup, PermissionFlag.View); + SetPermission(PermissionSection.Blocked, everyoneGroup, PermissionFlag.View); + SetPermission(PermissionSection.Apps, everyoneGroup, PermissionFlag.View); + SetPermission(PermissionSection.DnsClient, everyoneGroup, PermissionFlag.View); + SetPermission(PermissionSection.DhcpServer, everyoneGroup, PermissionFlag.View); + SetPermission(PermissionSection.Logs, everyoneGroup, PermissionFlag.View); + + string adminPassword = Environment.GetEnvironmentVariable("DNS_SERVER_ADMIN_PASSWORD"); + string adminPasswordFile = Environment.GetEnvironmentVariable("DNS_SERVER_ADMIN_PASSWORD_FILE"); + + User adminUser; + + if (!string.IsNullOrEmpty(adminPassword)) + { + adminUser = CreateUser("Administrator", "admin", adminPassword); + } + else if (!string.IsNullOrEmpty(adminPasswordFile)) + { + try + { + using (StreamReader sR = new StreamReader(adminPasswordFile, true)) + { + string password = sR.ReadLine(); + adminUser = CreateUser("Administrator", "admin", password); + } + } + catch (Exception ex) + { + _log.Write(ex); + + adminUser = CreateUser("Administrator", "admin", "admin"); + } + } + else + { + adminUser = CreateUser("Administrator", "admin", "admin"); + } + + adminUser.AddToGroup(adminGroup); + } + + private void LoadConfigFileInternal(UserSession implantSession) + { + string configFile = Path.Combine(_configFolder, "auth.config"); + + try + { + bool passwordResetOption = false; + + if (!File.Exists(configFile)) + { + string passwordResetConfigFile = Path.Combine(_configFolder, "resetadmin.config"); + + if (File.Exists(passwordResetConfigFile)) + { + passwordResetOption = true; + configFile = passwordResetConfigFile; + } + } + + using (FileStream fS = new FileStream(configFile, FileMode.Open, FileAccess.Read)) + { + ReadConfigFrom(new BinaryReader(fS)); + } + + if (implantSession is not null) + { + UserSession newSession; + + using (MemoryStream mS = new MemoryStream()) + { + implantSession.WriteTo(new BinaryWriter(mS)); + + mS.Position = 0; + newSession = new UserSession(new BinaryReader(mS), this); + } + + _sessions.TryAdd(newSession.Token, newSession); + } + + _log.Write("DNS Server auth config file was loaded: " + configFile); + + if (passwordResetOption) + { + User adminUser = GetUser("admin"); + if (adminUser is null) + { + adminUser = CreateUser("Administrator", "admin", "admin"); + } + else + { + adminUser.ChangePassword("admin"); + adminUser.Disabled = false; + } + + adminUser.AddToGroup(GetGroup(Group.ADMINISTRATORS)); + + _log.Write("DNS Server reset password for user: admin"); + SaveConfigFileInternal(); + + try + { + File.Delete(configFile); + } + catch + { } + } + } + catch (FileNotFoundException) + { + _log.Write("DNS Server auth config file was not found: " + configFile); + _log.Write("DNS Server is restoring default auth config file."); + + CreateDefaultConfig(); + + SaveConfigFileInternal(); + } + catch (Exception ex) + { + _log.Write("DNS Server encountered an error while loading auth config file: " + configFile + "\r\n" + ex.ToString()); + _log.Write("Note: You may try deleting the auth config file to fix this issue. However, you will lose auth settings but, rest of the DNS settings and zone data wont be affected."); + throw; + } + } + + private void SaveConfigFileInternal() + { + string configFile = Path.Combine(_configFolder, "auth.config"); + + using (MemoryStream mS = new MemoryStream()) + { + //serialize config + WriteConfigTo(new BinaryWriter(mS)); + + //write config + mS.Position = 0; + + using (FileStream fS = new FileStream(configFile, FileMode.Create, FileAccess.Write)) + { + mS.CopyTo(fS); + } + } + + _log.Write("DNS Server auth config file was saved: " + configFile); + } + + private void ReadConfigFrom(BinaryReader bR) + { + if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "AS") //format + throw new InvalidDataException("DNS Server auth config file format is invalid."); + + int version = bR.ReadByte(); + switch (version) + { + case 1: + { + int count = bR.ReadByte(); + for (int i = 0; i < count; i++) + { + Group group = new Group(bR); + _groups.TryAdd(group.Name.ToLower(), group); + } + } + + { + int count = bR.ReadByte(); + for (int i = 0; i < count; i++) + { + User user = new User(bR, this); + _users.TryAdd(user.Username, user); + } + } + + { + int count = bR.ReadInt32(); + for (int i = 0; i < count; i++) + { + Permission permission = new Permission(bR, this); + _permissions.TryAdd(permission.Section, permission); + } + } + + { + int count = bR.ReadInt32(); + for (int i = 0; i < count; i++) + { + UserSession session = new UserSession(bR, this); + if (!session.HasExpired()) + _sessions.TryAdd(session.Token, session); + } + } + break; + + default: + throw new InvalidDataException("DNS Server auth config version not supported."); + } + } + + private void WriteConfigTo(BinaryWriter bW) + { + bW.Write(Encoding.ASCII.GetBytes("AS")); //format + bW.Write((byte)1); //version + + bW.Write(Convert.ToByte(_groups.Count)); + + foreach (KeyValuePair group in _groups) + group.Value.WriteTo(bW); + + bW.Write(Convert.ToByte(_users.Count)); + + foreach (KeyValuePair user in _users) + user.Value.WriteTo(bW); + + bW.Write(_permissions.Count); + + foreach (KeyValuePair permission in _permissions) + permission.Value.WriteTo(bW); + + List activeSessions = new List(_sessions.Count); + + foreach (KeyValuePair session in _sessions) + { + if (session.Value.HasExpired()) + _sessions.TryRemove(session.Key, out _); + else + activeSessions.Add(session.Value); + } + + bW.Write(activeSessions.Count); + + foreach (UserSession session in activeSessions) + session.WriteTo(bW); + } + + private void FailedLoginAttempt(IPAddress address) + { + _failedLoginAttempts.AddOrUpdate(address, 1, delegate (IPAddress key, int attempts) + { + return attempts + 1; + }); + } + + private bool LoginAttemptsExceedLimit(IPAddress address, int limit) + { + if (!_failedLoginAttempts.TryGetValue(address, out int attempts)) + return false; + + return attempts >= limit; + } + + private void ResetFailedLoginAttempt(IPAddress address) + { + _failedLoginAttempts.TryRemove(address, out _); + } + + private void BlockAddress(IPAddress address, int interval) + { + _blockedAddresses.TryAdd(address, DateTime.UtcNow.AddMilliseconds(interval)); + } + + private bool IsAddressBlocked(IPAddress address) + { + if (!_blockedAddresses.TryGetValue(address, out DateTime expiry)) + return false; + + if (expiry > DateTime.UtcNow) + { + return true; + } + else + { + UnblockAddress(address); + ResetFailedLoginAttempt(address); + + return false; + } + } + + private void UnblockAddress(IPAddress address) + { + _blockedAddresses.TryRemove(address, out _); + } + + #endregion + + #region public + + public User GetUser(string username) + { + if (_users.TryGetValue(username.ToLower(), out User user)) + return user; + + return null; + } + + public User CreateUser(string displayName, string username, string password, int iterations = User.DEFAULT_ITERATIONS) + { + username = username.ToLower(); + + User user = new User(displayName, username, password, iterations); + + if (_users.TryAdd(username, user)) + { + user.AddToGroup(GetGroup(Group.EVERYONE)); + return user; + } + + throw new DnsWebServiceException("User already exists: " + username); + } + + public void ChangeUsername(User user, string newUsername) + { + if (user.Username.Equals(newUsername, StringComparison.OrdinalIgnoreCase)) + return; + + string oldUsername = user.Username; + user.Username = newUsername; + + if (!_users.TryAdd(user.Username, user)) + { + user.Username = oldUsername; //revert + throw new DnsWebServiceException("User already exists: " + newUsername); + } + + _users.TryRemove(oldUsername, out _); + } + + public bool DeleteUser(string username) + { + if (_users.TryRemove(username.ToLower(), out User deletedUser)) + { + //delete all sessions + foreach (UserSession session in GetSessions(deletedUser)) + DeleteSession(session.Token); + + //delete all permissions + foreach (KeyValuePair permission in _permissions) + { + permission.Value.RemovePermission(deletedUser); + permission.Value.RemoveAllSubItemPermissions(deletedUser); + } + + return true; + } + + return false; + } + + public Group GetGroup(string name) + { + if (_groups.TryGetValue(name.ToLower(), out Group group)) + return group; + + return null; + } + + public List GetGroupMembers(Group group) + { + List members = new List(); + + foreach (KeyValuePair user in _users) + { + if (user.Value.IsMemberOfGroup(group)) + members.Add(user.Value); + } + + return members; + } + + public void SyncGroupMembers(Group group, IReadOnlyDictionary users) + { + //remove + foreach (KeyValuePair user in _users) + { + if (!users.ContainsKey(user.Key)) + user.Value.RemoveFromGroup(group); + } + + //set + foreach (KeyValuePair user in users) + user.Value.AddToGroup(group); + } + + public Group CreateGroup(string name, string description) + { + Group group = new Group(name, description); + + if (_groups.TryAdd(name.ToLower(), group)) + return group; + + throw new DnsWebServiceException("Group already exists: " + name); + } + + public void RenameGroup(Group group, string newGroupName) + { + if (group.Name.Equals(newGroupName, StringComparison.OrdinalIgnoreCase)) + { + group.Name = newGroupName; + return; + } + + string oldGroupName = group.Name; + group.Name = newGroupName; + + if (!_groups.TryAdd(group.Name.ToLower(), group)) + { + group.Name = oldGroupName; //revert + throw new DnsWebServiceException("Group already exists: " + newGroupName); + } + + _groups.TryRemove(oldGroupName.ToLower(), out _); + + //update users + foreach (KeyValuePair user in _users) + user.Value.RenameGroup(oldGroupName); + } + + public bool DeleteGroup(string name) + { + name = name.ToLower(); + + switch (name) + { + case "everyone": + case "administrators": + case "dns administrators": + case "dhcp administrators": + throw new InvalidOperationException("Access was denied."); + + default: + if (_groups.TryRemove(name, out Group deletedGroup)) + { + //remove all users from deleted group + foreach (KeyValuePair user in _users) + user.Value.RemoveFromGroup(deletedGroup); + + //delete all permissions + foreach (KeyValuePair permission in _permissions) + { + permission.Value.RemovePermission(deletedGroup); + permission.Value.RemoveAllSubItemPermissions(deletedGroup); + } + + return true; + } + + return false; + } + } + + public UserSession GetSession(string token) + { + if (_sessions.TryGetValue(token, out UserSession session)) + return session; + + return null; + } + + public List GetSessions(User user) + { + List userSessions = new List(); + + foreach (KeyValuePair session in _sessions) + { + if (session.Value.User.Equals(user) && !session.Value.HasExpired()) + userSessions.Add(session.Value); + } + + return userSessions; + } + + public async Task CreateSessionAsync(UserSessionType type, string tokenName, string username, string password, IPAddress remoteAddress, string userAgent) + { + if (IsAddressBlocked(remoteAddress)) + throw new DnsWebServiceException("Max limit of " + MAX_LOGIN_ATTEMPTS + " attempts exceeded. Access blocked for " + (BLOCK_ADDRESS_INTERVAL / 1000) + " seconds."); + + User user = GetUser(username); + + if ((user is null) || !user.PasswordHash.Equals(user.GetPasswordHashFor(password), StringComparison.Ordinal)) + { + if (password != "admin") + { + FailedLoginAttempt(remoteAddress); + + if (LoginAttemptsExceedLimit(remoteAddress, MAX_LOGIN_ATTEMPTS)) + BlockAddress(remoteAddress, BLOCK_ADDRESS_INTERVAL); + + await Task.Delay(1000); + } + + throw new DnsWebServiceException("Invalid username or password for user: " + username); + } + + ResetFailedLoginAttempt(remoteAddress); + + if (user.Disabled) + throw new DnsWebServiceException("User account is disabled. Please contact your administrator."); + + UserSession session = new UserSession(type, tokenName, user, remoteAddress, userAgent); + + if (!_sessions.TryAdd(session.Token, session)) + throw new DnsWebServiceException("Error while creating session. Please try again."); + + user.LoggedInFrom(remoteAddress); + + return session; + } + + public UserSession CreateApiToken(string tokenName, string username, IPAddress remoteAddress, string userAgent) + { + if (IsAddressBlocked(remoteAddress)) + throw new DnsWebServiceException("Max limit of " + MAX_LOGIN_ATTEMPTS + " attempts exceeded. Access blocked for " + (BLOCK_ADDRESS_INTERVAL / 1000) + " seconds."); + + User user = GetUser(username); + if (user is null) + throw new DnsWebServiceException("No such user exists: " + username); + + if (user.Disabled) + throw new DnsWebServiceException("Account is suspended."); + + UserSession session = new UserSession(UserSessionType.ApiToken, tokenName, user, remoteAddress, userAgent); + + if (!_sessions.TryAdd(session.Token, session)) + throw new DnsWebServiceException("Error while creating session. Please try again."); + + user.LoggedInFrom(remoteAddress); + + return session; + } + + public UserSession DeleteSession(string token) + { + if (_sessions.TryRemove(token, out UserSession session)) + return session; + + return null; + } + + public Permission GetPermission(PermissionSection section) + { + if (_permissions.TryGetValue(section, out Permission permission)) + return permission; + + return null; + } + + public Permission GetPermission(PermissionSection section, string subItemName) + { + if (_permissions.TryGetValue(section, out Permission permission)) + return permission.GetSubItemPermission(subItemName); + + return null; + } + + public void SetPermission(PermissionSection section, User user, PermissionFlag flags) + { + Permission permission = _permissions.GetOrAdd(section, delegate (PermissionSection key) + { + return new Permission(key); + }); + + permission.SetPermission(user, flags); + } + + public void SetPermission(PermissionSection section, string subItemName, User user, PermissionFlag flags) + { + Permission permission = _permissions.GetOrAdd(section, delegate (PermissionSection key) + { + return new Permission(key); + }); + + permission.SetSubItemPermission(subItemName, user, flags); + } + + public void SetPermission(PermissionSection section, Group group, PermissionFlag flags) + { + Permission permission = _permissions.GetOrAdd(section, delegate (PermissionSection key) + { + return new Permission(key); + }); + + permission.SetPermission(group, flags); + } + + public void SetPermission(PermissionSection section, string subItemName, Group group, PermissionFlag flags) + { + Permission permission = _permissions.GetOrAdd(section, delegate (PermissionSection key) + { + return new Permission(key); + }); + + permission.SetSubItemPermission(subItemName, group, flags); + } + + public bool RemovePermission(PermissionSection section, User user) + { + return _permissions.TryGetValue(section, out Permission permission) && permission.RemovePermission(user); + } + + public bool RemovePermission(PermissionSection section, string subItemName, User user) + { + return _permissions.TryGetValue(section, out Permission permission) && permission.RemoveSubItemPermission(subItemName, user); + } + + public bool RemovePermission(PermissionSection section, Group group) + { + return _permissions.TryGetValue(section, out Permission permission) && permission.RemovePermission(group); + } + + public bool RemovePermission(PermissionSection section, string subItemName, Group group) + { + return _permissions.TryGetValue(section, out Permission permission) && permission.RemoveSubItemPermission(subItemName, group); + } + + public bool RemoveAllPermissions(PermissionSection section, string subItemName) + { + return _permissions.TryGetValue(section, out Permission permission) && permission.RemoveAllSubItemPermissions(subItemName); + } + + public bool IsPermitted(PermissionSection section, User user, PermissionFlag flag) + { + return _permissions.TryGetValue(section, out Permission permission) && permission.IsPermitted(user, flag); + } + + public bool IsPermitted(PermissionSection section, string subItemName, User user, PermissionFlag flag) + { + return _permissions.TryGetValue(section, out Permission permission) && permission.IsSubItemPermitted(subItemName, user, flag); + } + + public void LoadOldConfig(string password, bool isPasswordHash) + { + User user = GetUser("admin"); + if (user is null) + user = CreateUser("Administrator", "admin", "admin"); + + user.AddToGroup(GetGroup(Group.ADMINISTRATORS)); + + if (isPasswordHash) + user.LoadOldSchemeCredentials(password); + else + user.ChangePassword(password); + + lock (_lockObj) + { + SaveConfigFileInternal(); + } + } + + public void LoadConfigFile(UserSession implantSession = null) + { + lock (_lockObj) + { + _groups.Clear(); + _users.Clear(); + _permissions.Clear(); + _sessions.Clear(); + + LoadConfigFileInternal(implantSession); + } + } + + public void SaveConfigFile() + { + lock (_lockObj) + { + if (_pendingSave) + return; + + _pendingSave = true; + _saveTimer.Change(SAVE_TIMER_INITIAL_INTERVAL, Timeout.Infinite); + } + } + + #endregion + + #region properties + + public ICollection Groups + { get { return _groups.Values; } } + + public ICollection Users + { get { return _users.Values; } } + + public ICollection Permissions + { get { return _permissions.Values; } } + + public ICollection Sessions + { get { return _sessions.Values; } } + + #endregion + } +}