Active Directory Webservices for DevOps





5.00/5 (5投票s)
活动目录 Web 服务,处理身份验证、单点登录 (SSO) 以及原生例程之外的其他应用程序。
引言
我发现创建一个 Web 服务来处理与活动目录相关的大部分日常工作很有意思,例如添加/删除用户/组/计算机,启用/禁用用户/计算机等。 这样做的想法是,无需每次需要这些微型例程时都实现它们,只需使用 Web 服务即可。但我实现这个东西的真正原因是身份验证,以及能够为希望使用此 Web 服务的所有应用程序提供 SSO。 这种实现并非最终版本,但它提供了如何扩展以包含所有与活动目录相关的例程的想法,这使得系统管理员在想要自动化任务(例如创建 AD 用户以及邮箱和启用 Lync)时,生活更轻松。 之前提到的那些任务可以使用 PowerShell 完成,如果不是全部,那么大多数任务都可以完成,但为了能够向应用程序提供此类功能,通过 Web 服务来完成它已经足够方便了。
背景
我在这里所做的是为活动目录例程实现 WCF Web 服务。 大多数例程都是 ComputerPrincipal
、UserPrincipal
、GroupPrincipal
和 DirectoryEntry
的包装器,这些都可在以下命名空间下使用
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
using System.DirectoryServices.ActiveDirectory;
除此之外,还为包含 用户名
、密码
和 会话开始
日期的 XML 序列化对象的会话实现 AES 加密。
Using the Code
代码很长,所以我会尽量保持主要内容。
代码被分成两类
- 与身份验证相关 (包括会话处理和加密)
- 活动目录例程
因此,我们将从身份验证相关开始,首先是会话处理,其中包含一个 可序列化结构
,其中包含 SessionData
,然后处理程序处理 SessionData 结构
的序列化和反序列化,我不会在这里包含加密和解密,因为它可以被您希望实现的任何对称加密算法替换。 产生的加密会话将与每个活动目录例程的每个请求一起发送。
[Serializable]
public struct SessionData
{
public string Username { get; set; }
public string Password { get; set; }
public DateTime SessionStart { get; set; }
}
public class SessionHandler
{
private static string SerializeObject<t>(SessionData sessionData)
{
string serialized = string.Empty;
XmlSerializer serializer = new XmlSerializer( typeof( T ) );
using ( StringWriter writer = new StringWriter() )
{
serializer.Serialize( writer , sessionData );
byte[] data = Encoding.UTF8.GetBytes( writer.ToString() );
return Convert.ToBase64String( data );
}
}
private static T DesrializeObject<t>( string data )
{
return (T)DesrializeObject( data , typeof( T ) );
}
private static object DesrializeObject( string objectData , Type type )
{
var serializer = new XmlSerializer( type );
object result;
using ( TextReader reader = new StringReader( objectData ) )
{
result = serializer.Deserialize( reader );
}
return result;
}
private static string Encrypt(string session)
{
string sessionCypher = Cryptor.Encrypt( session );
return sessionCypher;
}
private static string Decrypt( string session )
{
string decrptedSession = Cryptor.Decrypt( session );
return decrptedSession;
}
public static string EncryptSession(SessionData session)
{
string serializedSession = SerializeObject<sessiondata>( session );
string encryptedSession = Encrypt( serializedSession );
return encryptedSession;
}
public static SessionData DecryptSession( string session )
{
string decryptedSession = Decrypt( session );
SessionData desrialized = DesrializeObject<sessiondata>( decryptedSession );
return desrialized;
}
}
当用户使用其活动目录凭据进行身份验证时,一切都开始,这会返回一个加密会话,作为包含 IsAuthenticated
、Message
、SessionKey
的 JSON 对象,AuthData
类包含 用户名
、密码
。
public Session AuthenticateUserUsingCredentials( AuthDataRequest authData )
{
UserInfoResponse userInfo = new UserInfoResponse();
string emailAddress = authData.username;
string password = authData.password;
Session stat = new Session();
string msg = string.Empty;
if ( string.IsNullOrEmpty( emailAddress ) || string.IsNullOrEmpty( password ) )
{
stat.Message = "Email and/or password can't be empty!";
stat.IsAuthenticated = false;
return stat;
}
try
{
userInfo = GetUserAttributes( emailAddress );
if ( userInfo == null )
{
stat.Message = "Error: Couldn't fetch user information!";
stat.IsAuthenticated = false;
return stat;
}
var directoryEntry = new DirectoryEntry( LocalGcUri , userInfo.Upn , password );
directoryEntry.AuthenticationType = AuthenticationTypes.None;
var localFilter = string.Format( AdSearchFilter , emailAddress );
var localSearcher = new DirectorySearcher( directoryEntry );
localSearcher.PropertiesToLoad.Add( "mail" );
localSearcher.Filter = localFilter;
var result = localSearcher.FindOne();
if ( result != null )
{
stat.Message = "You have logged in successfully!";
stat.IsAuthenticated = true;
//Set the session Data
SessionData session = new SessionData();
session.Username = userInfo.EmailAddress;
session.Password = password;
session.SessionStart = DateTime.Now;
//Encrypt Session Data
stat.SessionKey = SessionHandler.EncryptSession( session );
return stat;
}
stat.Message = "Login failed, please try again.";
stat.IsAuthenticated = false;
return stat;
}
catch ( Exception ex )
{
stat.Message = "Wrong Email and/or Password " + ex;
stat.IsAuthenticated = false;
return stat;
}
}
[OperationContract]
[WebInvoke(
UriTemplate = "auth/user" ,
RequestFormat= WebMessageFormat.Json,
ResponseFormat = WebMessageFormat.Json,
BodyStyle = WebMessageBodyStyle.Bare,
Method = "POST" )]
Session AuthenticateUserUsingCredentials
( [MessageParameter( Name = "authdata" )] AuthDataRequest authData );
请注意,在您成功进行身份验证后,您将能够使用会话密钥对使用相同 Web 服务进行身份验证的不同应用程序进行身份验证,这可以按如下方式完成
Web 服务 OperationContract
定义
[OperationContract]
[WebInvoke(
UriTemplate = "auth/session" ,
ResponseFormat = WebMessageFormat.Json ,
RequestFormat = WebMessageFormat.Json ,
BodyStyle = WebMessageBodyStyle.Bare,
Method="POST")]
Session AuthenticateUserUsingSession
( [MessageParameter( Name = "sessionkey" )] string sessionKey );
至于实现,描述如下
public Session AuthenticateUserUsingSession( string sessionKey )
{
return ValidateSession( sessionKey );
}
此函数只是 SessionValidation
的包装器,会话将基于两个因素进行验证
- 成功解密
SessionKey
- 会话的有效性不超过可配置的 TTL,就我而言,是两个小时。
public Session ValidateSession( string sessionKey )
{
Session stat = new Session();
if ( string.IsNullOrWhiteSpace( sessionKey ) )
{
stat.Message = "No Session key has been provide";
stat.IsAuthenticated = false;
return stat;
}
else
{
try
{
SessionData sessionData = SessionHandler.DecryptSession( sessionKey );
if ( sessionKey != null && ( ( DateTime.Now.Subtract
( sessionData.SessionStart ) ).TotalHours < SessionTTL ) )
{
stat.Message = "You have logged in successfully!";
stat.IsAuthenticated = true;
stat.SessionKey = sessionKey;
return stat;
}
else
{
AuthDataRequest authData = new AuthDataRequest();
authData.username = sessionData.Username;
authData.password = sessionData.Password;
stat = AuthenticateUserUsingCredentials( authData );
stat.Message = "You have logged in successfully!,
and Session key has been renewed";
return stat;
}
}
catch ( Exception ex )
{
stat.Message = "Couldn't validate Session key,
kindly authenticate first " + ex;
stat.IsAuthenticated = false;
return stat;
}
}
}
就身份验证而言,我认为我们可以在这里停止,然后转到活动目录例程,请注意,我不会列出所有例程,因为它们很多,所以我将坚持几个例子。
AddUser
首先,我们定义一个具有 DataMembers
的 DataContract
[DataContract]
public class RequestUserCreate
{
[DataMember]
public string FirstName { get; set; }
[DataMember]
public string LastName { get; set; }
[DataMember]
public string UserLogonName { get; set; }
[DataMember]
public string EmployeeID { get; set; }
[DataMember]
public string EmailAddress { get; set; }
[DataMember]
public string Telephone { get; set; }
[DataMember]
public string Address { get; set; }
[DataMember]
public string PostalCode { get; set; }
[DataMember]
public string PostOfficeBox { get; set; }
[DataMember]
public string PhysicalDeliveryOffice { get; set; }
[DataMember]
public string Country { get; set; }
[DataMember]
public string City { get; set; }
[DataMember]
public string Title { get; set; }
[DataMember]
public string Department { get; set; }
[DataMember]
public string Company { get; set; }
[DataMember]
public string Description { get; set; }
[DataMember]
public string PhoneExtention { get; set; }
[DataMember]
public string PhoneIpAccessCode { get; set; }
[DataMember]
public string Password { get; set; }
[DataMember]
public DomainRequest DomainInfo { get; set; }
}
我认为您会注意到有一个名为 DomainRequest
的 Object
,它包括连接到域控制器或连接到全局目录所需的所有信息,除了在成功进行身份验证后保存会话密钥之外。
[DataContract]
public class DomainRequest
{
[DataMember]
public string ADHost { get; set; }
[DataMember]
public string DomainName { get; set; }
[DataMember]
public string ContainerPath { get; set; }
[DataMember]
public string BindingUserName { get; set; }
[DataMember]
public string BindingUserPassword { get; set; }
[DataMember]
public string SessionKey { get; set; }
}
但是,既然我们已经有了会话密钥并且已经通过身份验证了,为什么还要添加用户名和密码呢,答案很简单,因为您可能对该特定域容器没有写入或读取权限,因此您可以使用不同的用户名运行这些命令,但您必须已经过身份验证并且会话密钥有效。 另外要提到的是,它始终检查您是否有权访问该特定容器。
然后,我们定义 OperationContract
[OperationContract]
[WebInvoke(
UriTemplate = "ad/account/add" ,
RequestFormat = WebMessageFormat.Json ,
ResponseFormat = WebMessageFormat.Json ,
BodyStyle = WebMessageBodyStyle.Bare ,
Method = "POST" )]
ResponseMessage AddADUser( [MessageParameter( Name = "userinfo" )]
RequestUserCreate userinfo );
实现将如下所示
public ResponseMessage AddADUser( RequestUserCreate userinfo )
{
ResponseMessage status = new ResponseMessage();
status.IsSuccessful = false;
status.Message = string.Empty;
Session stat = ValidateSession( userinfo.DomainInfo.SessionKey );
if ( stat.IsAuthenticated == true )
{
PrincipalContext principalContext = null;
string uri = FixADURI( userinfo.DomainInfo.ADHost ,
userinfo.DomainInfo.ContainerPath );
if ( string.IsNullOrWhiteSpace( uri ) )
{
status.Message = status.Message =
"AD Host is not allowed to be empty, kindly provide the AD Host";
return status;
}
bool isAllowWite = CheckWriteOermission( uri ,
userinfo.DomainInfo.BindingUserName , userinfo.DomainInfo.BindingUserPassword );
try
{
UserPrincipal usr =
FindADUser( userinfo.UserLogonName , userinfo.DomainInfo );
if ( usr != null )
{
status.Message = " user already exists.
Please use a different User Logon Name";
return status;
}
else
{
principalContext = new PrincipalContext
( ContextType.Domain , userinfo.DomainInfo.DomainName ,
userinfo.DomainInfo.ContainerPath ,
userinfo.DomainInfo.BindingUserName ,
userinfo.DomainInfo.BindingUserPassword );
}
}
catch ( Exception ex )
{
status.Message = @"Failed to create PrincipalContext: " + ex;
return status;
}
// Create the new UserPrincipal object
UserPrincipal userPrincipal = new UserPrincipal( principalContext );
if ( !string.IsNullOrWhiteSpace( userinfo.LastName ) )
userPrincipal.Surname = userinfo.LastName;
if ( !string.IsNullOrWhiteSpace( userinfo.FirstName ) )
userPrincipal.GivenName = userinfo.FirstName;
if ( !string.IsNullOrWhiteSpace( userinfo.LastName ) &&
!string.IsNullOrWhiteSpace( userinfo.FirstName ) )
userPrincipal.DisplayName = userinfo.FirstName + " " + userinfo.LastName;
if ( !string.IsNullOrWhiteSpace( userinfo.Description ) )
userPrincipal.Description = userinfo.Description;
if ( !string.IsNullOrWhiteSpace( userinfo.EmployeeID ) )
userPrincipal.EmployeeId = userinfo.EmployeeID;
if ( !string.IsNullOrWhiteSpace( userinfo.EmailAddress ) )
userPrincipal.EmailAddress = userinfo.EmailAddress;
if ( !string.IsNullOrWhiteSpace( userinfo.Telephone ) )
userPrincipal.VoiceTelephoneNumber = userinfo.Telephone;
if ( !string.IsNullOrWhiteSpace( userinfo.UserLogonName ) )
userPrincipal.SamAccountName = userinfo.UserLogonName;
if ( !string.IsNullOrWhiteSpace( userinfo.Password ) )
userPrincipal.SetPassword( userinfo.Password );
userPrincipal.Enabled = true;
userPrincipal.ExpirePasswordNow();
try
{
userPrincipal.Save();
DirectoryEntry de = (DirectoryEntry)userPrincipal.GetUnderlyingObject();
FillUserExtraAttributes( ref de , userinfo );
de.CommitChanges();
status.Message = "Account has been created successfully";
status.IsSuccessful = true;
}
catch ( Exception ex )
{
status.Message = "Exception creating user object. " + ex;
status.IsSuccessful = false;
return status;
}
return status;
}
else
{
status.Message = "Kindly authenticate first";
return status;
}
}
在这里,我使用 UserPrincipal
使用基本信息填充用户对象,保存它并返回类型为 DirectoryEntry
的 UnderlyingObject
以填充其余信息,额外的属性函数如下所示
private void FillUserExtraAttributes
( ref DirectoryEntry de , RequestUserCreate userinfo )
{
try
{
if ( !string.IsNullOrWhiteSpace( userinfo.Title ) )
de.Properties[ "title" ].Value = userinfo.Title;
if ( !string.IsNullOrWhiteSpace( userinfo.City ) )
de.Properties[ "l" ].Value = userinfo.City;
if ( !string.IsNullOrWhiteSpace( userinfo.Country ) )
de.Properties[ "c" ].Value = userinfo.Country;
if ( !string.IsNullOrWhiteSpace( userinfo.PostalCode ) )
de.Properties[ "postalCode" ].Value = userinfo.PostalCode;
if ( !string.IsNullOrWhiteSpace( userinfo.PostOfficeBox ) )
de.Properties[ "postOfficeBox" ].Value = userinfo.PostOfficeBox;
if ( !string.IsNullOrWhiteSpace( userinfo.Address ) )
de.Properties[ "streetAddress" ].Value = userinfo.Address;
if ( !string.IsNullOrWhiteSpace( userinfo.Department ) )
de.Properties[ "department" ].Value = userinfo.Department;
if ( !string.IsNullOrWhiteSpace( userinfo.PhysicalDeliveryOffice ) )
de.Properties[ "physicalDeliveryOfficeName" ].Value =
userinfo.PhysicalDeliveryOffice;
if ( !string.IsNullOrWhiteSpace( userinfo.Company ) )
de.Properties[ "company" ].Value = userinfo.Company;
if ( !string.IsNullOrWhiteSpace( userinfo.PhoneExtention ) )
de.Properties[ "extensionAttribute1" ].Value = userinfo.PhoneExtention;
if ( !string.IsNullOrWhiteSpace( userinfo.PhoneIpAccessCode ) )
de.Properties[ "extensionAttribute2" ].Value = userinfo.PhoneIpAccessCode;
}
catch ( Exception ex )
{
throw ex;
}
}
Web 服务定义
WCF Web 服务已通过 REST 和 SOAP 公开,以便为开发人员提供选择其使用方式的灵活性,至于 *web.conf*,配置如下所示
<configuration>
<appSettings>
<add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
<add key="LocalDomainURI" value="GC://x.x.x.x" />
<add key="LocalDomainUser" value="bind-user" />
<add key="LocalDomainPassword" value="bind-password" />
<add key="ADSearchFilter"
value="(&(objectClass=user)(objectCategory=person)(mail={0}))" />
<add key="MailHost" value="X.X.X.X />
<add key="ReplyTo" value="xxx@cexample.com" />
<add key="NotificationsEmail" value="xxx@example.com" />
<add key="AesKey" value="AES KEY"/>
<add key="AesIV" value="AES IV"/>
<add key="SessionTTL" value="2"/>
</appSettings>
<system.web>
<compilation debug="true" targetFramework="4.5" />
<httpRuntime targetFramework="4.5"/>
</system.web>
<system.serviceModel>
<services>
<service name="ADWS.Adws">
<endpoint address="rest" behaviorConfiguration="webBehaviour"
binding="webHttpBinding" name="RESTEndPoint" contract="ADWS.IAdws" />
<endpoint address="soap" binding="basicHttpBinding" name="SOAPEndPoint"
contract="ADWS.IAdws" />
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="true"/>
</behavior>
</serviceBehaviors>
<endpointBehaviors>
<behavior name="webBehaviour">
<webHttp helpEnabled="true"/>
</behavior>
</endpointBehaviors>
</behaviors>
<protocolMapping>
<add binding="webHttpBinding" scheme="http" />
</protocolMapping>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true"
multipleSiteBindingsEnabled="true" />
</system.serviceModel>
<system.webServer>
<security>
<requestFiltering allowDoubleEscaping="true"/>
</security>
<modules runAllManagedModulesForAllRequests="true"/>
<httpProtocol>
<customHeaders>
<add name="Access-Control-Allow-Origin" value="*" />
<add name="Access-Control-Allow-Headers" value="Content-Type, Accept" />
</customHeaders>
</httpProtocol>
<directoryBrowse enabled="true"/>
</system.webServer>
</configuration>
AES 密钥和 IV 应在此处提供,为了安全起见,如果有人访问了 *web.conf*,您可以加密 *web.conf*,请参考这篇文章 以获取有关该主题的更多信息。
历史
- 版本 1.0