ADSIとはプログラムからActive Directoryへのアクセス方法を提供してくれるAPI。普通これを使う時はVBScript、VB、C# のいずれかだと思う。
今回、VC++でやって欲しいという仕事でえらい苦労したので、そのメモ。メインの使用インターフェイスはこちら。
ヘッダ類は以下。それと、リンカの「追加の依存ファイル」に Activeds.lib Adsiid.lib comsuppw.lib を追加する。
#include <locale> #include "ATLComTime.h" #include "Iads.h" #include "Adshlp.h" #include "atlsafe.h" #include "comutil.h"
サーバへのアクセス方法は大きく分けて2つある*1。WinNTプロバイダとLDAPプロバイダ。以下は2種類のIADsContainerオブジェクトの取得。
const _bstr_t WinNT_pathname = L"WinNT://domain名"; const _bstr_t LDAP_domainname = L"DC=domain名,DC=local"; const _bstr_t LDAP_pathname = _bstr_t(L"LDAP://") + LDAP_domainname; IADsContainer* winnt_container, ldap_container; HRESULT hr; void getContainer(){ setlocale(LC_ALL, "Japanese"); hr = CoInitialize(NULL); hr = ADsGetObject(WinNT_pathname , IID_IADsContainer, (void**)&winnt_container); hr = ADsGetObject(LDAP_pathname , IID_IADsContainer, (void**)&ldap_container); }
処理によってどちらを使うか、切り替えなければならない。(後述)
BSTRと_bstr_t
ADSIでは*2文字列を扱うのにUTF-16のBSTR型(中身はwchar_t**3)を使う。_bstr_t型はBSTRを内部に持っていて、+演算子を使えたり、BSTRとchar*の変換とかを暗黙的にやってくれたりするので便利*4。std::wstringより便利かも。※wchar_t*を引数とする関数にBSTRを渡すのは問題無いが、BSTRを引数とする関数にwchar_t*を渡してはならない!(後で分かった)でもコンパイルは通るので更に注意!
wchar_t* str = L"メッセージ"; _bstr_t _b = str; cout<<str<<endl; //wchar_t*なのでポインタアドレスが表示される cout<<_b<<endl; //char*に変換されるので正しく表示される
IADsUser
ユーザ作成と取得IADsUser *user; IDispatch *disp; void createUserWinNT(_bstr_t username){ hr = winnt_container->Create(L"user", username, &disp); hr = disp->QueryInterface(IID_IADsUser, (void**) &user); } void getUserWinNT(_bstr_t username){ hr = winnt_container->GetObjectW(L"User", username, &disp); hr = disp->QueryInterface(IID_IADsUser, (void**) &user); } void getUserLDAP(_bstr_t username){ hr = ADsGetObject( _bstr_t(L"LDAP://CN=") + username + L",CN=Users," + LDAP_domainname, (void**) &user); }
WinNTの場合とLDAPの場合の2つの関数を作ったのは理由がある。プライマリグループの取得はWinNTじゃないと出来なかったり、LDAPじゃないと使えないプロパティがあったりする。
IADsUserの情報取得・設定などのメソッドは専用のメソッドget_XXXXXXX,put_XXXXXXXを使うやり方と、Put/Getにプロパティ名を指定して使うやり方がある。例えば最終パスワード変更日時を取得するには、以下どちらでも良い。
DATE d;
hr = user->get_PasswordLastChanged(&d); //(LDAPのみ)
CComVariant v; hr = user->Get(L"pwdLastSet", &v); //(LDAPのみ)
Put/Get関数の場合、2つ目の引数はVARIANT型。だけど、それを継承したCComVariant型にした方が次の様な事が出来る。これならVariantInitとかのキモい関数を使わなくて済む。
CComVariant v; COleDateTime d(2012, 3, 1, 0, 0, 0); v = d; //CComVariantのoperator=()が内部のVARIANTに日付型を代入してくれる hr = user->Put(L"AccountExpirationDate", v); //アカウント期限を設定
次回ログイン時にパスワード変更要求をさせる
//LDAP CComVariant v = 0L; //逆に要求しない様にするなら -1L hr = user->Put(L"pwdLastSet", v);
//WinNT //出来る事になっているが、やってみたら出来なかった。Getは出来たのに。 CComVariant v = 1L; //逆に要求しない様にするなら 0L hr = user->Put(L"PasswordExpired", v);
WinNTのuserFlagsとLDAPのuserAccountControlはだいたい同じ役割だけど、出来る事・出来ない事が異なる。
Get
CComVariant v; hr = user->Get("userFlags", &v); //WinNTの場合 hr = user->Get("userAccountControl", &v); //LDAPの場合 if(v.lval & ADS_UF_PASSWD_CANT_CHANGE){ /*処理*/ } //WinNTのみ if(v.lval & ADS_UF_PASSWORD_EXPIRED){ /*処理*/ } //WinNTのみ if(v.lval & ADS_UF_ACCOUNTDISABLE){ /*処理*/ } if(v.lval & ADS_UF_DONT_EXPIRE_PASSWD){ /*処理*/ }
Put
CComVariant v; v |= ADS_UF_PASSWD_CANT_CHANGE //WinNTのみ | ADS_UF_PASSWORD_EXPIRED //WinNTでも出来なかった。代わりにLDAPのpwdLastSetを0にする | ADS_UF_ACCOUNTDISABLE | ADS_UF_DONT_EXPIRE_PASSWD; hr = user->Put("userFlags", v); //WinNTの場合 hr = user->Put("userAccountControl", v); //LDAPの場合
IsAccountLockedはログオン時にパスワード間違いをし過ぎた場合にtrueになる。get/put出来るが、putの際はfalse(ロック解除)しか出来ない(プログラムからはロック出来ないみたい)。
LDAPではロックも解除も正確にサポートされてない?らしい。
hr = user->put_IsAccountLocked(VARIANT_FALSE);
//出来ない// hr = user->put_IsAccountLocked(VARIANT_TRUE);
IADsGroup
グループの取得IADsGroup *group; hr = ADsGetObject( _bstr_t(L"LDAP://CN=") + groupname + L",CN=Users," + LDAP_domainname, (void**) &group);
グループへの参加
hr = group->Add(_bstr_t(L"LDAP://CN=") + username + L",CN=Users," + LDAP_domainname);
プライマリグループの設定
CComSafeArray<BSTR> barray(1); barray[0] = L"PrimaryGroupToken"; CComVariant v = barray; //暗黙的型変換 hr = group->GetInfoEx(v, 0); hr = group->Get(barray[0], 0); hr = user->Put(L"primaryGroupID", v);
グループ内のユーザ
IADs *pADs; ULONG lFetch; CComVariant var; IDispatch * pDisp; IADsMembers *members; hr = group->Members(&members); IUnknown *unknown; hr = members->get__NewEnum(&unknown); IEnumVARIANT *ppEnumerator; hr = pUnk->QueryInterface(IID_IEnumVARIANT,(void**)&ppEnumerator); while(ppEnumerator->Next(1, &var, &lFetch) == S_OK){ if(lFetch == 1){ BSTR bstr; pDisp = V_DISPATCH(&var); pDisp->QueryInterface(IID_IADs, (void**)&pADs); //ユーザオブジェクト取得 pADs->get_Name(&bstr); //ユーザ名取得 /*〜ユーザに対する処理〜*/ SysFreeString(bstr); pADs->Release(); } }
ユーザの所属グループ
IADs *pADs; ULONG lFetch; CComVariant var; IDispatch * pDisp; IADsMembers *groups; hr = winnt_user->Groups(&groups); IUnknown *unknown; hr = groups->get__NewEnum(&unknown); IEnumVARIANT *ppEnumerator; hr = unknown->QueryInterface(IID_IEnumVARIANT, (void**)&ppEnumerator); while(ppEnumerator->Next(1, &var, &lFetch) == S_OK){ if(lFetch == 1){ BSTR bstr; pDisp = V_DISPATCH(&var); pDisp->QueryInterface(IID_IADs, (void**)&pADs); //グループオブジェクト取得 pADs->get_Name(&bstr); //グループ名取得 /*〜グループに対する処理〜*/ SysFreeString(bstr); pADs->Release(); } }
IADsGroup::Members と IADsUser::Groups は両方共、IADsMembers** を引数に取るというふざけた仕様。全く意味が違うのに・・・・
なので、IEnumVARIANT::Nextの使い方は共通。・・・・もっと抽象化しといてくれよ・・・C++なんだからさ・・・・
エラー処理
HRESULT hrの対応するエラーメッセージを取得したい時はwchar_t b[1024]; FormatMessage( FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, hr, //GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // 既定の言語 b, sizeof(b), NULL );
メモリ解放
開放は専用のメソッドを使う。new/deleteで出来る様にしといてくれよ・・・と言いたい。もっと言えば自動(スタック)変数に出来るようにしてくれよ・・・。C++なんだからさ・・・・members->Release(); groups->Release(); unknown->Release(); ppEnumerator->Release(); winnt_container->Release(); user->Release();