diff --git a/App_LocalResources/Settings.ascx.resx b/App_LocalResources/Settings.ascx.resx index d33b2e9..3256a2e 100644 --- a/App_LocalResources/Settings.ascx.resx +++ b/App_LocalResources/Settings.ascx.resx @@ -150,4 +150,16 @@ Select after user & role filtering + + When this option is enabled, the user being impersonated will receive an email with the details of the user doing the impersonation. The request will have to be approved before the impersonation is effective. + + + Request authorization + + + Include the Administrator users in the dropdown list. Caution: doing so will give non Administrator users the option to log on as an administrator + + + Include Administrator users + \ No newline at end of file diff --git a/App_LocalResources/SharedResources.resx b/App_LocalResources/SharedResources.resx new file mode 100644 index 0000000..61b4016 --- /dev/null +++ b/App_LocalResources/SharedResources.resx @@ -0,0 +1,383 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="https://www.w3.org/1999/xhtml"> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> + <style type="text/css"> + /* Reset Styles */ + html, body{height:100%;width:100%;} + body{ + margin:0; padding:0; + font-family:sans-serif; + font-size:0.83em; + color:#333; + } + table td{border-collapse:collapse;} + p{margin: 0 0 1.6em 0;} + a{color:#417CD3;} + #backgroundTable{height:100% !important; margin:0; padding:0; width:100% !important;} + body{ background-color:#fafafa; } + + #templateContainer{ + border: 1px solid #eeeeee; + box-shadow: 0px 0px 3px 0px #eee; + } + + /* Pre Header Styles */ + .preheaderContent{ + padding:15px 0 5px 0; + font-size:0.9em; + color:#999; + } + .headerContent {background-color:#bbb;} + .headerContent a{display:block;} + .headerContent img{ margin-bottom:0;} + + /* Sub Header Styles */ + #templateSubHeader{ color:#ffffff; } + #templateSubHeader td{ padding:5px 20px; } + </style> +</head> + +<body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0"> +<table cellpadding="0" cellspacing="0" border="0" width="100%" height="100%" id="backgroundTable"> + <tr> + <td align="center" valign="top"> + <!-- // Begin Template Preheader \\ --> + <table border="0" cellpadding="0" cellspacing="0" id="templatePreheader" width="600"> + <tr align="right"> + <td valign="top" class="preheaderContent" style="border-top:5px solid #417CD3;"> + + <!-- // Begin Module: Standard Preheader \ --> + <table border="0" cellpadding="0" cellspacing="0" > + <tr> + <td align="right"> + + </td> + </tr> + </table> + <!-- // End Module: Standard Preheader \ --> + </td> + </tr> + </table> + <!-- // End Template Preheader \\ --> + <!-- // Begin Template body \\ --> + <table border="0" cellpadding="0" cellspacing="0" width="600" id="templateContainer" style="border:1px solid #eeeeee; " > + + <tr > + <td align="center" valign="top"> + <!-- // Begin Template Sub Header \\ --> + <table border="0" cellpadding="0" cellspacing="0" width="600" id="templateSubHeader"> + <tr> + <td class="subHeaderContent" bgcolor="#417CD3" width="66%" align="left"> + <h2 style="color:#fff; font-weight:100; font-size:24px;">Authorization Request</h2> + </td><!--close subHeaderContent--> + <td width="33%" bgcolor="#417CD3" align="right"> + <p style="color:#fff;"></p> + </td> + </tr> + </table> + <!-- // End Template Sub Header \\ --> + </td> + </tr> + <tr> + <td align="center" valign="top" bgcolor="#ffffff"> + <!-- // Begin Template Body \\ --> + <table border="0" cellpadding="0" cellspacing="0" width="600" id="templateBody"> + <tr> + <td valign="top" class="bodyContent"> + <!-- // Begin Module: Standard Content \\ --> + <table border="0" cellpadding="30" cellspacing="0" width="100%"> + <tr> + <td valign="top"> + Your request has been approved. + </td> + </tr> + </table> + <!-- // End Module: Standard Content \\ --> + </td> + </tr> + </table> + <!-- // End Template Body \\ --> + </td> + </tr> + + </table><!-- // End Template Body \\ --> + + </td> + </tr> +</table><!-- wrapper table --> +</body> +</html> + + + <p> + Dear [Custom:0], +</p> +<p> + This is a request from <strong>[Portal:PortalName]</strong> in order to impersonate your account on the site. +</p> +<p> + The details of the request are as follows:<br/> + <ul> + <li>User that requests the action: [Custom:1]</li> + <li>Email of the user that requests the action: [Custom:2]</li> + <li><strong>Account that will be impersonated: [Custom:3]</strong></li> + <li>Date requested: [Custom:4]</li> + </ul> +</p> + +<p> + If you want to allow the request to proceed, please click the link below:<br/> + <a href=" + +[Portal:FULLURL]/DesktopModules/IdentitySwitcher/RequestConfirmation.ashx?id=[Custom:5]">I want to approve the request</a> +<p/> +<p> + <strong>Note: </strong> Remember that by allowing this request to proceed the above user will be able to act on your behalf on the site. +</p> + +<p> + <strong>Note: If you don't think this is a valid request, please inform the site administrator.</strong> +</p> + +<p> + Sincerely,<br/> + [Portal:PortalName] +</p> + + + <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="https://www.w3.org/1999/xhtml"> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> + <style type="text/css"> + /* Reset Styles */ + html, body{height:100%;width:100%;} + body{ + margin:0; padding:0; + font-family:sans-serif; + font-size:0.83em; + color:#333; + } + table td{border-collapse:collapse;} + p{margin: 0 0 1.6em 0;} + a{color:#417CD3;} + #backgroundTable{height:100% !important; margin:0; padding:0; width:100% !important;} + body{ background-color:#fafafa; } + + #templateContainer{ + border: 1px solid #eeeeee; + box-shadow: 0px 0px 3px 0px #eee; + } + + /* Pre Header Styles */ + .preheaderContent{ + padding:15px 0 5px 0; + font-size:0.9em; + color:#999; + } + .headerContent {background-color:#bbb;} + .headerContent a{display:block;} + .headerContent img{ margin-bottom:0;} + + /* Sub Header Styles */ + #templateSubHeader{ color:#ffffff; } + #templateSubHeader td{ padding:5px 20px; } + </style> +</head> + +<body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0"> +<table cellpadding="0" cellspacing="0" border="0" width="100%" height="100%" id="backgroundTable"> + <tr> + <td align="center" valign="top"> + <!-- // Begin Template Preheader \\ --> + <table border="0" cellpadding="0" cellspacing="0" id="templatePreheader" width="600"> + <tr align="right"> + <td valign="top" class="preheaderContent" style="border-top:5px solid #417CD3;"> + + <!-- // Begin Module: Standard Preheader \ --> + <table border="0" cellpadding="0" cellspacing="0" > + <tr> + <td align="right"> + + </td> + </tr> + </table> + <!-- // End Module: Standard Preheader \ --> + </td> + </tr> + </table> + <!-- // End Template Preheader \\ --> + <!-- // Begin Template body \\ --> + <table border="0" cellpadding="0" cellspacing="0" width="600" id="templateContainer" style="border:1px solid #eeeeee; " > + + <tr > + <td align="center" valign="top"> + <!-- // Begin Template Sub Header \\ --> + <table border="0" cellpadding="0" cellspacing="0" width="600" id="templateSubHeader"> + <tr> + <td class="subHeaderContent" bgcolor="#417CD3" width="66%" align="left"> + <h2 style="color:#fff; font-weight:100; font-size:24px;">Authorization Request</h2> + </td><!--close subHeaderContent--> + <td width="33%" bgcolor="#417CD3" align="right"> + <p style="color:#fff;"></p> + </td> + </tr> + </table> + <!-- // End Template Sub Header \\ --> + </td> + </tr> + <tr> + <td align="center" valign="top" bgcolor="#ffffff"> + <!-- // Begin Template Body \\ --> + <table border="0" cellpadding="0" cellspacing="0" width="600" id="templateBody"> + <tr> + <td valign="top" class="bodyContent"> + <!-- // Begin Module: Standard Content \\ --> + <table border="0" cellpadding="30" cellspacing="0" width="100%"> + <tr> + <td valign="top"> + Invalid request. + </td> + </tr> + </table> + <!-- // End Module: Standard Content \\ --> + </td> + </tr> + </table> + <!-- // End Template Body \\ --> + </td> + </tr> + + </table><!-- // End Template Body \\ --> + + </td> + </tr> +</table><!-- wrapper table --> +</body> +</html> + + + [Portal:PortalName] Request for impersonation + + \ No newline at end of file diff --git a/App_LocalResources/ViewIdentitySwitcher.ascx.resx b/App_LocalResources/ViewIdentitySwitcher.ascx.resx index 3743743..7ebca7f 100644 --- a/App_LocalResources/ViewIdentitySwitcher.ascx.resx +++ b/App_LocalResources/ViewIdentitySwitcher.ascx.resx @@ -1,6 +1,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Anonymous User - + <p class="normalRed">This is a user switcher module. Only to be used in development environments! Please delete the module if the site goes live!</p> - + [None Selected] - + Filter: - + Switch to: - + Biography - + Mobile - + City - + Filter - + Switch identity - + Country - + Email - + Fax - + First name - + IM - + Last name - + Middle name - + Postal code - + Preferred locale - + Prefix - + Region - + Street - + Suffix - + Telephone - + Telephone - + Time zone - + Unit - + Username - + Website - + Filter: - + Switch to: + + + Search + + + Switch + + + Waiting for user to confirm request... \ No newline at end of file diff --git a/Components/ModuleInstanceBase.cs b/Components/ModuleInstanceBase.cs index 73dd85e..e39c559 100644 --- a/Components/ModuleInstanceBase.cs +++ b/Components/ModuleInstanceBase.cs @@ -79,6 +79,30 @@ public class ModuleInstanceBase /// public string SwitchToText { get; set; } + /// + /// Gets or sets the filter icon text. + /// + /// + /// The filter text. + /// + public string FilterIconText { get; set; } + + /// + /// Gets or sets the switch to icon text. + /// + /// + /// The switch to text. + /// + public string SwitchIconText { get; set; } + + /// + /// Message displayed while waiting for the user to confirm the request + /// + /// + /// The switch to text. + /// + public string WaitingForConfirmation { get; set; } + #endregion } } \ No newline at end of file diff --git a/Controllers/IdentitySwitcherController.cs b/Controllers/IdentitySwitcherController.cs index 451ed57..e1073ab 100644 --- a/Controllers/IdentitySwitcherController.cs +++ b/Controllers/IdentitySwitcherController.cs @@ -25,6 +25,7 @@ namespace DNN.Modules.IdentitySwitcher.Controllers { using System; + using System.Collections; using System.Collections.Generic; using System.Linq; using System.Web; @@ -33,11 +34,18 @@ namespace DNN.Modules.IdentitySwitcher.Controllers using DNN.Modules.IdentitySwitcher.ModuleSettings; using DotNetNuke.Common; using DotNetNuke.Common.Utilities; + using DotNetNuke.Data; + using DotNetNuke.Entities.Modules; + using DotNetNuke.Entities.Portals; using DotNetNuke.Entities.Profile; using DotNetNuke.Entities.Users; using DotNetNuke.Security; + using DotNetNuke.Security.Permissions; using DotNetNuke.Security.Roles; using DotNetNuke.Services.Exceptions; + using DotNetNuke.Services.Localization; + using DotNetNuke.Services.Mail; + using DotNetNuke.Services.Tokens; using DotNetNuke.Web.Api; /// @@ -50,12 +58,26 @@ public class IdentitySwitcherController : DnnApiController /// Gets the search items. /// /// + [SupportedModules("IdentitySwitcher")] [AllowAnonymous] [HttpGet] public IHttpActionResult GetSearchItems() { var result = default(IHttpActionResult); + if (!CheckSecurity(ActiveModule, UserInfo)) + { + Exceptions.LogException(new Exception(String.Format("IdentitySwitcher module access without proper security context. " + + "ModuleId: {0} - TabId: {1} - PortalId: {2} - UserId: {3} - IP: {4}", + ActiveModule.ModuleID, + ActiveModule.TabID, + ActiveModule.PortalID, + UserInfo.UserID, + HttpContext.Current.Request.UserHostAddress + ))); + return result; + } + // Obtain the properties of each user profile and return these for the user to search by. try { @@ -64,11 +86,13 @@ public IHttpActionResult GetSearchItems() var profileProperties = ProfileController.GetPropertyDefinitionsByPortal(PortalSettings.PortalId, false); + resultData.AddRange(new List { "RoleName", "Email", "Username", "FirstName", "LastName", "DisplayName" }); + foreach (ProfilePropertyDefinition definition in profileProperties) { - resultData.Add(definition.PropertyName); + if (!resultData.Contains(definition.PropertyName)) + resultData.Add(definition.PropertyName); } - resultData.AddRange(new List { "RoleName", "Email", "Username" }); result = Ok(resultData); } @@ -89,27 +113,56 @@ public IHttpActionResult GetSearchItems() /// The selected search item. /// if set to true [only default]. /// + [SupportedModules("IdentitySwitcher")] [AllowAnonymous] [HttpGet] - public IHttpActionResult GetUsers(string searchText = null, string selectedSearchItem = null, - bool onlyDefault = false) + public IHttpActionResult GetUsers(string searchText = null, string selectedSearchItem = null, bool onlyDefault = false) { var result = default(IHttpActionResult); + if (!CheckSecurity(ActiveModule, UserInfo)) + { + Exceptions.LogException(new Exception(String.Format("IdentitySwitcher module access without proper security context. " + + "ModuleId: {0} - TabId: {1} - PortalId: {2} - UserId: {3} - IP: {4}", + ActiveModule.ModuleID, + ActiveModule.TabID, + ActiveModule.PortalID, + UserInfo.UserID, + HttpContext.Current.Request.UserHostAddress + ))); + return result; + } + + var repository = new IdentitySwitcherModuleSettingsRepository(); + var settings = repository.GetSettings(ActiveModule); + try { - var usersInfo = new List(); + List usersList = new List(); // Get only the default users or.. if (!onlyDefault) { // ..get all users if no searchtext is provided or filtered users if a searchtext is provided. - usersInfo = searchText == null + usersList = searchText == null ? GetAllUsers() : GetFilteredUsers(searchText, selectedSearchItem); - usersInfo = SortUsers(usersInfo); } + List usersInfo = new List(); + if (settings.IncludeAdmin.GetValueOrDefault()) + { + usersInfo.AddRange(usersList); + } + else + { + foreach (UserInfo user in usersList) + { + if (!user.IsInRole(PortalSettings.AdministratorRoleName)) + usersInfo.Add(user); + } + } + usersInfo = SortUsers(usersInfo); AddDefaultUsers(usersInfo); var selectedUserId = UserInfo.UserID; @@ -117,14 +170,13 @@ public IHttpActionResult GetUsers(string searchText = null, string selectedSearc var resultData = new UserCollectionDto { Users = usersInfo.Select(userInfo => new UserDto - { - Id = userInfo.UserID, - UserName = userInfo.Username, - UserAndDisplayName = userInfo.DisplayName != null + { + Id = userInfo.UserID, + UserName = userInfo.Username, + UserAndDisplayName = userInfo.DisplayName != null ? $"{userInfo.DisplayName} - {userInfo.Username}" : userInfo.Username - }) - .ToList(), + }).ToList(), SelectedUserId = selectedUserId }; @@ -146,33 +198,118 @@ public IHttpActionResult GetUsers(string searchText = null, string selectedSearc /// The selected user identifier. /// Name of the selected user user. /// + [SupportedModules("IdentitySwitcher")] [AllowAnonymous] [HttpPost] public IHttpActionResult SwitchUser(int selectedUserId, string selectedUserName) { var result = default(IHttpActionResult); + if (!CheckSecurity(ActiveModule, UserInfo)) + { + Exceptions.LogException(new Exception(String.Format("IdentitySwitcher module access without proper security context. " + + "ModuleId: {0} - TabId: {1} - PortalId: {2} - UserId: {3} - IP: {4}", + ActiveModule.ModuleID, + ActiveModule.TabID, + ActiveModule.PortalID, + UserInfo.UserID, + HttpContext.Current.Request.UserHostAddress + ))); + return result; + } + try { - if (selectedUserId == -1) + // Log request + RequestLog requestLog = RequestLog(selectedUserId); + + // Request approval if required + var repository = new IdentitySwitcherModuleSettingsRepository(); + var settings = repository.GetSettings(ActiveModule); + if (settings.RequestAuthorization.GetValueOrDefault() && selectedUserId != -1) { - HttpContext.Current.Response.Redirect(Globals.NavigateURL("LogOff")); + SendRequestAuthorization(requestLog); } else { - var selectedUser = UserController.GetUserById(PortalSettings.PortalId, selectedUserId); + if (selectedUserId == -1) + { + HttpContext.Current.Response.Redirect(Globals.NavigateURL("LogOff")); + } + else + { + UserInfo selectedUser = UserController.GetUserById(PortalSettings.PortalId, selectedUserId); + ExecuteSwitchUser(selectedUser); + } + } + result = Ok(new + { + requestAuthorization = settings.RequestAuthorization.GetValueOrDefault(), + requestId = requestLog.Id + }); + } + catch (Exception exception) + { + Exceptions.LogException(exception); - DataCache.ClearUserCache(PortalSettings.PortalId, selectedUserName); + result = InternalServerError(exception); + } - // Sign current user out. - var objPortalSecurity = new PortalSecurity(); - objPortalSecurity.SignOut(); + return result; + } - // Sign new user in. - UserController.UserLogin(PortalSettings.PortalId, selectedUser, PortalSettings.PortalName, - HttpContext.Current.Request.UserHostAddress, false); + /// + /// Checks status of the request. + /// + /// Id of the request. + /// + [SupportedModules("IdentitySwitcher")] + [AllowAnonymous] + [HttpGet] + public IHttpActionResult CheckStatus(int id) + { + var result = default(IHttpActionResult); + + if (!CheckSecurity(ActiveModule, UserInfo)) + { + Exceptions.LogException(new Exception(String.Format("IdentitySwitcher module access without proper security context. " + + "ModuleId: {0} - TabId: {1} - PortalId: {2} - UserId: {3} - IP: {4}", + ActiveModule.ModuleID, + ActiveModule.TabID, + ActiveModule.PortalID, + UserInfo.UserID, + HttpContext.Current.Request.UserHostAddress + ))); + return result; + } + + try + { + bool approved = false; + + using (IDataContext ctx = DataContext.Instance()) + { + var rep = ctx.GetRepository(); + RequestLog log = rep.Find("WHERE Id = @0 ", id).FirstOrDefault(); + + // We allow the identity switch if the request is valid as: + // - user is the same that initiated the request + // - it is on the same IP + // - request is approved + // - approval date is not older than 1 hour + if (log != null && + log.RequestByUserId == UserInfo.UserID && + log.RequestIP == HttpContext.Current.Request.UserHostAddress && + log.ApprovalDate.HasValue && + DateTime.Now < log.ApprovalDate.Value.AddHours(1)) + { + UserInfo selectedUser = UserController.GetUserById(PortalSettings.PortalId, log.SwitchToUserId); + ExecuteSwitchUser(selectedUser); + + approved = true; + } } - result = Ok(); + result = Ok(approved); } catch (Exception exception) { @@ -183,6 +320,7 @@ public IHttpActionResult SwitchUser(int selectedUserId, string selectedUserName) return result; } + #endregion #region Private methods @@ -206,7 +344,7 @@ private void AddDefaultUsers(List users) var settings = repository.GetSettings(ActiveModule); // If includehost setting is set to true, add host users to the list. - if (settings.IncludeHost ?? false) + if (settings.IncludeHost.GetValueOrDefault()) { var hostUsers = UserController.GetUsers(false, true, Null.NullInteger); @@ -214,12 +352,11 @@ private void AddDefaultUsers(List users) { users.Insert( 0, - new UserInfo {Username = hostUser.Username, UserID = hostUser.UserID, DisplayName = null}); + new UserInfo { Username = hostUser.Username, UserID = hostUser.UserID, DisplayName = null }); } } - users.Insert(0, new UserInfo {Username = "Anonymous", DisplayName = null}); - + users.Insert(0, new UserInfo { Username = "Anonymous", DisplayName = null }); } /// @@ -283,6 +420,124 @@ private List GetFilteredUsers(string searchText, string selectedSearch return users; } + + /// + /// Adds a log of the impersonation action + /// + /// + private RequestLog RequestLog(int userIdToImpersonate) + { + RequestLog log = new RequestLog(); + + using (IDataContext ctx = DataContext.Instance()) + { + var rep = ctx.GetRepository(); + log.RequestId = Guid.NewGuid().ToString(); + log.RequestByUserId = UserInfo.UserID; + log.RequestDate = DateTime.Now; + log.RequestIP = HttpContext.Current.Request.UserHostAddress; + log.SwitchToUserId = userIdToImpersonate; + + rep.Insert(log); + } + + return log; + } + + /// + /// Sends a request authorization to the user that is being impersonated + /// + /// + private void SendRequestAuthorization(RequestLog requestLog) + { + string SharedResourceFile = "~/DesktopModules/IdentitySwitcher/App_LocalResources/SharedResources.resx"; + + string subject = Localization.GetString("RequestSubject", SharedResourceFile); + string body = Localization.GetString("RequestBody", SharedResourceFile); + + UserInfo user = UserController.GetUserById(PortalSettings.PortalId, requestLog.SwitchToUserId); + + ArrayList parameters = new ArrayList + { + user.DisplayName, + UserInfo.DisplayName, + UserInfo.Email, + user.Username, + requestLog.RequestDate.ToString(), + requestLog.RequestId + }; + + var tokenizer = new TokenReplace(); + subject = tokenizer.ReplaceEnvironmentTokens(subject); + body = tokenizer.ReplaceEnvironmentTokens(body, parameters, "Custom"); + + Mail.SendEmail(PortalSettings.Email, user.Email, subject, body); + } + + private bool CheckSecurity(ModuleInfo module, UserInfo user) + { + bool isValid = true; + + // module is of proper type + if (module.DesktopModule.FriendlyName != "IdentitySwitcher") + isValid = false; + + // user has permissions on the page + if (!TabPermissionController.CanViewPage(module.ParentTab)) + isValid = false; + + // user has permissions on the module + if(!ModulePermissionController.CanViewModule(module)) + isValid = false; + + return isValid; + } + + private void ExecuteSwitchUser(UserInfo selectedUser) + { + var repository = new IdentitySwitcherModuleSettingsRepository(); + var settings = repository.GetSettings(ActiveModule); + + if (!settings.IncludeHost.GetValueOrDefault() && selectedUser.IsSuperUser) + return; + + if (!settings.IncludeAdmin.GetValueOrDefault() && selectedUser.IsInRole(PortalSettings.AdministratorRoleName)) + return; + + DataCache.ClearUserCache(PortalSettings.PortalId, selectedUser.Username); + + // Sign current user out. + var objPortalSecurity = new PortalSecurity(); + objPortalSecurity.SignOut(); + + // Sign new user in. + UserController.UserLogin(PortalSettings.PortalId, selectedUser, PortalSettings.PortalName, + HttpContext.Current.Request.UserHostAddress, false); + } + #endregion + + public static bool ApproveRequest(string requestId) + { + using (IDataContext ctx = DataContext.Instance()) + { + var rep = ctx.GetRepository(); + RequestLog log = rep.Find("WHERE RequestId = @0 ", requestId).FirstOrDefault(); + + if (log != null) + { + PortalSettings ps = PortalController.Instance.GetCurrentPortalSettings(); + log.ApprovalByUserId = ps.UserInfo.UserID; + log.ApprovalDate = DateTime.Now; + log.ApprovalIP = HttpContext.Current.Request.UserHostAddress; + rep.Update(log); + + return true; + } + else + { + return false; + } + } + } } - #endregion } \ No newline at end of file diff --git a/IdentitySwitcher.csproj b/IdentitySwitcher.csproj index 05775dc..d9d64b0 100644 --- a/IdentitySwitcher.csproj +++ b/IdentitySwitcher.csproj @@ -27,7 +27,7 @@ On - v4.5.1 + v4.7.2 @@ -51,21 +51,19 @@ false - - packages\DotNetNuke.Core.8.0.2.4\lib\net40\DotNetNuke.dll + + False + packages\DotNetNuke.Core.9.4.0\lib\net45\DotNetNuke.dll False - - packages\DotNetNuke.Web.8.0.2.4\lib\net40\DotNetNuke.Web.dll - False + + packages\DotNetNuke.Web.9.4.0\lib\net45\DotNetNuke.Web.dll - - packages\DotNetNuke.Web.Client.8.0.2.4\lib\net40\DotNetNuke.Web.Client.dll - False + + packages\DotNetNuke.Web.Client.9.4.0\lib\net45\DotNetNuke.Web.Client.dll - packages\DotNetNuke.Web.8.0.2.4\lib\net40\DotNetNuke.WebUtility.dll - False + packages\DotNetNuke.Web.9.4.0\lib\net45\DotNetNuke.WebUtility.dll packages\DotNetNuke.Core.8.0.2.4\lib\net40\Microsoft.ApplicationBlocks.Data.dll @@ -73,9 +71,8 @@ - - packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll - False + + packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll @@ -100,10 +97,15 @@ - + + + Designer + + + @@ -113,6 +115,9 @@ + + RequestConfirmation.ashx + Settings.ascx @@ -134,11 +139,14 @@ Designer + + Designer + @@ -148,6 +156,8 @@ + + Designer diff --git a/Installation/04.00.00.sqlDataProvider b/Installation/04.00.00.sqlDataProvider new file mode 100644 index 0000000..3792f48 --- /dev/null +++ b/Installation/04.00.00.sqlDataProvider @@ -0,0 +1,19 @@ + +if not exists (select * from dbo.sysobjects where id = object_id(N'{databaseOwner}[{objectQualifier}DNN_IdentitySwitcherLog]') and OBJECTPROPERTY(id, N'IsTable') = 1) + BEGIN + CREATE TABLE {databaseOwner}[{objectQualifier}DNN_IdentitySwitcherLog] + ( + Id int NOT NULL IDENTITY (1, 1), + RequestId nvarchar(50) NOT NULL, + RequestByUserId int NOT NULL, + RequestDate datetime NOT NULL, + RequestIP nvarchar(50) NOT NULL, + SwitchToUserId int NOT NULL, + ApprovalDate datetime NULL, + ApprovalByUserId int NULL, + ApprovalIP nvarchar(50) NULL + ) + + ALTER TABLE {databaseOwner}[{objectQualifier}DNN_IdentitySwitcherLog] ADD CONSTRAINT [PK_{objectQualifier}DNN_IdentitySwitcherLog] PRIMARY KEY CLUSTERED ([Id]) + END +GO diff --git a/Installation/IdentitySwitcher.dnn b/Installation/IdentitySwitcher.dnn index 67d34b5..b7fbb54 100644 --- a/Installation/IdentitySwitcher.dnn +++ b/Installation/IdentitySwitcher.dnn @@ -1,6 +1,6 @@ - + IdentitySwitcher The IdentitySwitcher is a simple and useful tool to allow you to switch between users, without knowing their passwords. DesktopModules\IdentitySwitcher\IdentitySwitcher.png @@ -11,7 +11,7 @@ info@dnn-connect.org - + true @@ -23,6 +23,11 @@ 03.00.00.SqlDataProvider 03.00.00 + diff --git a/Installation/Project.targets b/Installation/Project.targets index 16abc09..f37fe72 100644 --- a/Installation/Project.targets +++ b/Installation/Project.targets @@ -552,8 +552,7 @@ - - + \ No newline at end of file diff --git a/Model/RequestLog.cs b/Model/RequestLog.cs new file mode 100644 index 0000000..7c494b8 --- /dev/null +++ b/Model/RequestLog.cs @@ -0,0 +1,47 @@ +#region Copyright + +// +// DotNetNuke® - http://www.dotnetnuke.com +// Copyright (c) 2002-2018 +// by DotNetNuke Corporation +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and +// to permit persons to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or substantial portions +// of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. +// + +#endregion + +using DotNetNuke.ComponentModel.DataAnnotations; +using System; + +namespace DNN.Modules.IdentitySwitcher.Model +{ + /// + /// + /// + [TableName("DNN_IdentitySwitcherLog")] + [PrimaryKey("Id", AutoIncrement = true)] + public class RequestLog + { + public int Id { get; set; } + public string RequestId { get; set; } + public int RequestByUserId { get; set; } + public DateTime RequestDate { get; set; } + public string RequestIP { get; set; } + public int SwitchToUserId { get; set; } + public DateTime? ApprovalDate { get; set; } + public int? ApprovalByUserId { get; set; } + public string ApprovalIP { get; set; } + } +} \ No newline at end of file diff --git a/ModuleSettings/IdentitySwitcherModuleSettings.cs b/ModuleSettings/IdentitySwitcherModuleSettings.cs index 8aa405d..2cbb0a8 100644 --- a/ModuleSettings/IdentitySwitcherModuleSettings.cs +++ b/ModuleSettings/IdentitySwitcherModuleSettings.cs @@ -46,6 +46,16 @@ public class IdentitySwitcherModuleSettings [TabModuleSetting(ParameterName = "includeHost")] public bool? IncludeHost { get; set; } + /// + /// Gets or sets the include admin. + /// + /// + /// The include admin. + /// + [TabModuleSetting(ParameterName = "includeAdmin")] + public bool? IncludeAdmin { get; set; } + + /// /// Gets or sets the sort by. /// @@ -63,5 +73,14 @@ public class IdentitySwitcherModuleSettings /// [TabModuleSetting(ParameterName = "userSwitchingSpeed")] public UserSwitchingSpeed UserSwitchingSpeed { get; set; } = UserSwitchingSpeed.Fast; + + /// + /// Enables the option to request authorization for the impersonation. + /// + /// + /// Request Authorization. + /// + [TabModuleSetting(ParameterName = "requestAuthorization")] + public bool? RequestAuthorization { get; set; } } } \ No newline at end of file diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 8a99a0a..2447ea6 100644 --- a/Properties/AssemblyInfo.cs +++ b/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: -[assembly: AssemblyVersion("3.1.0.0")] -[assembly: AssemblyFileVersion("3.1.0.0")] \ No newline at end of file +[assembly: AssemblyVersion("4.0.0.0")] +[assembly: AssemblyFileVersion("4.0.0.0")] \ No newline at end of file diff --git a/RequestConfirmation.ashx b/RequestConfirmation.ashx new file mode 100644 index 0000000..54f39fd --- /dev/null +++ b/RequestConfirmation.ashx @@ -0,0 +1 @@ +<%@ WebHandler Language="C#" CodeBehind="RequestConfirmation.ashx.cs" Class="DNN.Modules.IdentitySwitcher.RequestConfirmation" %> diff --git a/RequestConfirmation.ashx.cs b/RequestConfirmation.ashx.cs new file mode 100644 index 0000000..821a40d --- /dev/null +++ b/RequestConfirmation.ashx.cs @@ -0,0 +1,50 @@ +using DotNetNuke.Services.Localization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace DNN.Modules.IdentitySwitcher +{ + /// + /// Summary description for RequestConfirmation + /// + public class RequestConfirmation : IHttpHandler + { + + public void ProcessRequest(HttpContext context) + { + string SharedResourceFile = "~/DesktopModules/IdentitySwitcher/App_LocalResources/SharedResources.resx"; + + string requestId = ""; + bool validRequest = false; + + if (context.Request.QueryString["id"] != null) + { + requestId = context.Request.QueryString["id"]; + + validRequest = IdentitySwitcher.Controllers.IdentitySwitcherController.ApproveRequest(requestId); + } + + + if (validRequest) + { + context.Response.ContentType = "text/html"; + context.Response.Write(Localization.GetString("RequestApproved", SharedResourceFile)); + } + else + { + context.Response.ContentType = "text/html"; + context.Response.Write(Localization.GetString("RequestInvalid", SharedResourceFile)); + } + } + + public bool IsReusable + { + get + { + return true; + } + } + } +} diff --git a/Settings.ascx b/Settings.ascx index 993d0e8..afe76e3 100644 --- a/Settings.ascx +++ b/Settings.ascx @@ -6,6 +6,10 @@ +
+ + +
@@ -16,4 +20,8 @@
+
+ + +
diff --git a/Settings.ascx.cs b/Settings.ascx.cs index 8c7587d..dffb55d 100644 --- a/Settings.ascx.cs +++ b/Settings.ascx.cs @@ -74,18 +74,26 @@ public override void LoadSettings() if (UserInfo.IsSuperUser) { - if (settings.IncludeHost != null) - { - cbIncludeHostUser.Checked = (bool) settings.IncludeHost; - } + cbIncludeHostUser.Checked = settings.IncludeHost.GetValueOrDefault(); } else { trHostSettings.Visible = false; } - - rbSortBy.SelectedValue = ((int) settings.SortBy).ToString(); - rbSelectingMethod.SelectedValue = ((int) settings.UserSwitchingSpeed).ToString(); + + if (UserInfo.IsSuperUser || UserInfo.IsInRole(PortalSettings.AdministratorRoleName)) + { + cbIncludeAdminUser.Checked = settings.IncludeAdmin.GetValueOrDefault(); + } + else + { + trAdminSettings.Visible = false; + } + + rbSortBy.SelectedValue = ((int)settings.SortBy).ToString(); + rbSelectingMethod.SelectedValue = ((int)settings.UserSwitchingSpeed).ToString(); + + cbRequestAuthorization.Checked = settings.RequestAuthorization.GetValueOrDefault(); } } catch (Exception exception) //Module failed to load @@ -114,9 +122,13 @@ public override void UpdateSettings() { settings.IncludeHost = cbIncludeHostUser.Checked; } - settings.SortBy = (SortBy) Enum.Parse(typeof(SortBy), rbSortBy.SelectedValue); - settings.UserSwitchingSpeed = - (UserSwitchingSpeed) Enum.Parse(typeof(UserSwitchingSpeed), rbSelectingMethod.SelectedValue); + if (UserInfo.IsSuperUser || UserInfo.IsInRole(PortalSettings.AdministratorRoleName)) + { + settings.IncludeAdmin = cbIncludeAdminUser.Checked; + } + settings.SortBy = (SortBy)Enum.Parse(typeof(SortBy), rbSortBy.SelectedValue); + settings.UserSwitchingSpeed = (UserSwitchingSpeed)Enum.Parse(typeof(UserSwitchingSpeed), rbSelectingMethod.SelectedValue); + settings.RequestAuthorization = cbRequestAuthorization.Checked; repository.SaveSettings(ModuleConfiguration, settings); diff --git a/Settings.ascx.designer.cs b/Settings.ascx.designer.cs index 7930a11..2091618 100644 --- a/Settings.ascx.designer.cs +++ b/Settings.ascx.designer.cs @@ -7,11 +7,13 @@ // //------------------------------------------------------------------------------ -namespace DNN.Modules.IdentitySwitcher { - - - public partial class Settings { - +namespace DNN.Modules.IdentitySwitcher +{ + + + public partial class Settings + { + /// /// trHostSettings control. /// @@ -20,7 +22,7 @@ public partial class Settings { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.HtmlControls.HtmlGenericControl trHostSettings; - + /// /// plIncludeHostUser control. /// @@ -29,7 +31,7 @@ public partial class Settings { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.UserControl plIncludeHostUser; - + /// /// cbIncludeHostUser control. /// @@ -38,7 +40,34 @@ public partial class Settings { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.CheckBox cbIncludeHostUser; - + + /// + /// trAdminSettings control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.HtmlControls.HtmlGenericControl trAdminSettings; + + /// + /// plIncludeAdminUser control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.UserControl plIncludeAdminUser; + + /// + /// cbIncludeAdminUser control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.CheckBox cbIncludeAdminUser; + /// /// plSortBy control. /// @@ -47,7 +76,7 @@ public partial class Settings { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.UserControl plSortBy; - + /// /// rbSortBy control. /// @@ -56,7 +85,7 @@ public partial class Settings { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.RadioButtonList rbSortBy; - + /// /// plSelectingMethod control. /// @@ -65,7 +94,7 @@ public partial class Settings { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.UserControl plSelectingMethod; - + /// /// rbSelectingMethod control. /// @@ -74,5 +103,23 @@ public partial class Settings { /// To modify move field declaration from designer file to code-behind file. /// protected global::System.Web.UI.WebControls.RadioButtonList rbSelectingMethod; + + /// + /// plRequestAuthorization control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.UserControl plRequestAuthorization; + + /// + /// cbRequestAuthorization control. + /// + /// + /// Auto-generated field. + /// To modify move field declaration from designer file to code-behind file. + /// + protected global::System.Web.UI.WebControls.CheckBox cbRequestAuthorization; } } diff --git a/TypeScript/app/identitySwitcher.d.ts b/TypeScript/app/identitySwitcher.d.ts index dfb9023..8720b8c 100644 --- a/TypeScript/app/identitySwitcher.d.ts +++ b/TypeScript/app/identitySwitcher.d.ts @@ -6,7 +6,8 @@ interface IIdentitySwitcherFactory { getSearchItems(moduleInstance: IModuleInstance): angular.IHttpPromise; getUsers(moduleInstance: IModuleInstance, selectedSearchText: string, selectedSearchItem: string, onlyDefault: boolean): angular.IHttpPromise; - switchUser(moduleInstance: IModuleInstance, selectedUserId: number, selectedUserName: string): angular.IHttpPromise; + switchUser(moduleInstance: IModuleInstance, selectedUserId: number, selectedUserName: string): angular.IHttpPromise; + checkStatus(moduleInstance: IModuleInstance, id: number): angular.IHttpPromise; } interface IUser { @@ -20,6 +21,11 @@ selectedUserId: number; } + interface ISwitchRequest { + requestAuthorization: boolean; + requestId: string; + } + interface IModuleInstanceValue { value: IModuleInstance; } @@ -31,6 +37,7 @@ PortalId: number; FilterText: string; SwitchToText: string; + WaitingForConfirmation: string; SwitchUserInOneClick: boolean; } } \ No newline at end of file diff --git a/TypeScript/app/identitySwitcher/identitySwitcher.controller.ts b/TypeScript/app/identitySwitcher/identitySwitcher.controller.ts index 3b26695..e9dafd4 100644 --- a/TypeScript/app/identitySwitcher/identitySwitcher.controller.ts +++ b/TypeScript/app/identitySwitcher/identitySwitcher.controller.ts @@ -22,6 +22,8 @@ foundUsers: IUser[] = []; selectedUser: IUser; + request: ISwitchRequest; + timerId; /**************************************************************************/ /* PUBLIC METHODS */ @@ -30,9 +32,14 @@ * search() */ search(onlyDefault: boolean = false): void { + this.request = null; + if (this.timerId) + clearInterval(this.timerId); + this.identitySwitcherFactory.getUsers(this.moduleInstance.value, this.selectedSearchText, - this.selectedItem, onlyDefault).then((serverData) => { + this.selectedItem, + onlyDefault).then((serverData) => { this.foundUsers = serverData.data.users; angular.forEach(this.foundUsers, (user) => { @@ -44,7 +51,7 @@ } }); } - ); + ); } /* @@ -61,17 +68,20 @@ */ switchUser(): void { this.identitySwitcherFactory.switchUser(this.moduleInstance.value, - this.selectedUser.id, - this.selectedUser.userName) + this.selectedUser.id, + this.selectedUser.userName) .then((serverData) => { - // Success - }, + // Success + this.request = serverData.data; + this.timerId = setInterval(() => this.checkRequestStatus(this.request.requestId), 2000); + }, (serverData) => { // Error alert('Something went wrong whilst switching users.'); } ).then(() => { - this.$window.location.reload(); + if (!this.request.requestAuthorization) + this.$window.location.reload(); }); } @@ -102,15 +112,30 @@ private getSearchItems(): void { this.identitySwitcherFactory.getSearchItems(this.moduleInstance.value) .then((serverData) => { - // Success - this.searchItems = serverData.data; - this.selectedItem = this.searchItems[0]; - }, + // Success + this.searchItems = serverData.data; + this.selectedItem = this.searchItems[0]; + }, (serverData) => { // Error } ); } + + private checkRequestStatus(id) { + this.identitySwitcherFactory.checkStatus(this.moduleInstance.value, id) + .then((serverData) => { + // Success + if (serverData.data) { + this.$window.location.reload(); + } + }, + (serverData) => { + // Error + alert('Something went wrong whilst switching users.'); + } + ); + } } /**************************************************************************/ diff --git a/TypeScript/app/identitySwitcher/identityswitcher.factory.ts b/TypeScript/app/identitySwitcher/identityswitcher.factory.ts index 3db2d33..b499ff4 100644 --- a/TypeScript/app/identitySwitcher/identityswitcher.factory.ts +++ b/TypeScript/app/identitySwitcher/identityswitcher.factory.ts @@ -37,14 +37,14 @@ }); } - switchUser(moduleInstance: IModuleInstance, selectedUserId: number, selectedUserName: string): angular.IHttpPromise { + switchUser(moduleInstance: IModuleInstance, selectedUserId: number, selectedUserName: string): angular.IHttpPromise { const apiUrl: string = moduleInstance.ApplicationPath + this.config.apiUrl + "identityswitcher/switchuser?selecteduserid=" + selectedUserId + "&selectedusername=" + selectedUserName; - return this.$http.post(apiUrl, null, + return this.$http.post(apiUrl, null, { headers: { "PortalId": moduleInstance.PortalId, @@ -53,6 +53,21 @@ } }); } + + checkStatus(moduleInstance: IModuleInstance, id: number): angular.IHttpPromise { + const apiUrl: string = moduleInstance.ApplicationPath + this.config.apiUrl + + "identityswitcher/checkstatus?id=" + id; + + return this.$http.get(apiUrl, + { + headers: { + "PortalId": moduleInstance.PortalId, + "ModuleId": moduleInstance.ModuleID, + "TabId": moduleInstance.ServicesFramework.getTabId() + } + }); + } + } angular.module(IdentitySwitcher.appName) .factory("IdentitySwitcherFactory", ["$q", "$http", "IdentitySwitcherConstants", ($q, $http, identitySwitcherConstants) => new IdentitySwitcherFactory($q, $http, identitySwitcherConstants)]); diff --git a/ViewIdentitySwitcher.ascx b/ViewIdentitySwitcher.ascx index 4974e0a..726417b 100644 --- a/ViewIdentitySwitcher.ascx +++ b/ViewIdentitySwitcher.ascx @@ -1,32 +1,37 @@ -<%@ Control Language="C#" Inherits="DNN.Modules.IdentitySwitcher.ViewIdentitySwitcher" - AutoEventWireup="true" Explicit="True" CodeBehind="ViewIdentitySwitcher.ascx.cs" %> +<%@ Control Language="C#" Inherits="DNN.Modules.IdentitySwitcher.ViewIdentitySwitcher" AutoEventWireup="true" Explicit="True" CodeBehind="ViewIdentitySwitcher.ascx.cs" %>
{{vm.moduleInstance.value.FilterText}}
- + -
-
{{vm.moduleInstance.value.SwitchToText}}
-
+
+ {{vm.moduleInstance.value.WaitingForConfirmation}} +
+ +
diff --git a/ViewIdentitySwitcher.ascx.cs b/ViewIdentitySwitcher.ascx.cs index 65e8cfc..0dd7ebf 100644 --- a/ViewIdentitySwitcher.ascx.cs +++ b/ViewIdentitySwitcher.ascx.cs @@ -153,7 +153,10 @@ private TModuleInstance GetModuleInstance(PortalModuleBase modu result.ModuleID = moduleControl.ModuleId; result.PortalId = moduleControl.PortalId; result.FilterText = Localization.GetString("FilterText.Text", LocalResourceFile); + result.FilterIconText = Localization.GetString("FilterIcon.Text", LocalResourceFile); result.SwitchToText = Localization.GetString("SwitchToText.Text", LocalResourceFile); + result.WaitingForConfirmation = Localization.GetString("WaitingForConfirmation.Text", LocalResourceFile); + result.SwitchIconText = Localization.GetString("SwitchIcon.Text", LocalResourceFile); var moduleInfo = new ModuleController().GetModule(moduleControl.ModuleId); var repository = new IdentitySwitcherModuleSettingsRepository(); @@ -201,6 +204,7 @@ private void Page_Load(object sender, EventArgs e) { try { + DotNetNuke.Framework.ServicesFramework.Instance.RequestAjaxScriptSupport(); if (!Page.IsPostBack) { InitializeModuleInstanceJson(divBaseDiv); diff --git a/module.css b/module.css index a0a7560..1a70c15 100644 --- a/module.css +++ b/module.css @@ -1,14 +1,43 @@ -.is_SearchLabel, .is_SearchTask, .is_SwitchLabel, .is_SwitchTask, .is_progress { float: left } +.is_SearchRow, .is_SwitchRow { + min-height: 35px; + clear: both +} + +.is_WaitRow { + text-align: center; + margin: 10px; + padding: 10px; + font-weight: bold; + border: solid 1px #888; + background-color: #dcdcdc; +} + + .is_WaitRow img { + padding-top: 10px + } + + +.is_SearchLabel, .is_SearchTask, .is_SwitchLabel, .is_SwitchTask, .is_progress { + float: left +} .is_SearchLabel, .is_SwitchLabel { vertical-align: middle; - width: 65px; + width: 100px; } -.is_SearchText, .is_SearchMenu { width: 125px } +.is_SearchText { + width: 200px +} -.is_SearchSeperator { width: 3px } +.is_SearchMenu { + width: 200px +} -.is_SwitchMenu { width: 253px } +.is_SearchSeperator { + width: 3px +} -.is_Clear { clear: both } \ No newline at end of file +.is_SwitchMenu { + width: 253px +} diff --git a/packages.config b/packages.config index 8469c2d..7ae3847 100644 --- a/packages.config +++ b/packages.config @@ -2,12 +2,12 @@ - - + + - + \ No newline at end of file