update functionallity for security groups
This commit is contained in:
parent
53dc8efa5d
commit
14e505a502
6 changed files with 311 additions and 9 deletions
|
@ -2359,6 +2359,159 @@ namespace WebsitePanel.EnterpriseServer
|
||||||
|
|
||||||
return userId;
|
return userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int DeleteSecurityGroup(int itemId, int accountId)
|
||||||
|
{
|
||||||
|
// check account
|
||||||
|
int accountCheck = SecurityContext.CheckAccount(DemandAccount.NotDemo | DemandAccount.IsActive);
|
||||||
|
if (accountCheck < 0) return accountCheck;
|
||||||
|
|
||||||
|
// place log record
|
||||||
|
TaskManager.StartTask("ORGANIZATION", "DELETE_SECURITY_GROUP", itemId);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// load organization
|
||||||
|
Organization org = GetOrganization(itemId);
|
||||||
|
if (org == null)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
// load account
|
||||||
|
ExchangeAccount account = GetAccount(itemId, accountId);
|
||||||
|
|
||||||
|
Organizations orgProxy = GetOrganizationProxy(org.ServiceId);
|
||||||
|
|
||||||
|
orgProxy.DeleteSecurityGroup(itemId, account.AccountName);
|
||||||
|
|
||||||
|
DeleteUserFromMetabase(itemId, accountId);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw TaskManager.WriteError(ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
TaskManager.CompleteTask();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int SetUserGeneralSettings(int itemId, int accountId, string displayName, string managedBy, string[] memberAccounts, string notes)
|
||||||
|
{
|
||||||
|
// check account
|
||||||
|
int accountCheck = SecurityContext.CheckAccount(DemandAccount.NotDemo | DemandAccount.IsActive);
|
||||||
|
if (accountCheck < 0) return accountCheck;
|
||||||
|
|
||||||
|
// place log record
|
||||||
|
TaskManager.StartTask("ORGANIZATION", "UPDATE_SECURITY_GROUP_GENERAL", itemId);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
displayName = displayName.Trim();
|
||||||
|
|
||||||
|
// load organization
|
||||||
|
Organization org = GetOrganization(itemId);
|
||||||
|
if (org == null)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
// check package
|
||||||
|
int packageCheck = SecurityContext.CheckPackage(org.PackageId, DemandPackage.IsActive);
|
||||||
|
if (packageCheck < 0) return packageCheck;
|
||||||
|
|
||||||
|
// load account
|
||||||
|
ExchangeAccount account = ExchangeServerController.GetAccount(itemId, accountId);
|
||||||
|
|
||||||
|
string accountName = GetAccountName(account.AccountName);
|
||||||
|
// get mailbox settings
|
||||||
|
Organizations orgProxy = GetOrganizationProxy(org.ServiceId);
|
||||||
|
// external email
|
||||||
|
|
||||||
|
orgProxy.SetSecurityGroupGeneralSettings(
|
||||||
|
org.OrganizationId,
|
||||||
|
accountName,
|
||||||
|
displayName,
|
||||||
|
managedBy,
|
||||||
|
memberAccounts,
|
||||||
|
notes);
|
||||||
|
|
||||||
|
// update account
|
||||||
|
account.DisplayName = displayName;
|
||||||
|
|
||||||
|
UpdateAccount(account);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw TaskManager.WriteError(ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
TaskManager.CompleteTask();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExchangeAccountsPaged GetOrganizationSecurityGroupsPaged(int itemId, string filterColumn, string filterValue, string sortColumn,
|
||||||
|
int startRow, int maximumRows)
|
||||||
|
{
|
||||||
|
|
||||||
|
#region Demo Mode
|
||||||
|
if (IsDemoMode)
|
||||||
|
{
|
||||||
|
ExchangeAccountsPaged res = new ExchangeAccountsPaged();
|
||||||
|
List<ExchangeAccount> demoSecurityGroups = new List<ExchangeAccount>();
|
||||||
|
|
||||||
|
ExchangeAccount r1 = new ExchangeAccount();
|
||||||
|
r1.AccountId = 20;
|
||||||
|
r1.AccountName = "group1_fabrikam";
|
||||||
|
r1.AccountType = ExchangeAccountType.SecurityGroup;
|
||||||
|
r1.DisplayName = "Group 1";
|
||||||
|
demoSecurityGroups.Add(r1);
|
||||||
|
|
||||||
|
ExchangeAccount r2 = new ExchangeAccount();
|
||||||
|
r1.AccountId = 21;
|
||||||
|
r1.AccountName = "group2_fabrikam";
|
||||||
|
r1.AccountType = ExchangeAccountType.SecurityGroup;
|
||||||
|
r1.DisplayName = "Group 2";
|
||||||
|
demoSecurityGroups.Add(r2);
|
||||||
|
|
||||||
|
|
||||||
|
res.PageUsers = demoSecurityGroups.ToArray();
|
||||||
|
res.RecordsCount = res.PageUsers.Length;
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
string accountTypes = string.Format("{0}", ((int)ExchangeAccountType.SecurityGroup));
|
||||||
|
|
||||||
|
|
||||||
|
DataSet ds =
|
||||||
|
DataProvider.GetExchangeAccountsPaged(SecurityContext.User.UserId, itemId, accountTypes, filterColumn,
|
||||||
|
filterValue, sortColumn, startRow, maximumRows);
|
||||||
|
|
||||||
|
ExchangeAccountsPaged result = new ExchangeAccountsPaged();
|
||||||
|
result.RecordsCount = (int)ds.Tables[0].Rows[0][0];
|
||||||
|
|
||||||
|
List<ExchangeAccount> Tmpaccounts = new List<ExchangeAccount>();
|
||||||
|
ObjectUtils.FillCollectionFromDataView(Tmpaccounts, ds.Tables[1].DefaultView);
|
||||||
|
result.PageUsers = Tmpaccounts.ToArray();
|
||||||
|
|
||||||
|
List<ExchangeAccount> accounts = new List<ExchangeAccount>();
|
||||||
|
|
||||||
|
foreach (ExchangeAccount account in Tmpaccounts.ToArray())
|
||||||
|
{
|
||||||
|
OrganizationSecurityGroup tmpSecurityGroup = GetSecurityGroupGeneralSettings(itemId, account.AccountId);
|
||||||
|
|
||||||
|
if (tmpUser != null)
|
||||||
|
accounts.Add(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.PageUsers = accounts.ToArray();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -250,6 +250,24 @@ namespace WebsitePanel.EnterpriseServer
|
||||||
return OrganizationController.GetSecurityGroupGeneralSettings(itemId, accountId);
|
return OrganizationController.GetSecurityGroupGeneralSettings(itemId, accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[WebMethod]
|
||||||
|
public int DeleteSecurityGroup(int itemId, int accountId)
|
||||||
|
{
|
||||||
|
return OrganizationController.DeleteSecurityGroup(itemId, accountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[WebMethod]
|
||||||
|
public int SetUserGeneralSettings(int itemId, int accountId, string displayName, string managedBy, string[] memberAccounts, string notes)
|
||||||
|
{
|
||||||
|
return OrganizationController.SetUserGeneralSettings(itemId, accountId, displayName, managedBy, memberAccounts, notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExchangeAccountsPaged GetOrganizationSecurityGroupsPaged(int itemId, string filterColumn, string filterValue, string sortColumn,
|
||||||
|
int startRow, int maximumRows)
|
||||||
|
{
|
||||||
|
return OrganizationController.GetOrganizationSecurityGroupsPaged(itemId, filterColumn, filterValue, sortColumn, startRow, maximumRows);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -368,6 +368,14 @@ namespace WebsitePanel.Providers.HostedSolution
|
||||||
group.Invoke("Add", user.Path);
|
group.Invoke("Add", user.Path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void RemoveUserFromGroup(string userPath, string groupPath)
|
||||||
|
{
|
||||||
|
DirectoryEntry user = new DirectoryEntry(userPath);
|
||||||
|
DirectoryEntry group = new DirectoryEntry(groupPath);
|
||||||
|
|
||||||
|
group.Invoke("Remove", user.Path);
|
||||||
|
}
|
||||||
|
|
||||||
public static bool AdObjectExists(string path)
|
public static bool AdObjectExists(string path)
|
||||||
{
|
{
|
||||||
return DirectoryEntry.Exists(path);
|
return DirectoryEntry.Exists(path);
|
||||||
|
|
|
@ -44,6 +44,10 @@ namespace WebsitePanel.Providers.HostedSolution
|
||||||
|
|
||||||
OrganizationSecurityGroup GetSecurityGroupGeneralSettings(string groupName, string organizationId);
|
OrganizationSecurityGroup GetSecurityGroupGeneralSettings(string groupName, string organizationId);
|
||||||
|
|
||||||
|
void DeleteSecurityGroup(string groupName, string organizationId);
|
||||||
|
|
||||||
|
void SetSecurityGroupGeneralSettings(string organizationId, string groupName, string displayName, string managedBy, string[] memberAccounts, string notes);
|
||||||
|
|
||||||
void SetUserGeneralSettings(string organizationId, string accountName, string displayName, string password,
|
void SetUserGeneralSettings(string organizationId, string accountName, string displayName, string password,
|
||||||
bool hideFromAddressBook, bool disabled, bool locked, string firstName, string initials,
|
bool hideFromAddressBook, bool disabled, bool locked, string firstName, string initials,
|
||||||
string lastName,
|
string lastName,
|
||||||
|
|
|
@ -102,6 +102,20 @@ namespace WebsitePanel.Providers.HostedSolution
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetGroupPath(string organizationId, string groupName)
|
||||||
|
{
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
// append provider
|
||||||
|
AppendProtocol(sb);
|
||||||
|
AppendDomainController(sb);
|
||||||
|
AppendCNPath(sb, groupName);
|
||||||
|
AppendOUPath(sb, organizationId);
|
||||||
|
AppendOUPath(sb, RootOU);
|
||||||
|
AppendDomainPath(sb, RootDomain);
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
private string GetRootOU()
|
private string GetRootOU()
|
||||||
{
|
{
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
@ -810,42 +824,59 @@ namespace WebsitePanel.Providers.HostedSolution
|
||||||
|
|
||||||
#region Security Groups
|
#region Security Groups
|
||||||
|
|
||||||
public int CreateSecurityGroup(string organizationId, string displayName, string managedBy)
|
public int CreateSecurityGroup(string organizationId, string groupName, string displayName, string managedBy, string notes)
|
||||||
{
|
{
|
||||||
return CreateSecurityGroupInternal(organizationId, displayName, managedBy);
|
return CreateSecurityGroupInternal(organizationId, groupName, displayName, managedBy, notes);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal int CreateSecurityGroupInternal(string organizationId, string displayName, string managedBy)
|
internal int CreateSecurityGroupInternal(string organizationId, string groupName, string displayName, string managedBy, string notes)
|
||||||
{
|
{
|
||||||
HostedSolutionLog.LogStart("CreateSecurityGroupInternal");
|
HostedSolutionLog.LogStart("CreateSecurityGroupInternal");
|
||||||
HostedSolutionLog.DebugInfo("organizationId : {0}", organizationId);
|
HostedSolutionLog.DebugInfo("organizationId : {0}", organizationId);
|
||||||
HostedSolutionLog.DebugInfo("displayName : {0}", displayName);
|
HostedSolutionLog.DebugInfo("groupName : {0}", groupName);
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(organizationId))
|
if (string.IsNullOrEmpty(organizationId))
|
||||||
throw new ArgumentNullException("organizationId");
|
throw new ArgumentNullException("organizationId");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(groupName))
|
||||||
|
throw new ArgumentNullException("groupName");
|
||||||
|
|
||||||
bool groupCreated = false;
|
bool groupCreated = false;
|
||||||
string groupPath = null;
|
string groupPath = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string path = GetOrganizationPath(organizationId);
|
string path = GetOrganizationPath(organizationId);
|
||||||
groupPath = GetUserPath(organizationId, displayName);
|
groupPath = GetGroupPath(organizationId, groupName);
|
||||||
|
|
||||||
if (!ActiveDirectoryUtils.AdObjectExists(groupPath))
|
if (!ActiveDirectoryUtils.AdObjectExists(groupPath))
|
||||||
{
|
{
|
||||||
ActiveDirectoryUtils.CreateGroup(path, displayName);
|
ActiveDirectoryUtils.CreateGroup(path, groupName);
|
||||||
|
|
||||||
DirectoryEntry entry = new DirectoryEntry(groupPath);
|
DirectoryEntry entry = new DirectoryEntry(groupPath);
|
||||||
ActiveDirectoryUtils.SetADObjectProperty(entry, ADAttributes.Manager, managedBy);
|
|
||||||
|
ActiveDirectoryUtils.SetADObjectProperty(entry, ADAttributes.DisplayName, displayName);
|
||||||
|
|
||||||
|
string manager = string.Empty;
|
||||||
|
if (!string.IsNullOrEmpty(managedBy))
|
||||||
|
{
|
||||||
|
string managerPath = GetUserPath(organizationId, managedBy);
|
||||||
|
manager = ActiveDirectoryUtils.AdObjectExists(managerPath) ? managerPath : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveDirectoryUtils.SetADObjectProperty(entry, ADAttributes.Manager, ActiveDirectoryUtils.RemoveADPrefix(manager));
|
||||||
|
|
||||||
|
ActiveDirectoryUtils.SetADObjectProperty(entry, ADAttributes.Notes, notes);
|
||||||
entry.CommitChanges();
|
entry.CommitChanges();
|
||||||
|
|
||||||
groupCreated = true;
|
groupCreated = true;
|
||||||
HostedSolutionLog.DebugInfo("Security Group created: {0}", displayName);
|
|
||||||
|
HostedSolutionLog.DebugInfo("Security Group created: {0}", groupName);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
HostedSolutionLog.DebugInfo("AD_OBJECT_ALREADY_EXISTS: {0}", groupPath);
|
HostedSolutionLog.DebugInfo("AD_OBJECT_ALREADY_EXISTS: {0}", groupPath);
|
||||||
HostedSolutionLog.LogEnd("CreateSecurityGroupInternal");
|
HostedSolutionLog.LogEnd("CreateSecurityGroupInternal");
|
||||||
|
|
||||||
return Errors.AD_OBJECT_ALREADY_EXISTS;
|
return Errors.AD_OBJECT_ALREADY_EXISTS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -866,6 +897,7 @@ namespace WebsitePanel.Providers.HostedSolution
|
||||||
}
|
}
|
||||||
|
|
||||||
HostedSolutionLog.LogEnd("CreateSecurityGroupInternal");
|
HostedSolutionLog.LogEnd("CreateSecurityGroupInternal");
|
||||||
|
|
||||||
return Errors.OK;
|
return Errors.OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -880,10 +912,13 @@ namespace WebsitePanel.Providers.HostedSolution
|
||||||
HostedSolutionLog.DebugInfo("groupName : {0}", groupName);
|
HostedSolutionLog.DebugInfo("groupName : {0}", groupName);
|
||||||
HostedSolutionLog.DebugInfo("organizationId : {0}", organizationId);
|
HostedSolutionLog.DebugInfo("organizationId : {0}", organizationId);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(organizationId))
|
||||||
|
throw new ArgumentNullException("organizationId");
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(groupName))
|
if (string.IsNullOrEmpty(groupName))
|
||||||
throw new ArgumentNullException("groupName");
|
throw new ArgumentNullException("groupName");
|
||||||
|
|
||||||
string path = GetUserPath(organizationId, groupName);
|
string path = GetGroupPath(organizationId, groupName);
|
||||||
|
|
||||||
DirectoryEntry entry = ActiveDirectoryUtils.GetADObject(path);
|
DirectoryEntry entry = ActiveDirectoryUtils.GetADObject(path);
|
||||||
|
|
||||||
|
@ -908,6 +943,78 @@ namespace WebsitePanel.Providers.HostedSolution
|
||||||
return securityGroup;
|
return securityGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void DeleteSecurityGroup(string groupName, string organizationId)
|
||||||
|
{
|
||||||
|
DeleteSecurityGroupInternal(groupName, organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void DeleteSecurityGroupInternal(string groupName, string organizationId)
|
||||||
|
{
|
||||||
|
HostedSolutionLog.LogStart("DeleteSecurityGroupInternal");
|
||||||
|
HostedSolutionLog.DebugInfo("groupName : {0}", groupName);
|
||||||
|
HostedSolutionLog.DebugInfo("organizationId : {0}", organizationId);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(organizationId))
|
||||||
|
throw new ArgumentNullException("organizationId");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(groupName))
|
||||||
|
throw new ArgumentNullException("groupName");
|
||||||
|
|
||||||
|
string path = GetGroupPath(organizationId, groupName);
|
||||||
|
|
||||||
|
if (ActiveDirectoryUtils.AdObjectExists(path))
|
||||||
|
ActiveDirectoryUtils.DeleteADObject(path, true);
|
||||||
|
|
||||||
|
HostedSolutionLog.LogEnd("DeleteSecurityGroupInternal");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetSecurityGroupGeneralSettings(string organizationId, string groupName, string displayName, string managedBy, string[] memberAccounts, string notes)
|
||||||
|
{
|
||||||
|
|
||||||
|
SetSecurityGroupGeneralSettingsInternal(organizationId, groupName, displayName, managedBy, memberAccounts, notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void SetSecurityGroupGeneralSettingsInternal(string organizationId, string groupName, string displayName, string managedBy, string[] memberAccounts, string notes)
|
||||||
|
{
|
||||||
|
HostedSolutionLog.LogStart("SetSecurityGroupGeneralSettingsInternal");
|
||||||
|
HostedSolutionLog.DebugInfo("organizationId : {0}", organizationId);
|
||||||
|
HostedSolutionLog.DebugInfo("groupName : {0}", groupName);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(organizationId))
|
||||||
|
throw new ArgumentNullException("organizationId");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(groupName))
|
||||||
|
throw new ArgumentNullException("groupName");
|
||||||
|
|
||||||
|
string path = GetGroupPath(organizationId, groupName);
|
||||||
|
|
||||||
|
DirectoryEntry entry = ActiveDirectoryUtils.GetADObject(path);
|
||||||
|
|
||||||
|
ActiveDirectoryUtils.SetADObjectProperty(entry, ADAttributes.DisplayName, displayName);
|
||||||
|
|
||||||
|
string manager = string.Empty;
|
||||||
|
if (!string.IsNullOrEmpty(managedBy))
|
||||||
|
{
|
||||||
|
string managerPath = GetUserPath(organizationId, managedBy);
|
||||||
|
manager = ActiveDirectoryUtils.AdObjectExists(managerPath) ? managerPath : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
ActiveDirectoryUtils.SetADObjectProperty(entry, ADAttributes.Manager, ActiveDirectoryUtils.RemoveADPrefix(manager));
|
||||||
|
|
||||||
|
ActiveDirectoryUtils.SetADObjectProperty(entry, ADAttributes.Notes, notes);
|
||||||
|
|
||||||
|
foreach(string userPath in ActiveDirectoryUtils.GetUsersGroup(groupName)) {
|
||||||
|
ActiveDirectoryUtils.RemoveUserFromGroup(userPath, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(string user in memberAccounts) {
|
||||||
|
string userPath = GetUserPath(organizationId, user);
|
||||||
|
ActiveDirectoryUtils.AddUserToGroup(userPath, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.CommitChanges();
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public override bool IsInstalled()
|
public override bool IsInstalled()
|
||||||
|
|
|
@ -116,6 +116,18 @@ namespace WebsitePanel.Server
|
||||||
return Organization.GetSecurityGroupGeneralSettings(groupName, organizationId);
|
return Organization.GetSecurityGroupGeneralSettings(groupName, organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[WebMethod, SoapHeader("settings")]
|
||||||
|
public void DeleteSecurityGroup(string groupName, string organizationId)
|
||||||
|
{
|
||||||
|
Organization.DeleteSecurityGroup(groupName, organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[WebMethod, SoapHeader("settings")]
|
||||||
|
public void SetSecurityGroupGeneralSettings(string organizationId, string groupName, string displayName, string managedBy, string[] memberAccounts, string notes)
|
||||||
|
{
|
||||||
|
Organization.SetSecurityGroupGeneralSettings(organizationId, groupName, displayName, managedBy, memberAccounts, notes);
|
||||||
|
}
|
||||||
|
|
||||||
[WebMethod, SoapHeader("settings")]
|
[WebMethod, SoapHeader("settings")]
|
||||||
public void SetUserGeneralSettings(string organizationId, string accountName, string displayName, string password,
|
public void SetUserGeneralSettings(string organizationId, string accountName, string displayName, string password,
|
||||||
bool hideFromAddressBook, bool disabled, bool locked, string firstName, string initials, string lastName,
|
bool hideFromAddressBook, bool disabled, bool locked, string firstName, string initials, string lastName,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue