/* Copyright (c) Citrix Systems, Inc. * All rights reserved. * * 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.Windows.Forms; using XenAPI; using XenAdmin.Dialogs; using System.Threading; using XenAdmin.Controls.DataGridViewEx; using XenAdmin.Core; using XenAdmin.Actions; using XenAdmin.Commands; using XenAdmin.Network; 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 master; private IXenConnection _connection; private Thread _loggedInStatusUpdater; private bool _updateInProgress; 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 master 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 RefreshMaster() { if (master != null) master.PropertyChanged -= master_PropertyChanged; master = Helpers.GetMaster(_connection); if (master != null) master.PropertyChanged += master_PropertyChanged; } /// /// Clears all event handles that could be set in this control. /// private void ClearHandles() { if (pool != null) pool.PropertyChanged -= pool_PropertyChanged; if (master != null) master.PropertyChanged -= master_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 /// master and should also be updated if that changes. /// /// /// void master_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 master 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 master in case the cache is slow RefreshMaster(); if (master == null) { log.WarnFormat("Could not resolve pool master for connection '{0}'; disabling.", Helpers.GetName(_connection)); OnMasterUnavailable(); 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 (master.external_auth_type == Auth.AUTH_TYPE_NONE) // AD is not yet configured { OnAdDisabled(); } else // AD is already configured { if (master.external_auth_type != Auth.AUTH_TYPE_AD) { log.WarnFormat("Unrecognised value '{0}' for external_auth_type on pool master '{1}' for pool '{2}'; assuming AD enabled on pool.", master.external_auth_type, Helpers.GetName(master), Helpers.GetName(_connection)); } OnAdEnabled(); } } private void SubjectCollectionChanged(object sender, EventArgs e) { Program.BeginInvoke(this, RepopulateListBox); } private string Domain { get { Program.AssertOnEventThread(); Host master = Helpers.GetMaster(_connection); if (master == null) { log.WarnFormat("Could not resolve pool master for connection '{0}'; disabling.", Helpers.GetName(_connection)); return Messages.UNKNOWN; } return master.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 OnMasterUnavailable() { Program.AssertOnEventThread(); flowLayoutPanel1.Enabled = false; SetSubjectListEnable(false); buttonJoinLeave.Enabled = false; labelBlurb.Text = string.Format(Messages.AD_MASTER_UNAVAILABLE_BLURB, BrandManager.BrandConsole); } 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(); } }); } private object statusUpdaterLock = new object(); /// /// 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) { Program.AssertOnEventThread(); 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 master = Helpers.GetMaster(_connection); if (master == null) { // Really shouldn't happen unless we have been very slow with the cache log.Error("Could not retrieve master when trying to look up domain.."); throw new Exception(Messages.CONNECTION_IO_EXCEPTION); } using (var passPrompt = new AdPasswordPrompt(false, master.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_PASSWORD, passPrompt.Password); } new DisableAdAction(_connection, creds).RunAsync(); } } } private void buttonAdd_Click(object sender, EventArgs e) { Program.AssertOnEventThread(); if (!buttonAdd.Enabled) return; using (var dlog = new ResolvingSubjectsDialog(_connection)) dlog.ShowDialog(this); } private void ButtonRemove_Click(object sender, EventArgs e) { Program.AssertOnEventThread(); // 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.QUESTION_REMOVE_AD_USER_ONE, subjectsToRemove[0].DisplayName ?? subjectsToRemove[0].SubjectName) : string.Format(Messages.QUESTION_REMOVE_AD_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_SUICIDE_GROUP : Messages.AD_CONFIRM_SUICIDE, 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 void ButtonChangeRoles_Click(object sender, EventArgs e) { Program.AssertOnEventThread(); 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 selectedSubjects = GridViewSubjectList.SelectedRows.Cast().Where(r => !r.IsLocalRootRow).Select(r => r.subject).ToList(); using (var dialog = new RoleSelectionDialog(_connection, selectedSubjects)) if (dialog.ShowDialog(this) == DialogResult.OK) { foreach (Subject s in selectedSubjects) new AddRemoveRolesAction(_connection, s, dialog.SelectedRoles).RunAsync(); } } private void ButtonLogout_Click(object sender, EventArgs e) { Program.AssertOnEventThread(); // Double check, this method is called from a context menu as well and the state could have changed under it if (!ButtonLogout.Enabled) return; Session session = _connection.Session; if (session == null) return; var subjectsToLogout = new List(); bool 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; } bool suicide = false; if (logSelfOut) { var warnMsg = string.Format(subjectsToLogout.Count > 1 ? Messages.AD_LOGOUT_SUICIDE_MANY : Messages.AD_LOGOUT_SUICIDE_ONE, 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; suicide = true; } } var logoutMessage = subjectsToLogout.Count == 1 ? string.Format(Messages.QUESTION_LOGOUT_AD_USER_ONE, subjectsToLogout[0].DisplayName ?? subjectsToLogout[0].SubjectName) : string.Format(Messages.QUESTION_LOGOUT_AD_USER_MANY, subjectsToLogout.Count); if (!suicide)//CA-68645 { 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; } } // Then we go through the list and disconnect each user session, doing our own last if necessary foreach (AdSubjectRow r in GridViewSubjectList.SelectedRows) { if (r.IsLocalRootRow || !r.LoggedIn) continue; if (session.UserSid == r.subject.subject_identifier) continue; new DelegatedAsyncAction(_connection, string.Format(Messages.TERMINATING_USER_SESSION, r.subject.DisplayName ?? r.subject.SubjectName), Messages.IN_PROGRESS, Messages.COMPLETED, s => Session.logout_subject_identifier(s, r.subject.subject_identifier), "session.logout_subject_identifier").RunAsync(); } if (suicide) { new DisconnectCommand(Program.MainWindow, _connection, true).Execute(); } else { // signal the background thread to update the logged in status lock (statusUpdaterLock) Monitor.Pulse(statusUpdaterLock); } } } }