コンピュータや音楽の事書いてます

VC++でADSI

ADSIとはプログラムからActive Directoryへのアクセス方法を提供してくれるAPI。普通これを使う時はVBScriptVBC# のいずれかだと思う。
今回、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();

*1:ホントはもっとあるみたいだけど

*2:というかCOM全般かな?

*3:普通のwchar_t*とは違って、ポインタアドレスの手前4BYTEに長さ情報を持っているという変態仕様

*4:ADSI以外で_bstr_tを使う時はcomsupp.libをリンカの「追加の依存ファイル」に加える