mirror of
https://github.com/xcp-ng/xenadmin.git
synced 2024-11-25 14:27:26 +01:00
2e66ec9133
Signed-off-by: Konstantina Chremmou <konstantina.chremmou@citrix.com> # Conflicts: # XenAdmin/Dialogs/OptionsPages/SecurityOptionsPage.cs
1161 lines
47 KiB
C#
1161 lines
47 KiB
C#
/* 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 XenAdmin.Properties;
|
|
using XenCenterLib;
|
|
|
|
namespace XenAdmin.TabPages
|
|
{
|
|
public partial class AdPage : BaseTabPage
|
|
{
|
|
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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>(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";
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="sender"></param>
|
|
/// <param name="e"></param>
|
|
void Pool_CollectionChanged(object sender, CollectionChangeEventArgs e)
|
|
{
|
|
pool = Helpers.GetPoolOfOne(_connection);
|
|
if (pool == null) // Cache not populated
|
|
return;
|
|
|
|
_connection.Cache.DeregisterCollectionChanged<Pool>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all event handles that could be set in this control.
|
|
/// </summary>
|
|
private void ClearHandles()
|
|
{
|
|
if (pool != null)
|
|
pool.PropertyChanged -= pool_PropertyChanged;
|
|
|
|
if (master != null)
|
|
master.PropertyChanged -= master_PropertyChanged;
|
|
|
|
if (_connection != null)
|
|
{
|
|
_connection.Cache.DeregisterBatchCollectionChanged<Subject>(SubjectCollectionChanged);
|
|
if (_connection.Session != null)
|
|
_connection.Session.PropertyChanged -= Session_PropertyChanged;
|
|
}
|
|
}
|
|
|
|
public override void PageHidden()
|
|
{
|
|
ClearHandles();
|
|
}
|
|
|
|
/// <summary>
|
|
/// We keep track of the actions currently running so we can disable the tab if we are in the middle of configuring AD.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="sender1"></param>
|
|
/// <param name="e"></param>
|
|
void master_PropertyChanged(object sender1, PropertyChangedEventArgs e)
|
|
{
|
|
if (e.PropertyName == "external_auth_type" || e.PropertyName == "name_label")
|
|
Program.Invoke(this, checkAdType);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<Subject>(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<Subject>(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 = Messages.AD_MASTER_UNAVAILABLE_BLURB;
|
|
}
|
|
|
|
private void RepopulateListBox()
|
|
{
|
|
Program.AssertOnEventThread();
|
|
|
|
if (!GridViewSubjectList.Enabled)
|
|
return;
|
|
|
|
Dictionary<string, bool> selectedSubjectUuids = new Dictionary<string, bool>();
|
|
Dictionary<string, bool> expandedSubjectUuids = new Dictionary<string, bool>();
|
|
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<DataGridViewRow> {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();
|
|
/// <summary>
|
|
/// Background thread called periodically to update subjects logged in status.
|
|
/// </summary>
|
|
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<string, bool> loggedSids = new Dictionary<string, bool>();
|
|
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
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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;
|
|
/// <summary>
|
|
/// The row is created with unknown status until it's updated from outside the class
|
|
/// </summary>
|
|
private bool statusLost = true;
|
|
|
|
public bool LoggedIn
|
|
{
|
|
get
|
|
{
|
|
return loggedIn;
|
|
}
|
|
set
|
|
{
|
|
loggedIn = value;
|
|
statusLost = false;
|
|
RefreshCellContent();
|
|
}
|
|
}
|
|
|
|
internal bool IsLocalRootRow
|
|
{
|
|
get { return subject == null; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The full list of the subject's roles
|
|
/// </summary>
|
|
private readonly string expandedRoles = string.Empty;
|
|
/// <summary>
|
|
/// Topmost of the subject's roles
|
|
/// </summary>
|
|
private readonly string contractedRoles = string.Empty;
|
|
/// <summary>
|
|
/// The detailed info from the subject's other_config along with their display name
|
|
/// </summary>
|
|
private readonly List<KeyValuePair<string, string>> expandedSubjectInfo = new List<KeyValuePair<String, String>>();
|
|
/// <summary>
|
|
/// The subject's display name
|
|
/// </summary>
|
|
private readonly List<KeyValuePair<string, string>> contractedSubjectInfo = new List<KeyValuePair<String, String>>();
|
|
|
|
/// <summary>
|
|
/// A DataGridViewRow that corresponds to a subject and shows their
|
|
/// information in expanded and collapsed states
|
|
/// </summary>
|
|
/// <param name="subject">If null, if it is a root account (no subject)</param>
|
|
internal AdSubjectRow(Subject subject)
|
|
{
|
|
if (subject == null) //root account
|
|
{
|
|
expandedSubjectInfo.Add(new KeyValuePair<string, string>(Messages.AD_LOCAL_ROOT_ACCOUNT, ""));
|
|
expandedSubjectInfo.Add(new KeyValuePair<string, string>("", ""));
|
|
expandedSubjectInfo.Add(new KeyValuePair<string, string>(Messages.AD_ALWAYS_GRANTED_ACCESS, ""));
|
|
contractedSubjectInfo.Add(new KeyValuePair<string, string>(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<string, string>(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<string, string>();
|
|
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<AdSubjectRow>().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<string>(), 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<Role> s1Roles = _connection.ResolveAll(s1.roles);
|
|
List<Role> 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))
|
|
{
|
|
// Show upsell dialog
|
|
using (var dlg = new UpsellDialog(HiddenFeatures.LinkLabelHidden ? Messages.UPSELL_BLURB_RBAC : Messages.UPSELL_BLURB_RBAC + Messages.UPSELL_BLURB_TRIAL,
|
|
InvisibleMessages.UPSELL_LEARNMOREURL_TRIAL))
|
|
dlg.ShowDialog(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<AdSubjectRow>().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<Subject>();
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|