AD、SQL Server、GMail 同步工具






4.86/5 (5投票s)
GMail、AD 和 SQL Server 的多账户同步器。
引言
此程序旨在通过 Google 提供的 .NET API 来创建 GMail 账户。它使用 Visual Studio 2008 编写。整个项目都包含在内,在Release目录下应该有一个可运行的版本。
如何运行
第 1 步:设置 SQL 数据库配置
选择适合您用户账户和日志记录的 SQL Server。
第 2 步:创建 AD 账户和组
用户设置
用户可以从表或视图中拉取,并放置在执行账户有权访问的任何 Active Directory 位置。通过“自定义”选项卡,可以创建自定义列来添加字段映射。几乎所有 AD 中可写字段都可用。每个用户在创建后都会被放入一个默认组,以识别创建他们的用户同步。
组设置
组对于此工具来说并非必需,但似乎是很好的包含项,所以在此列出。它需要一个包含组列表的表或视图。该表中的唯一组集将在具有组追加名称的子 OU 中创建。要将用户分配到组,需要一个交叉引用表或视图。
- 将任何 SQL Server 2000 数据转换为 Microsoft Active Directory 用户或安全组。
- 通过字段分配将 AD 账户/组从 SQL Server 映射到 AD。
- 根据数据库源中的用户唯一 ID 同步数据到 AD。
- 账户数据不是实时同步的。
第 3 步:创建 GMail 账户
- 从 AD 到 GMail 的转换。
- 用户以 ID99999999@domain.gmail.apps.edu 的形式创建。
- 用户电子邮件账户的别名为 first.last@domain.gmail.apps.edu;如果存在重复,将通过组合中间名和名字来缩写以获得唯一的名称。
执行
- 任务计划程序可以调用 LDAP Magic 命令行,提供已保存映射的参数和操作类型。
- LDAP Magic 执行选定的操作并创建日志。
- 日志文件生成成功事务、警告和错误的记录。目前日志未保存。
Using the Code
代码量很大,我不知道哪些内容适合在此处列出。相当一部分代码已注释掉,简单的调试代码也隐藏在注释中。平均而言,每个函数都有一个简短的描述和一组预期的参数(如果它们不显而易见)。大部分代码设计得都非常健壮且易于重用。几乎所有可能出错的部分都配备了错误处理代码和日志记录。现在,让我们来看看一些精彩的部分。
首先,一个 SQL 查询逻辑的例子
//utils/toolset.cs
public SqlDataReader QueryInnerJoin(string table1, string table2,
string pkey1, string pkey2, ArrayList additionalFields,
SqlConnection sqlConn, LogFile log)
{
// additionalFields takes the field names " table.field,"
// Returns data from table1 where the row is in both table 1 and table2
// additional fields table2.data2
//**************************************************
//| Table1 | Table2 | Returned result
//**************************************************
//| ID Data | ID Data Data2 | | Table1. Table1. Table2.
// ID DATA data2
//| 1 a | 1 a e | RETURNED | 1 a e
//| 2 b | null null f | NOT RETURNED |
//| 3 c | 3 null g | RETURNED | 3 c g
//| 4 d | 4 e h | RETURNED | 4 d h
SqlDataReader r;
SqlCommand sqlComm;
string additionalfields = "";
foreach (string key in additionalFields)
{
additionalfields += key;
}
additionalfields = additionalfields.Remove(additionalfields.Length - 2);
if (additionalFields.Count > 0)
{
sqlComm = new SqlCommand("SELECT DISTINCT " + table1 + ".*, " + additionalfields +
" FROM " + table1 + " INNER JOIN " + table2 + " ON " +
table1 + "." + pkey1 + " = " + table2 + "." + pkey2, sqlConn);
}
else
{
sqlComm = new SqlCommand("SELECT DISTINCT " + table1 + ".* FROM " +
table1 + " INNER JOIN " + table2 + " ON " + table1 +
"." + pkey1 + " = " + table2 + "." + pkey2, sqlConn);
}
try
{
sqlComm.CommandTimeout = 360;
r = sqlComm.ExecuteReader();
log.addTrn(sqlComm.CommandText.ToString(), "Query");
return r;
}
catch (Exception ex)
{
log.addTrn("Failed SQL command " + sqlComm.CommandText.ToString() +
" error " + ex.Message.ToString() + "\n" +
ex.StackTrace.ToString(), "Error");
}
return null;
}
这是用于合并数据的查询样本之一。大多数查询是重复的;一个返回 `SQLDataReader`;互补的查询使用 `SELECT INTO` 并创建一个 SQL 表供其他方法使用。
接下来是批量 Active Directory 用户创建代码片段
//utils/toolset.cs
public void CreateUsersAccounts(string ouPath, SqlDataReader users,
string groupDn, string ldapDomain, UserSynch usersyn, LogFile log)
{
// oupath holds the path for the AD OU to hold the Users
// users is a sqldatareader witht the required fields
// in it ("CN") other Datastructures would be easy to substitute
// groupDN is a base group which all new users get automatically inserted into
int i;
int fieldcount;
int val;
string name = "";
string last = "";
string first = "";
fieldcount = users.FieldCount;
try
{
while (users.Read())
{
try
{
if (users[usersyn.User_password].ToString() != "")
{
if (!DirectoryEntry.Exists("LDAP://CN=" +
System.Web.HttpUtility.UrlEncode(
users[usersyn.User_sAMAccount].ToString()
).Replace("+", " ").Replace("*", "%2A") + "," + ouPath))
{
DirectoryEntry entry = new DirectoryEntry("LDAP://" + ouPath);
DirectoryEntry newUser = entry.Children.Add("CN=" +
System.Web.HttpUtility.UrlEncode(
users[usersyn.User_CN].ToString()).Replace(
"+", " ").Replace("*", "%2A"), "user");
// generated
newUser.Properties["samAccountName"].Value =
System.Web.HttpUtility.UrlEncode(
users[usersyn.User_sAMAccount].ToString()).Replace(
"+", " ").Replace("*", "%2A");
//newUser.Properties["mail"].Value = System.Web.HttpUtility.UrlEncode(
// users[usersyn.User_mail].ToString()).Replace("+", " ").Replace(
// "*", "%2A") + "@" + System.Web.HttpUtility.UrlEncode(
// users[usersyn.UserEmailDomain].ToString()).Replace(
// "+", " ").Replace("*", "%2A");
newUser.Properties["UserPrincipalName"].Value =
System.Web.HttpUtility.UrlEncode(
users[usersyn.User_sAMAccount].ToString()).Replace(
"+", " ").Replace("*", "%2A");
newUser.Properties["displayName"].Value =
System.Web.HttpUtility.UrlEncode(
(string)users[usersyn.User_Lname]).Replace(
"+", " ").Replace("*", "%2A") + ", " +
System.Web.HttpUtility.UrlEncode(
(string)users[usersyn.User_Fname]).Replace(
"+", " ").Replace("*", "%2A");
newUser.Properties["description"].Value =
System.Web.HttpUtility.UrlEncode(
(string)users[usersyn.User_Lname]).Replace("+", " ").Replace(
"*", "%2A") + ", " + System.Web.HttpUtility.UrlEncode(
(string)users[usersyn.User_Fname]).Replace(
"+", " ").Replace("*", "%2A");
newUser.CommitChanges();
// SQL query generated ensures matching field
// names between the SQL form fields and AD
for (i = 0; i < fieldcount; i++)
{
name = users.GetName(i);
// eliminiate non updatable fields
if (name != "password" && name != "CN")
{
// mail needs some special handling
if (name != "mail")
{
if ((string)users[name] != "")
{
newUser.Properties[name].Value =
System.Web.HttpUtility.UrlEncode(
(string)users[name]).Replace(
"+", " ").Replace("*", "%2A");
}
}
else
{
first = System.Web.HttpUtility.UrlEncode(
(string)users[name]).Replace("+", " ").Replace(
"*", "%2A").Replace("%40", "@");
last = (string)users[name];
// check to see if mail field has illegal characters
if (first == last)
{
// no illegal characters input the value into AD
newUser.Properties[name].Value =
System.Web.HttpUtility.UrlEncode(
(string)users[name]).Replace("+", " ").Replace(
"*", "%2A").Replace("!", "%21").Replace(
"(", "%28").Replace(")", "%29").Replace(
"'", "%27").Replace("_", "%5f").Replace(
" ", "%20").Replace("%40", "@");
}
else
{
// newUser.Properties[name].Value = "";
}
}
}
}
AddUserToGroup("CN=" + System.Web.HttpUtility.UrlEncode(
users[usersyn.User_sAMAccount].ToString()).Replace(
"+", " ").Replace("*", "%2A") + "," + usersyn.UserHoldingTank,
groupDn, false, ldapDomain, log);
newUser.Invoke("SetPassword", new object[] {
System.Web.HttpUtility.UrlEncode(
(string)users[usersyn.User_password]).Replace(
"+", " ").Replace("*", "%2A") });
newUser.CommitChanges();
val = (int)newUser.Properties["userAccountControl"].Value;
// set to normal user
newUser.Properties["userAccountControl"].Value =
val | (int)accountFlags.ADS_UF_NORMAL_ACCOUNT;
// set to enabled account val & ~0c0002 creates
// a bitmask which reverses the disabled bit
newUser.Properties["userAccountControl"].Value =
val & ~(int)accountFlags.ADS_UF_ACCOUNTDISABLE;
newUser.CommitChanges();
newUser.Close();
newUser.Dispose();
entry.Close();
entry.Dispose();
log.addTrn("User added |" + (string)users[usersyn.User_sAMAccount] +
" " + usersyn.UserHoldingTank, "Transaction");
}
else
{
log.addTrn("CN=" + System.Web.HttpUtility.UrlEncode(
(string)users[usersyn.User_sAMAccount]).Replace("+", " ").Replace(
"*", "%2A") + "," + ouPath +
" user already exists from adding", "Error");
//MessageBox.Show("CN=" + System.Web.HttpUtility.UrlEncode(
(string)users["CN"]).Replace("+", " ").Replace("*", "%2A") +
"," + ouPath + " user already exists from adding");
}
}
}
catch (Exception ex)
{
string debugdata = "";
for (i = 0; i < fieldcount; i++)
{
debugdata += users.GetName(i) + "=" +
System.Web.HttpUtility.UrlEncode((string)users[i]).Replace(
"+", " ").Replace("*", "%2A") + ", ";
}
log.addTrn("issue create user LDAP://CN=" + System.Web.HttpUtility.UrlEncode(
(string)users["CN"]).Replace("+", " ").Replace("*", "%2A") + "," + ouPath +
"\n" + debugdata + " User create failed, commit error" + name + " | " +
ex.Message.ToString() + "\n" + ex.StackTrace.ToString(), "Error");
// MessageBox.Show(e.Message.ToString() + "issue create user LDAP://CN=" +
// System.Web.HttpUtility.UrlEncode((string)users["CN"]).Replace("+",
// " ").Replace("*", "%2A") + "," + ouPath + "\n" + debugdata);
}
}
}
catch (Exception ex)
{
if (users != null)
{
string debugdata = "";
for (i = 0; i < fieldcount; i++)
{
debugdata += users.GetName(i) + "=" + System.Web.HttpUtility.UrlEncode(
(string)users[i]).Replace("+", " ").Replace("*", "%2A") + ", ";
}
log.addTrn("issue create user LDAP://CN=" + System.Web.HttpUtility.UrlEncode(
(string)users["CN"]).Replace("+", " ").Replace("*", "%2A") + "," + ouPath +
"\n" + debugdata + " failed field maybe " + name + " | " +
ex.Message.ToString() + "\n" + ex.StackTrace.ToString(), "Error");
// MessageBox.Show(e.Message.ToString() + "issue create user LDAP://CN=" +
// System.Web.HttpUtility.UrlEncode((string)users["CN"]).Replace(
// "+", " ").Replace("*", "%2A") + "," + ouPath + "\n" + debugdata);
}
else
{
log.addTrn("issue creating users datareader is null " +
ex.Message.ToString() + "\n" + ex.StackTrace.ToString(), "Error");
}
}
/*
//Add this to the create account method
int val = (int)newUser.Properties["userAccountControl"].Value;
//newUser is DirectoryEntry object
newUser.Properties["userAccountControl"].Value = val | 0x80000;
}
请注意,`UserSynch` 包含了从 GUI 界面收集的大部分字段名称。此代码负责接收 `SqlDataReader` 并创建 Active Directory 账户。有很多通用的方法可以做同样的事情。但是,如果您尝试从 SQL 表创建 AD 账户,这是一个很好的起点。
最后,用于从 SQLDataReader 创建 GMail 账户的代码
public DataTable Create_Gmail_Users(AppsService service,
GmailUsers gusersyn, SqlDataReader users, LogFile log)
{
// user alising not created yet
// Takes the SQLDataReader and creates all users in the reader
// create the table for holding the users final
// gmail account infomation for email writeback
DataTable returnvalue = new DataTable();
DataRow row;
returnvalue.TableName = "users";
returnvalue.Columns.Add(gusersyn.Writeback_primary_key);
returnvalue.Columns.Add(gusersyn.Writeback_email_field);
row = returnvalue.NewRow();
string studentID = "";
string first_name = "";
string last_name = "";
string middle_name = "";
string password = "";
string userNickName = "Aliasing off";
try
{
while (users.Read())
{
try
{
// using _ as escape character allows illegal characters in username
studentID = System.Web.HttpUtility.UrlEncode(
users[gusersyn.User_StuID].ToString()).Replace(
"+", " ").Replace("*", "%2A").Replace("!", "%21").Replace("(",
"%28").Replace(")", "%29").Replace("'", "%27").Replace(
"_", "%5f").Replace(" ", "%20").Replace("%", "_");
// names are less restricitve the only illegal characters are < > =
first_name = users[gusersyn.User_Fname].ToString().Replace(
"<", "%3c").Replace(">", "%3e").Replace("=", "%3d").Replace("%", "%25");
middle_name = users[gusersyn.User_Mname].ToString().Replace("<",
"%3c").Replace(">", "%3e").Replace(
"=", "%3d").Replace("%", "%25");
last_name = users[gusersyn.User_Lname].ToString().Replace("<",
"%3c").Replace(">", "%3e").Replace(
"=", "%3d").Replace("%", "%25");
if (gusersyn.User_password_generate_checkbox == false)
{
// password needs to bea able to handle special characters
password = users[gusersyn.User_password].ToString();
}
else
{
password = GetPassword(14);
}
if (gusersyn.User_password_short_fix_checkbox == true && password.Length < 8)
{
password = GetPassword(14);
}
//Create a new user.
UserEntry insertedEntry =
service.CreateUser(studentID, first_name, last_name, password);
//if (gusersyn.Levenshtein == true)
//{
// create user ailas here
userNickName = GetNewUserNickname(service, studentID,
first_name, middle_name, last_name, 0, false);
row[0] = studentID;
if (userNickName != "failure")
{
row[1] = userNickName + "@" + gusersyn.Admin_domain;
}
else
{
row[1] = studentID + "@" + gusersyn.Admin_domain;
}
returnvalue.Rows.Add(row);
row = returnvalue.NewRow();
log.addTrn("Added Gmail user " + studentID + "@" +
gusersyn.Admin_domain +
" Aliased as " + userNickName + "@" +
gusersyn.Admin_domain, "Transaction");
// }
}
catch (AppsException e)
{
log.addTrn("Failed adding Gmail user " + studentID + "@" +
gusersyn.Admin_domain +
" Aliased as " + userNickName + "@" + gusersyn.Admin_domain +
" failed " + e.Message.ToString() + " reason " +
e.Reason.ToString(), "Error");
}
catch (Exception ex)
{
log.addTrn("Failed adding Gmail user " + studentID + "@" +
gusersyn.Admin_domain + " Aliased as " + userNickName + "@" +
gusersyn.Admin_domain + " failed " + ex.Message.ToString() +
"\n" + ex.StackTrace.ToString(), "Error");
}
}
}
catch (Exception ex)
{
log.addTrn("Issue adding gmail users datareader is null " +
ex.Message.ToString() + "\n" + ex.StackTrace.ToString(), "Error");
}
return returnvalue;
}
此代码负责接收 `SQLDataReader` 并创建 GMail 账户。在创建 GMail 账户之前,它会进行一些关于非法字符的简单检查。它通过用更长的 14 个字符的密码覆盖来处理非法密码长度。最后,该方法构建一个包含成功创建的用户的 datatable 并返回它。
编译代码
该代码使用了 Google Data API 1.7.0.1,没有它将无法编译。请安装它,并确保 Google.Gdata 的引用指向您的安装。
关注点
这个项目最有趣的部分是花时间重新设计了其中的很多部分。每次,我都扔掉了 3/5 的代码。我花了大量时间研究数据结构和追求速度改进。该程序利用 SQL Server 来完成大部分处理,大部分功能都来自查询。
代码设计
LDAP 驱动程序分为四个主要文件
- Program.cs 包含命令行逻辑。
- Form1.cs 包含 GUI 代码。
- utils/toolset.cs 包含所有逻辑。
- utils/arguments.cs 包含命令行解析器。
LDAP Magic 警告
LDAP Magic 限制
- 此程序依赖于已保存数据中选定列的存在;如果列名更改,则需要重新映射。
- 此程序依赖于使用 (#) 临时表,因此需要对选定数据库的读写能力。
- 此程序尝试检查空列表,并对其进行鲁棒处理。但是,如果发生问题,很可能来自函数之间传递的未正确处理的空列表。
- 创建了 360 秒的自定义 SQL 连接超时,因此超时将不会与默认值匹配。当程序保持打开状态时,无限期保持的连接可能会成为一个问题。
- 创建的对象集很大,可能会出现内存泄漏。大多数对象已关闭;但是,有些依赖于垃圾回收器。
- 工具中的许多函数已重载,在更新工具时,请务必确保每个重载方法都符合新标准。
- 此程序有一个日志文件,当抛出异常时,它会显示哪个函数失败,并尝试添加有用的数据(时间戳、失败的函数、传递的变量)。
- 此程序必须由具有足够权限读取和写入 AD 用户和组的用户运行。
问题
- 用于多实例同时运行的唯一表命名。
- 创建后很少出现无法启用 Active Directory 账户的情况。
请勿
- 使用(用户映射)或(执行顺序)选项卡,它们已损坏。
- 除非使用配置中的临时表,否则不要同时运行多个副本。每个副本都将尝试访问数据库中的相同表。
愿望清单
- 用户/组/账户的预览区域
- AD 点击选择 OU 按钮
- 修复所有未解决的问题
感谢
- GriffonRL 提供了一个有用的 命令行解析器。
- thund3rstruck 提供了终极 Active Directory 操作指南。
- T. Wooley 提供了 SQL 查询、编码和代码设计评审方面的工作。
- P. Martin 负责美术和界面设计。
历史
- 代码更新于 2011 年 6 月 28 日。
- 已完成日志记录
- 提高了稳定性
- 更新了 GUI 设计
- 修复了更新查询
- 修复了 AD 中的删除问题
- Levenshtein 的基础代码已到位但未激活 - 应该很简单
- 包含的用户同步代码,只允许更新传输到 AD
- 更新了对具有特殊条件的 Active Directory 字段的处理
- 已更新以匹配 Gdata API 的新密码要求
- 代码于 2009 年 4 月 22 日首次发布。