/* Copyright (c) Cloud Software Group, Inc. * * Redistribution and use in source and binary forms, * with or without modification, are permitted provided * that the following conditions are met: * * * Redistributions of source code must retain the above * copyright notice, this list of conditions and the * following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the * following disclaimer in the documentation and/or other * materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Threading; using System.Windows.Forms; using XenAdmin.Actions; using XenAdmin.Commands; using XenAdmin.Controls.DataGridViewEx; using XenAdmin.Core; using XenAdmin.Dialogs; using XenAdmin.Network; using XenAPI; using XenCenterLib; namespace XenAdmin.TabPages { public partial class AdPage : BaseTabPage { private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); /// /// The pool this settings page refers to (can be a poolOfOne). Only set on the GUI thread, and we also assert that it is only /// ever set once. /// private Pool pool; private Host coordinator; private IXenConnection _connection; private Thread _loggedInStatusUpdater; private bool _updateInProgress; private readonly object _statusUpdaterLock = new object(); private string _storedDomain; private string _storedUsername; private readonly CollectionChangeEventHandler Pool_CollectionChangedWithInvoke; public IXenObject XenObject { set { Program.AssertOnEventThread(); ClearHandles(); _connection = value == null ? null : value.Connection; if (_connection == null) return; pool = Helpers.GetPoolOfOne(_connection); if (pool == null) // Cache not populated _connection.Cache.RegisterCollectionChanged(Pool_CollectionChangedWithInvoke); else pool.PropertyChanged += pool_PropertyChanged; if (_connection.Session != null) _connection.Session.PropertyChanged += Session_PropertyChanged; checkAdType(); if (_loggedInStatusUpdater == null) { // Fire off the the thread that will update the logged in status _loggedInStatusUpdater = new Thread(updateLoggedInStatus); _loggedInStatusUpdater.IsBackground = true; _loggedInStatusUpdater.Start(); } } } public AdPage() { InitializeComponent(); Pool_CollectionChangedWithInvoke = Program.ProgramInvokeHandler(Pool_CollectionChanged); ColumnSubject.CellTemplate = new KeyValuePairCell(); tTipLogoutButton.SetToolTip(Messages.AD_CANNOT_MODIFY_ROOT); tTipRemoveButton.SetToolTip(Messages.AD_CANNOT_MODIFY_ROOT); ConnectionsManager.History.CollectionChanged += History_CollectionChanged; Text = Messages.ACTIVE_DIRECTORY_TAB_TITLE; } public override string HelpID => "TabPageAD"; /// /// This method is used when the cache was not populated by the time we set the XenObject. It sets the appropriate event handlers, /// references to the coordinator and the pool, and populates the tab with the correct configuration. It de-registers /// itself when successful. /// /// /// void Pool_CollectionChanged(object sender, CollectionChangeEventArgs e) { pool = Helpers.GetPoolOfOne(_connection); if (pool == null) // Cache not populated return; _connection.Cache.DeregisterCollectionChanged(Pool_CollectionChangedWithInvoke); pool.PropertyChanged += pool_PropertyChanged; Program.Invoke(this, checkAdType); } private void RefreshCoordinator() { if (coordinator != null) coordinator.PropertyChanged -= coordinator_PropertyChanged; coordinator = Helpers.GetCoordinator(_connection); if (coordinator != null) coordinator.PropertyChanged += coordinator_PropertyChanged; } /// /// Clears all event handles that could be set in this control. /// private void ClearHandles() { if (pool != null) pool.PropertyChanged -= pool_PropertyChanged; if (coordinator != null) coordinator.PropertyChanged -= coordinator_PropertyChanged; if (_connection != null) { _connection.Cache.DeregisterBatchCollectionChanged(SubjectCollectionChanged); if (_connection.Session != null) _connection.Session.PropertyChanged -= Session_PropertyChanged; } } public override void PageHidden() { ClearHandles(); } /// /// We keep track of the actions currently running so we can disable the tab if we are in the middle of configuring AD. /// void History_CollectionChanged(object sender, CollectionChangeEventArgs e) { if (e.Action == CollectionChangeAction.Add && (e.Element is EnableAdAction || e.Element is DisableAdAction)) { AsyncAction action = (AsyncAction)e.Element; action.Completed += action_Completed; if (_connection != null && _connection == action.Connection) Program.Invoke(this, checkAdType); } } void action_Completed(ActionBase sender) { AsyncAction action = (AsyncAction)sender; action.Completed -= action_Completed; if (_connection != null && _connection == action.Connection) Program.Invoke(this, checkAdType); } void Session_PropertyChanged(object sender, PropertyChangedEventArgs e) { Program.BeginInvoke(this, RepopulateListBox); } /// /// We need to update the configuration if the authentication method changes, and also various labels display the name of the /// coordinator and should also be updated if that changes. /// /// /// void coordinator_PropertyChanged(object sender1, PropertyChangedEventArgs e) { if (e.PropertyName == "external_auth_type" || e.PropertyName == "name_label") Program.Invoke(this, checkAdType); } /// /// Various labels display the name of the pool and should also be updated if that changes. /// Additionally if the pool coordinator changes we need to update our event handles. /// There is a sanity check in the checkAdType() method in case this event is stuck in a queue. /// void pool_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == "name_label" || e.PropertyName == "master") Program.Invoke(this, checkAdType); } private void SetSubjectListEnable(bool enable) { Program.AssertOnEventThread(); GridViewSubjectList.Enabled = enable; if (GridViewSubjectList.Enabled) { LabelGridViewDisabled.Visible = false; RepopulateListBox(); foreach (AdSubjectRow r in GridViewSubjectList.Rows) { if (r.IsLocalRootRow) { r.ToggleExpandedState(); break; } } } else { foreach (AdSubjectRow row in GridViewSubjectList.Rows) { if (row.IsLocalRootRow) continue; row.subject.PropertyChanged -= subject_PropertyChanged; } GridViewSubjectList.Rows.Clear(); LabelGridViewDisabled.Visible = true; } } private void checkAdType() { Program.AssertOnEventThread(); //refresh the coordinator in case the cache is slow RefreshCoordinator(); if (coordinator == null) { log.WarnFormat("Could not resolve pool coordinator for connection '{0}'; disabling.", Helpers.GetName(_connection)); OnCoordinatorUnavailable(); return; } var action = (from ActionBase act in ConnectionsManager.History let async = act as AsyncAction where async != null && !async.IsCompleted && !async.Cancelled && async.Connection == _connection && (async is EnableAdAction || async is DisableAdAction) select async).FirstOrDefault(); if (action != null) { OnAdConfiguring(); } else if (coordinator.external_auth_type == Auth.AUTH_TYPE_NONE) // AD is not yet configured { OnAdDisabled(); } else // AD is already configured { if (coordinator.external_auth_type != Auth.AUTH_TYPE_AD) { log.WarnFormat("Unrecognised value '{0}' for external_auth_type on pool coordinator '{1}' for pool '{2}'; assuming AD enabled on pool.", coordinator.external_auth_type, Helpers.GetName(coordinator), Helpers.GetName(_connection)); } OnAdEnabled(); } } private void SubjectCollectionChanged(object sender, EventArgs e) { Program.BeginInvoke(this, RepopulateListBox); } private string Domain { get { Program.AssertOnEventThread(); Host coordinator = Helpers.GetCoordinator(_connection); if (coordinator == null) { log.WarnFormat("Could not resolve pool coordinator for connection '{0}'; disabling.", Helpers.GetName(_connection)); return Messages.UNKNOWN; } return coordinator.external_auth_service_name; } } private void OnAdEnabled() { Program.AssertOnEventThread(); flowLayoutPanel1.Enabled = true; SetSubjectListEnable(true); buttonJoinLeave.Text = Messages.AD_LEAVE_DOMAIN; buttonJoinLeave.Enabled = true; labelBlurb.Text = string.Format(Helpers.GetPool(_connection) != null ? Messages.AD_CONFIGURED_BLURB : Messages.AD_CONFIGURED_BLURB_HOST, Helpers.GetName(_connection).Ellipsise(70), Domain.Ellipsise(30)); _connection.Cache.RegisterBatchCollectionChanged(SubjectCollectionChanged); } private void OnAdDisabled() { Program.AssertOnEventThread(); flowLayoutPanel1.Enabled = false; SetSubjectListEnable(false); buttonJoinLeave.Text = Messages.AD_JOIN_DOMAIN; buttonJoinLeave.Enabled = true; labelBlurb.Text = string.Format(Helpers.GetPool(_connection) != null ? Messages.AD_NOT_CONFIGURED_BLURB : Messages.AD_NOT_CONFIGURED_BLURB_HOST, Helpers.GetName(_connection).Ellipsise(70)); _connection.Cache.DeregisterBatchCollectionChanged(SubjectCollectionChanged); } private void OnAdConfiguring() { Program.AssertOnEventThread(); flowLayoutPanel1.Enabled = false; SetSubjectListEnable(false); buttonJoinLeave.Enabled = false; labelBlurb.Text = string.Format(Helpers.GetPool(_connection) != null ? Messages.AD_CONFIGURING_BLURB : Messages.AD_CONFIGURING_BLURB_HOST, Helpers.GetName(_connection).Ellipsise(70)); } private void OnCoordinatorUnavailable() { Program.AssertOnEventThread(); flowLayoutPanel1.Enabled = false; SetSubjectListEnable(false); buttonJoinLeave.Enabled = false; labelBlurb.Text = Messages.AD_COORDINATOR_UNAVAILABLE_BLURB; } private void RepopulateListBox() { Program.AssertOnEventThread(); if (!GridViewSubjectList.Enabled) return; Dictionary selectedSubjectUuids = new Dictionary(); Dictionary expandedSubjectUuids = new Dictionary(); bool rootExpanded = false; string topSubject = ""; if (GridViewSubjectList.FirstDisplayedScrollingRowIndex > 0) { AdSubjectRow topRow = GridViewSubjectList.Rows[GridViewSubjectList.FirstDisplayedScrollingRowIndex] as AdSubjectRow; if (topRow != null && topRow.subject != null) topSubject = topRow.subject.uuid; } if (GridViewSubjectList.SelectedRows.Count > 0) { foreach (AdSubjectRow r in GridViewSubjectList.SelectedRows) { if (r.subject != null) selectedSubjectUuids.Add(r.subject.uuid, true); } } foreach (AdSubjectRow row in GridViewSubjectList.Rows) { if (row.Expanded) if (row.subject == null) rootExpanded = true; else expandedSubjectUuids.Add(row.subject.uuid, true); } try { _updateInProgress = true; GridViewSubjectList.SuspendLayout(); foreach (AdSubjectRow row in GridViewSubjectList.Rows) { if (row.IsLocalRootRow) continue; row.subject.PropertyChanged -= subject_PropertyChanged; } GridViewSubjectList.Rows.Clear(); var rows = new List { new AdSubjectRow(null) }; //local root account foreach (Subject subject in _connection.Cache.Subjects) //all other subjects in the pool { subject.PropertyChanged += subject_PropertyChanged; rows.Add(new AdSubjectRow(subject)); } GridViewSubjectList.Rows.AddRange(rows.ToArray()); GridViewSubjectList.Sort(GridViewSubjectList.SortedColumn ?? ColumnSubject, GridViewSubjectList.SortOrder == SortOrder.Ascending ? ListSortDirection.Ascending : ListSortDirection.Descending); // restore old selection, old expansion state and top row foreach (AdSubjectRow r in GridViewSubjectList.Rows) { r.Selected = !r.IsLocalRootRow && selectedSubjectUuids.ContainsKey(r.subject.uuid); r.Expanded = r.IsLocalRootRow ? rootExpanded : expandedSubjectUuids.ContainsKey(r.subject.uuid); if (!r.IsLocalRootRow && topSubject == r.subject.uuid) GridViewSubjectList.FirstDisplayedScrollingRowIndex = r.Index; } if (GridViewSubjectList.SelectedRows.Count == 0) GridViewSubjectList.Rows[0].Selected = true; HelpersGUI.ResizeGridViewColumnToAllCells(ColumnSubject); HelpersGUI.ResizeGridViewColumnToAllCells(ColumnRoles); HelpersGUI.ResizeGridViewColumnToAllCells(ColumnStatus); } finally { GridViewSubjectList.ResumeLayout(); _updateInProgress = false; EnableButtons(); } } private void subject_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName != "roles" && e.PropertyName != "other_config") return; var subject = sender as Subject; if (subject == null) return; Program.Invoke(this, () => { if (!GridViewSubjectList.Enabled) return; var found = (from DataGridViewRow row in GridViewSubjectList.Rows let adRow = row as AdSubjectRow where adRow != null && adRow.subject != null && adRow.subject.opaque_ref == subject.opaque_ref select adRow).FirstOrDefault(); try { GridViewSubjectList.SuspendLayout(); if (found == null) GridViewSubjectList.Rows.Add(new AdSubjectRow(subject)); else found.RefreshCellContent(subject); } finally { GridViewSubjectList.ResumeLayout(); EnableButtons(); } }); } /// /// Background thread called periodically to update subjects logged in status. /// private void updateLoggedInStatus() { // This could get a bit spammy with a repeated exception, consider a back off or summary approach if it becomes an issue Program.AssertOffEventThread(); while (!Disposing && !IsDisposed) { try { bool showing = false; Program.Invoke(this, delegate { showing = Program.MainWindow.TheTabControl.SelectedTab == Program.MainWindow.TabPageAD; }); if (showing) { String[] loggedInSids = _connection.Session.get_all_subject_identifiers(); // Want fast access time for when we take the lock and switch off the background thread Dictionary loggedSids = new Dictionary(); foreach (string s in loggedInSids) loggedSids.Add(s, true); Program.Invoke(this, delegate { foreach (AdSubjectRow r in GridViewSubjectList.Rows) { // local root row if (r.IsLocalRootRow) continue; r.LoggedIn = loggedSids.ContainsKey(r.subject.subject_identifier); } EnableButtons(); }); } lock (_statusUpdaterLock) { Monitor.Wait(_statusUpdaterLock, 5000); } } catch (Exception e) { showLoggedInStatusError(); log.Error(e); Thread.Sleep(5000); } } } private void showLoggedInStatusError() { Program.Invoke(this, delegate { foreach (AdSubjectRow r in GridViewSubjectList.Rows) { if (r.IsLocalRootRow) continue; r.SetStatusLost(); } }); } #region Custom AD Row Class /// /// Used in the DataGridView on the ConfigureAdDialog. Stores information about the subject and the different text to show if the /// row is expanded or not. /// private class AdSubjectRow : DataGridViewRow { private readonly DataGridViewImageCell _cellExpander = new DataGridViewImageCell(); private readonly DataGridViewImageCell _cellGroupOrUser = new DataGridViewImageCell(); private readonly KeyValuePairCell _cellSubjectInfo = new KeyValuePairCell(); private readonly DataGridViewTextBoxCell _cellRoles = new DataGridViewTextBoxCell(); private readonly DataGridViewTextBoxCell _cellLoggedIn = new DataGridViewTextBoxCell(); internal Subject subject { get; private set; } private bool expanded; private bool loggedIn; /// /// The row is created with unknown status until it's updated from outside the class /// private bool statusLost = true; public bool LoggedIn { get { return loggedIn; } set { loggedIn = value; statusLost = false; RefreshCellContent(); } } internal bool IsLocalRootRow { get { return subject == null; } } /// /// The full list of the subject's roles /// private readonly string expandedRoles = string.Empty; /// /// Topmost of the subject's roles /// private readonly string contractedRoles = string.Empty; /// /// The detailed info from the subject's other_config along with their display name /// private readonly List> expandedSubjectInfo = new List>(); /// /// The subject's display name /// private readonly List> contractedSubjectInfo = new List>(); /// /// A DataGridViewRow that corresponds to a subject and shows their /// information in expanded and collapsed states /// /// If null, if it is a root account (no subject) internal AdSubjectRow(Subject subject) { if (subject == null) //root account { expandedSubjectInfo.Add(new KeyValuePair(Messages.AD_LOCAL_ROOT_ACCOUNT, "")); expandedSubjectInfo.Add(new KeyValuePair("", "")); expandedSubjectInfo.Add(new KeyValuePair(Messages.AD_ALWAYS_GRANTED_ACCESS, "")); contractedSubjectInfo.Add(new KeyValuePair(Messages.AD_LOCAL_ROOT_ACCOUNT, "")); } else { this.subject = subject; var roles = subject.Connection.ResolveAll(subject.roles); roles.Sort(); roles.Reverse(); if (roles.Count > 0) expandedRoles = roles.Select(r => r.FriendlyName()).Aggregate((acc, s) => acc + "\n" + s); contractedRoles = roles.Count > 0 ? roles.Count > 1 ? roles[0].FriendlyName().AddEllipsis() : roles[0].FriendlyName() : ""; contractedSubjectInfo.Add(new KeyValuePair(subject.DisplayName ?? subject.SubjectName ?? "", "")); expandedSubjectInfo = Subject.ExtractKvpInfo(subject); } Cells.AddRange(_cellExpander, _cellGroupOrUser, _cellSubjectInfo, _cellRoles, _cellLoggedIn); RefreshCellContent(); } public void RefreshCellContent(Subject subj = null) { if (subj != null) subject = subj; _cellExpander.Value = expanded ? Images.StaticImages.expanded_triangle : Images.StaticImages.contracted_triangle; _cellGroupOrUser.Value = IsLocalRootRow || !subject.IsGroup ? Images.StaticImages._000_User_h32bit_16 : Images.StaticImages._000_UserAndGroup_h32bit_16; _cellSubjectInfo.Value = expanded ? expandedSubjectInfo : contractedSubjectInfo; _cellRoles.Value = expanded ? expandedRoles : contractedRoles; _cellLoggedIn.Value = IsLocalRootRow || subject.IsGroup || statusLost ? "-" : loggedIn ? Messages.YES : Messages.NO; } public void ToggleExpandedState() { expanded = !expanded; RefreshCellContent(); } public bool Expanded { get { return expanded; } set { if (expanded != value) { expanded = value; RefreshCellContent(); } } } public void SetStatusLost() { if (statusLost) return; statusLost = true; RefreshCellContent(); } } #endregion private void buttonJoinLeave_Click(object sender, EventArgs e) { if (buttonJoinLeave.Text == Messages.AD_JOIN_DOMAIN) { // We're enabling AD // Obtain domain, username and password; store the domain and username // so the user won't have to retype it for future join attempts using (var joinPrompt = new AdPasswordPrompt(true) { Domain = _storedDomain, Username = _storedUsername }) { var result = joinPrompt.ShowDialog(this); _storedDomain = joinPrompt.Domain; _storedUsername = joinPrompt.Username; if (result == DialogResult.Cancel) return; new EnableAdAction(_connection, joinPrompt.Domain, joinPrompt.Username, joinPrompt.Password).RunAsync(); } } else { // We're disabling AD // Warn if the user will boot himself out by disabling AD Session session = _connection.Session; if (session == null) return; if (session.IsLocalSuperuser) { // User is authenticated using local root account. Confirm anyway. string msg = string.Format(Messages.AD_LEAVE_CONFIRM, Helpers.GetName(_connection).Ellipsise(50).EscapeAmpersands(), Domain.Ellipsise(30)); DialogResult r; using (var dlg = new NoIconDialog(msg, ThreeButtonDialog.ButtonYes, new ThreeButtonDialog.TBDButton(Messages.NO_BUTTON_CAPTION, DialogResult.No, selected: true)) { WindowTitle = Messages.AD_FEATURE_NAME }) { r = dlg.ShowDialog(this); } //CA-64818: DialogResult can be No if the No button has been hit //or Cancel if the dialog has been closed from the control box if (r != DialogResult.Yes) return; } else { // Warn user will be booted out. string msg = string.Format(Helpers.GetPool(_connection) != null ? Messages.AD_LEAVE_WARNING : Messages.AD_LEAVE_WARNING_HOST, Helpers.GetName(_connection).Ellipsise(50), Domain.Ellipsise(30)); DialogResult r; using (var dlg = new WarningDialog(msg, ThreeButtonDialog.ButtonYes, new ThreeButtonDialog.TBDButton(Messages.NO_BUTTON_CAPTION, DialogResult.No, selected: true)) { WindowTitle = Messages.ACTIVE_DIRECTORY_TAB_TITLE }) { r = dlg.ShowDialog(this); } //CA-64818: DialogResult can be No if the No button has been hit //or Cancel if the dialog has been closed from the control box if (r != DialogResult.Yes) return; } Host coordinator = Helpers.GetCoordinator(_connection); if (coordinator == null) { // Really shouldn't happen unless we have been very slow with the cache log.Error("Could not retrieve coordinator when trying to look up domain.."); throw new Exception(Messages.CONNECTION_IO_EXCEPTION); } using (var passPrompt = new AdPasswordPrompt(false, coordinator.external_auth_service_name)) { var result = passPrompt.ShowDialog(this); if (result == DialogResult.Cancel) return; var creds = new Dictionary(); if (result != DialogResult.Ignore) { creds.Add(DisableAdAction.KEY_USER, passPrompt.Username); creds.Add(DisableAdAction.KEY_PASS, passPrompt.Password); } new DisableAdAction(_connection, creds).RunAsync(); } } } private void buttonAdd_Click(object sender, EventArgs e) { if (!buttonAdd.Enabled) return; using (var dlog = new ResolvingSubjectsDialog(_connection)) dlog.ShowDialog(this); } private void ButtonRemove_Click(object sender, EventArgs e) { // Double check, this method is called from a context menu as well and the state could have changed under it if (!ButtonRemove.Enabled) return; var subjectsToRemove = GridViewSubjectList.SelectedRows.Cast().Select(r => r.subject).ToList(); if (subjectsToRemove.Count < 1) return; var removeMessage = subjectsToRemove.Count == 1 ? string.Format(Messages.AD_REMOVE_USER_ONE, subjectsToRemove[0].DisplayName ?? subjectsToRemove[0].SubjectName) : string.Format(Messages.AD_REMOVE_USER_MANY, subjectsToRemove.Count); string adminMessage = null; var conn = subjectsToRemove.FirstOrDefault(s => s.Connection != null)?.Connection; if (conn != null && Helpers.StockholmOrGreater(conn) && !conn.Cache.Hosts.Any(Host.RestrictPoolSecretRotation)) { var poolAdminsToRemove = (from Subject s in subjectsToRemove let roles = s.Connection.ResolveAll(s.roles) where roles.Any(r => r.name_label == Role.MR_ROLE_POOL_ADMIN) select s).ToList(); if (subjectsToRemove.Count == poolAdminsToRemove.Count) adminMessage = poolAdminsToRemove.Count == 1 ? Messages.QUESTION_ADMIN_EXIT_PROCEDURE_ONE : Messages.QUESTION_ADMIN_EXIT_PROCEDURE_MANY; else if (poolAdminsToRemove.Count > 0) adminMessage = poolAdminsToRemove.Count == 1 ? Messages.QUESTION_ADMIN_EXIT_PROCEDURE_ONE_OF_MANY : string.Format(Messages.QUESTION_ADMIN_EXIT_PROCEDURE_SOME_OF_MANY, poolAdminsToRemove.Count); if (!string.IsNullOrEmpty(adminMessage)) removeMessage = string.Format("{0}\n\n{1} {2}", removeMessage, adminMessage, Messages.QUESTION_ADMIN_EXIT_PROCEDURE_ADVISORY); } using (var dlg = new WarningDialog(removeMessage, ThreeButtonDialog.ButtonYes, new ThreeButtonDialog.TBDButton(Messages.NO_BUTTON_CAPTION, DialogResult.No, selected: true)) { WindowTitle = Messages.AD_FEATURE_NAME }) { //CA-64818: DialogResult can be No if the No button has been hit //or Cancel if the dialog has been closed from the control box if (dlg.ShowDialog(this) != DialogResult.Yes) return; } // Warn if user is revoking his currently-in-use credentials Session session = _connection.Session; if (session != null && session.SessionSubject != null) { foreach (Subject entry in subjectsToRemove) { if (entry.opaque_ref == session.SessionSubject) { string subjectName = entry.DisplayName ?? entry.SubjectName; if (subjectName == null) { subjectName = entry.subject_identifier; } else { subjectName = subjectName.Ellipsise(256); } string msg = string.Format(entry.IsGroup ? Messages.AD_CONFIRM_LOGOUT_CURRENT_USER_GROUP : Messages.AD_CONFIRM_LOGOUT_CURRENT_USER, subjectName, Helpers.GetName(_connection).Ellipsise(50)); DialogResult r; using (var dlg = new WarningDialog(msg, ThreeButtonDialog.ButtonYes, new ThreeButtonDialog.TBDButton(Messages.NO_BUTTON_CAPTION, DialogResult.No, selected: true)) { WindowTitle = Messages.AD_FEATURE_NAME }) { r = dlg.ShowDialog(this); } //CA-64818: DialogResult can be No if the No button has been hit //or Cancel if the dialog has been closed from the control box if (r != DialogResult.Yes) return; break; } } } var action = new AddRemoveSubjectsAction(_connection, new List(), subjectsToRemove); using (var dlog = new ActionProgressDialog(action, ProgressBarStyle.Continuous)) dlog.ShowDialog(this); } private void GridViewSubjectList_SelectionChanged(object sender, EventArgs e) { if (!_updateInProgress) EnableButtons(); } private void EnableButtons() { Program.AssertOnEventThread(); if (GridViewSubjectList.SelectedRows.Count < 1) return; bool adminSelected = false; bool allSelectedLoggedIn = true; foreach (AdSubjectRow r in GridViewSubjectList.SelectedRows) { if (r.IsLocalRootRow) adminSelected = true; if (!r.LoggedIn) allSelectedLoggedIn = false; } tTipChangeRole.SuppressTooltip = ButtonChangeRoles.Enabled = !adminSelected; tTipChangeRole.SetToolTip(Messages.AD_CANNOT_MODIFY_ROOT); tTipLogoutButton.SuppressTooltip = !adminSelected; ButtonLogout.Enabled = !adminSelected && allSelectedLoggedIn; tTipRemoveButton.SuppressTooltip = ButtonRemove.Enabled = !adminSelected; toolStripMenuItemChangeRoles.Enabled = ButtonChangeRoles.Enabled; toolStripMenuItemLogout.Enabled = ButtonLogout.Enabled; toolStripMenuItemRemove.Enabled = ButtonRemove.Enabled; } private void GridViewSubjectList_CellClick(object sender, DataGridViewCellEventArgs e) { if (e.ColumnIndex < 0 || e.RowIndex < 0 || e.ColumnIndex != ColumnExpand.Index) return; var row = GridViewSubjectList.Rows[e.RowIndex] as AdSubjectRow; if (row != null) row.ToggleExpandedState(); } private void GridViewSubjectList_CellDoubleClick(object sender, DataGridViewCellEventArgs e) { if (e.ColumnIndex < 0 || e.RowIndex < 0) return; var row = GridViewSubjectList.Rows[e.RowIndex] as AdSubjectRow; if (row != null) row.ToggleExpandedState(); } private void GridViewSubjectList_SortCompare(object sender, DataGridViewSortCompareEventArgs e) { // First of all, the admin row is always displayed at the top. AdSubjectRow row1 = (AdSubjectRow)GridViewSubjectList.Rows[e.RowIndex1]; AdSubjectRow row2 = (AdSubjectRow)GridViewSubjectList.Rows[e.RowIndex2]; if (row1.IsLocalRootRow) { e.SortResult = GridViewSubjectList.SortOrder == SortOrder.Ascending ? -1 : 1; } else if (row2.IsLocalRootRow) { e.SortResult = GridViewSubjectList.SortOrder == SortOrder.Ascending ? 1 : -1; } else { // Next we look to see which column we are sorting on switch (GridViewSubjectList.SortedColumn.Index) { case 0: // shouldn't happen (expander column) case 1: // Group/individual picture column e.SortResult = GroupCompare(row1.subject, row2.subject); break; case 2: // Name and detail column e.SortResult = NameCompare(row1.subject, row2.subject); break; case 3: // Role Column e.SortResult = RoleCompare(row1.subject, row2.subject); break; case 4: // Logged in column e.SortResult = LoggedInCompare(row1, row2); break; } } e.Handled = true; } private int RoleCompare(Subject s1, Subject s2) { List s1Roles = _connection.ResolveAll(s1.roles); List s2Roles = _connection.ResolveAll(s2.roles); s1Roles.Sort(); s2Roles.Sort(); // If one subject doesn't have any roles, but it below the one with roles if (s1Roles.Count < 1) { if (s2Roles.Count < 1) { return 0; } return -1; } if (s2Roles.Count < 1) return 1; return s1Roles[0].CompareTo(s2Roles[0]); } private int NameCompare(Subject s1, Subject s2) { return StringUtility.NaturalCompare(s1.SubjectName, s2.SubjectName) * -1; } private int LoggedInCompare(AdSubjectRow s1, AdSubjectRow s2) { if (s1.LoggedIn) { if (s2.LoggedIn) return 0; return 1; } else { if (s2.LoggedIn) return -1; return 0; } } private int GroupCompare(Subject s1, Subject s2) { if (s1.IsGroup) { if (s2.IsGroup) return 0; return 1; } else { if (s2.IsGroup) return -1; return 0; } } private static string GetFormattedSubjectList(List selectedSubjects) { var listOfSubjects = string.Join(", ", selectedSubjects.Select(sub => sub.DisplayName ?? sub.SubjectName ?? sub.subject_identifier).ToList() ); return listOfSubjects; } private void ButtonChangeRoles_Click(object sender, EventArgs e) { if (Helpers.FeatureForbidden(_connection, Host.RestrictRBAC)) { UpsellDialog.ShowUpsellDialog(string.Format(Messages.UPSELL_BLURB_RBAC, BrandManager.ProductBrand), this); return; } // Double check, this method is called from a context menu as well and the state could have changed under it if (!ButtonChangeRoles.Enabled) return; var selectedRows = GridViewSubjectList.SelectedRows.Cast().ToList(); var selectedSubjects = selectedRows.Where(r => !r.IsLocalRootRow).Select(r => r.subject).ToList(); var loggedInSelectedSubjects = selectedRows.Where(r => !r.IsLocalRootRow && r.LoggedIn).Select(r => r.subject).ToList(); using (var dialog = new RoleSelectionDialog(_connection, selectedSubjects)) if (dialog.ShowDialog(this) == DialogResult.OK) { var selectedRoles = dialog.SelectedRoles.OrderBy(x => x).ToList(); var subjectsToLogout = new List(); var logSelfOut = false; foreach (var subject in loggedInSelectedSubjects) { var subjectRoles = subject.roles.Select(_connection.Resolve).OrderBy(x => x).ToList(); if (!selectedRoles.SequenceEqual(subjectRoles)) subjectsToLogout.Add(subject); if (subject.opaque_ref == _connection.Session.SessionSubject) logSelfOut = true; } if (subjectsToLogout.Count > 0 && !ConfirmLogout(logSelfOut, true, subjectsToLogout)) return; var actions = new List(); var successfulSubjects = new List(); foreach (var subject in selectedSubjects) { var action = new AddRemoveRolesAction(_connection, subject, dialog.SelectedRoles); action.Completed += actionBase => { if (!actionBase.IsCancelled && actionBase.Exception == null) successfulSubjects.Add(subject); }; actions.Add(action); } var format = selectedSubjects.Count > 1 ? Messages.AD_ADDING_REMOVING_ROLES_ON_MULTIPLE : Messages.AD_ADDING_REMOVING_ROLES_ON; var actionTitle = string.Format(format, GetFormattedSubjectList(selectedSubjects)); var updateRolesAction = new MultipleAction(_connection, actionTitle, Messages.IN_PROGRESS, Messages.COMPLETED, actions, true, true); updateRolesAction.Completed += actionBase => Program.Invoke(this, () => { if (actionBase.IsCancelled) return; var successfulSubjectsToLogOut = successfulSubjects.Where(subjectsToLogout.Contains).ToList(); if (successfulSubjectsToLogOut.Count <= 0) return; LogoutSubjects(_connection.Session, successfulSubjectsToLogOut); }); updateRolesAction.RunAsync(); } } private void ButtonLogout_Click(object sender, EventArgs e) { // Double check, this method is called from a context menu as well and the state could have changed under it if (!ButtonLogout.Enabled) return; var session = _connection.Session; if (session == null) return; var subjectsToLogout = new List(); var logSelfOut = false; foreach (AdSubjectRow r in GridViewSubjectList.SelectedRows) { if (r.IsLocalRootRow || !r.LoggedIn) continue; subjectsToLogout.Add(r.subject); if (session.SessionSubject != null && r.subject.opaque_ref == session.SessionSubject) logSelfOut = true; } if (subjectsToLogout.Count <= 0 || !ConfirmLogout(logSelfOut, false, subjectsToLogout)) return; LogoutSubjects(session, subjectsToLogout); } private void LogoutSubjects(Session session, List subjectsToLogout) { // We go through the list and disconnect each user session, doing our own last if necessary var currentSubject = subjectsToLogout.FirstOrDefault(subject => subject.subject_identifier == session.UserSid); var otherSubjects = subjectsToLogout.Where(subject => subject.subject_identifier != session.UserSid).ToList(); if (otherSubjects.Count == 0 && currentSubject != null) { LogOutCurrentSubject(currentSubject); return; } var actions = otherSubjects.Select(NewLogOutSubjectAction).ToList(); var format = otherSubjects.Count > 1 ? Messages.TERMINATING_USER_SESSION_MULTIPLE : Messages.TERMINATING_USER_SESSION; var actionTitle = string.Format(format, GetFormattedSubjectList(otherSubjects)); var logoutAction = new MultipleAction(_connection, actionTitle, Messages.IN_PROGRESS, Messages.COMPLETED, actions, true, true); logoutAction.Completed += actionBase => Program.Invoke(this, () => { if (!(actionBase is MultipleAction ma) || actionBase.IsCancelled) return; if (currentSubject != null) { //passing in elevated credentials avoids multiple prompts LogOutCurrentSubject(currentSubject, new AsyncAction.SudoElevationResult(ma.sudoUsername, ma.sudoPassword, null)); } else { // signal the background thread to update the logged in status lock (_statusUpdaterLock) Monitor.Pulse(_statusUpdaterLock); } }); logoutAction.RunAsync(); } private void LogOutCurrentSubject(Subject currentSubject, AsyncAction.SudoElevationResult sudoElevation = null) { var action = NewLogOutSubjectAction(currentSubject); action.Completed += actionBase => Program.Invoke(this, () => { if (actionBase.IsCancelled) return; //Session.logout_subject_identifier logs out all sessions except the current one, //so if an elevated session was not needed, the current session will not have been //logged out, hence we need to disconnect explicitly. new DisconnectCommand(Program.MainWindow, _connection, false).Run(); }); action.RunAsync(sudoElevation); } private AsyncAction NewLogOutSubjectAction(Subject subject) { return new DelegatedAsyncAction(_connection, string.Format(Messages.TERMINATING_USER_SESSION, subject.DisplayName ?? subject.SubjectName), Messages.IN_PROGRESS, Messages.COMPLETED, s => Session.logout_subject_identifier(s, subject.subject_identifier), "session.logout_subject_identifier"); } private bool ConfirmLogout(bool logSelfOut, bool isRoleChange, List subjectsToLogout) { var logoutCurrentSubject = false; if (logSelfOut) { var format = isRoleChange ? subjectsToLogout.Count > 1 ? Messages.AD_LOGOUT_CURRENT_USER_MANY_ROLE_CHANGE : Messages.AD_LOGOUT_CURRENT_USER_ONE_ROLE_CHANGE : subjectsToLogout.Count > 1 ? Messages.AD_LOGOUT_CURRENT_USER_MANY : Messages.AD_LOGOUT_CURRENT_USER_ONE; var warnMsg = string.Format(format, Helpers.GetName(_connection).Ellipsise(50)); using (var dlg = new WarningDialog(warnMsg, ThreeButtonDialog.ButtonYes, new ThreeButtonDialog.TBDButton(Messages.NO_BUTTON_CAPTION, DialogResult.No, selected: true)) { WindowTitle = Messages.AD_FEATURE_NAME }) { //CA-64818: DialogResult can be No if the No button has been hit //or Cancel if the dialog has been closed from the control box if (dlg.ShowDialog(this) != DialogResult.Yes) return false; logoutCurrentSubject = true; } if (!DisconnectCommand.ConfirmCancelRunningActions(Program.MainWindow, this, _connection, true)) return false; } if (!logoutCurrentSubject) //CA-68645 { var logoutMessage = isRoleChange ? subjectsToLogout.Count == 1 ? string.Format(Messages.AD_LOGOUT_USER_ONE_ROLE_CHANGE, subjectsToLogout[0].DisplayName ?? subjectsToLogout[0].SubjectName) : string.Format(Messages.AD_LOGOUT_USER_MANY_ROLE_CHANGE, subjectsToLogout.Count) : subjectsToLogout.Count == 1 ? string.Format(Messages.AD_LOGOUT_USER_ONE, subjectsToLogout[0].DisplayName ?? subjectsToLogout[0].SubjectName) : string.Format(Messages.AD_LOGOUT_USER_MANY, subjectsToLogout.Count); using (var dlg = new WarningDialog(logoutMessage, ThreeButtonDialog.ButtonYes, new ThreeButtonDialog.TBDButton(Messages.NO_BUTTON_CAPTION, DialogResult.No, selected: true)) { WindowTitle = Messages.AD_FEATURE_NAME }) { //CA-64818: DialogResult can be No if the No button has been hit //or Cancel if the dialog has been closed from the control box if (dlg.ShowDialog(this) != DialogResult.Yes) return false; } } return true; } } }