관리자 권한이 필요한 작업을 COM+에 대행
얼마 전 답변한 내용에서,
관리자 권한과 ClickOnce, 그리고 Bootstrapper 문제
; https://www.sysnet.pe.kr/3/0/1050
관리자 권한의 코드를 COM+에 대행해 보는 것이 어떨까 하는 의견을 냈었는데요. 저도 사실 이론적으로만 알고 있었을 뿐 해본 적은 없었는데, 그 답변을 계기로 직접 한번 해보았습니다.
우선, COM+ 코드를 만들어야 겠지요. 기본적인 COM+ 생성 강좌는 다음에 있으니 참고하시고,
Deploy the component as a Shared Assembly and Configure it in the COM+ Catalog
; http://gsraj.tripod.com/dotnet/complus/complus.net_accountmanager.html
여기서는 위에 설명된 내용을 중복할 필요없이 곧바로 "ClassLibraryAsAdmin"이라는 프로젝트를 생성해서 다음과 같이 작성했습니다.
[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationAccessControl(false)]
[assembly: ApplicationName("ClassLibraryAsAdmin")]
[assembly: ComVisible(true)]
namespace ClassLibraryAsAdmin
{
[System.Runtime.InteropServices.Guid("41AC8568-9230-4E63-B7C5-CAAD997EE207")]
public class AdminCode : ServicedComponent, IAdminCode
{
public void SetRegistryValue()
{
using (RegistryKey regKey = Registry.LocalMachine.OpenSubKey("SYSTEM", true)) // 관리자 권한 필요
{
regKey.CreateSubKey("Test");
}
}
}
}
IAdminCode 인터페이스는 위의 클래스 라이브러리를 직접 참조를 하지 않도록 별도의 DLL 프로젝트(ComBaseClass)를 하나 만들어서 정의했고,
[assembly: ComVisible(true)]
namespace ComBaseClass
{
[Guid("23172f2f-a3d3-4180-97ae-7805f74a5a46")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IAdminCode
{
void SetRegistryValue();
}
}
이제 "ClassLibraryAsAdmin" 프로젝트를 빌드하고 gacutil.exe와 regsvcs.exe를 이용해서 시스템에 등록해 줍니다. 등록과 관련해서 이전에 쭉 정리를 했었지요. ^^
gacutil.exe로 어셈블리 등록 시 시스템 변경 사항
; https://www.sysnet.pe.kr/2/0/1285
regasm.exe로 어셈블리 등록 시 시스템 변경 사항 (1) - .NET 2.0 + x86/x64/AnyCPU
; https://www.sysnet.pe.kr/2/0/1286
regasm.exe로 어셈블리 등록 시 시스템 변경 사항 (2) - .NET 4.0 + .NET 2.0
; https://www.sysnet.pe.kr/2/0/1287
regasm.exe로 어셈블리 등록 시 시스템 변경 사항 (3) - Type Library
; https://www.sysnet.pe.kr/2/0/1288
regsvcs.exe로 어셈블리 등록 시 시스템 변경 사항
; https://www.sysnet.pe.kr/2/0/1289
그렇습니다. 위의 글들을 쓰게 된 동기는... ^^ 바로 이 글을 쓰다가 등록 과정을 정리해야 할 필요가 있겠다 싶은 생각이 든 때문이었습니다. 위의 글 이상으로 자세히 소개할 수 없으니 여기서는 그냥 아래와 같이 등록했다고만 이야기 하고 지나가겠습니다. ^^
D:\temp\net20\AnyCPU>"..\..\net20\gacutil.exe" /i ComBaseClass.dll
Microsoft (R) .NET Global Assembly Cache Utility. Version 3.5.30729.1
Copyright (c) Microsoft Corporation. All rights reserved.
Assembly successfully added to the cache
D:\temp\net20\AnyCPU>"..\..\net20\gacutil.exe" /i ClassLibraryAsAdmin.dll
Microsoft (R) .NET Global Assembly Cache Utility. Version 3.5.30729.1
Copyright (c) Microsoft Corporation. All rights reserved.
Assembly successfully added to the cache
D:\temp\net20\AnyCPU>C:\WINDOWS\Microsoft.NET\Framework64\v2.0.50727\regsvcs.exe ClassLibraryAsAdmin.dll
Microsoft (R) .NET Framework Services Installation Utility Version 2.0.50727.3053
Copyright (c) Microsoft Corporation. All rights reserved.
Auto exporting 'ComBaseClass, Version=1.0.0.0, Culture=neutral, PublicKeyToken=c91e971f6240da9f' as 'C:\WINDOWS\assembly\GAC_MSIL\ComBaseClass\1.0.0.0__c91e971f6240da9f\ComBaseClass.tlb'.
Installed Assembly:
Assembly: D:\temp\net20\AnyCPU\ClassLibraryAsAdmin.dll
Application: ClassLibraryAsAdmin
TypeLib: D:\temp\net20\AnyCPU\ClassLibraryAsAdmin.tlb
참고로, 쓸모없을 것 같다고 해서 Type Library 파일을 지우면 다음과 같은 예외가 발생할 수 있습니다. ^^
System.InvalidCastException was unhandled
HResult=-2147467262
Message=Unable to cast COM object of type 'System.__ComObject' to interface type 'ComBaseClass.IAdminCode'. This operation failed because the QueryInterface call on the COM component for the interface with IID '{23172F2F-A3D3-4180-97AE-7805F74A5A46}' failed due to the following error: Error loading type library/DLL. (Exception from HRESULT: 0x80029C4A (TYPE_E_CANTLOADLIBRARY)).
Source=mscorlib
StackTrace:
Server stack trace:
at System.StubHelpers.StubHelpers.GetCOMIPFromRCW(Object objSrc, IntPtr pCPCMD, IntPtr& ppTarget, Boolean& pfNeedsRelease)
at ComBaseClass.IAdminCode.SetRegistryValue()
at System.Runtime.Remoting.Messaging.StackBuilderSink._PrivateProcessMessage(IntPtr md, Object[] args, Object server, Object[]& outArgs)
at System.Runtime.Remoting.Messaging.StackBuilderSink.SyncProcessMessage(IMessage msg)
Exception rethrown at [0]:
at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
at ComBaseClass.IAdminCode.SetRegistryValue()
at ConsoleApplication1.Program.Main(String[] args) in D:\...[생략]...\Program.cs:line 15
InnerException:
여기까지 진행했으면 Component Services 관리 콘솔에 다음과 같이 등록된 것을 확인할 수 있습니다.
"관리자 권한"을 필요로 하기 때문에 해당 COM+ 서버의 구동 계정을 "Local SYSTEM"(또는, 그 외의 관리자 계정)으로 변경해 줄 필요가 있습니다. 그런데, 다음과 같이 "Identity" 탭의 "Local System" 계정이 비활성화되어 있는 것을 볼 수 있습니다.
왜냐하면, COM+ 서버에 대해 Local System 권한을 주는 것은 NT 서비스를 통해서만 가능하도록 바뀌었기 때문입니다. 그래서, "Activation" 탭을 이용하여 명시적으로 "Run application as NT Service" 옵션을 설정해 주어야 합니다.
이렇게 바꿔준 후 다시 "Identity" 탭으로 가면 "Local System" 옵션이 활성화되어 있어 설정이 가능합니다.
변경 사항을 적용해 주면, 이제 "서비스 관리자"에서 다음과 같이 NT 서비스로 등록된 것을 확인할 수 있습니다.
이것으로 설정 작업은 모두 끝났습니다.
마지막으로, 이를 직접 사용하는 C# 콘솔 응용 프로그램을 만들어서 다음과 같이 COM+로 등록된 개체를 동적으로 생성할 수 있습니다.
using System;
using ComBaseClass;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Guid guid = new Guid("{23172f2f-a3d3-4180-97ae-7805f74a5a46}");
Type type = Type.GetTypeFromCLSID(guid);
IAdminCode adminCode = Activator.CreateInstance(type) as IAdminCode;
adminCode.SetRegistryValue();
}
}
}
여기까지, 실제로 COM+ 개체를 이용하여 '관리자 권한의 코드를 실행'하는 것이 가능한지 알아보았습니다.
모두 좋은데, 딱 한가지 마음에 들지 않는 것이 있다면 COM+ 관리 콘솔에서 해당 COM+ Application에 대해 수동으로 NT 서비스 등록을 해주어야 한다는 점입니다. 사내 시스템에 배포하는 프로그램일지라도 이런 식의 설정 작업을 해주어야 한다는 것은 현실성이 없어보입니다.
다행히, 이런 작업을 코딩으로 대체하는 것이 가능합니다. 이에 대해서는 아래의 글에서 자세히 나와 있습니다.
Set Identity to Local System for Com+ Application
; https://social.msdn.microsoft.com/Forums/en-US/df74247b-d107-41cf-bb10-404f976f7d2c/set-identity-to-local-system-for-com-application?forum=winformssetup
이를 위해서 우선 COM+ 관리 개체를 참조해야 하는데요.
관리의 편이성을 위해 아래의 방법을 사용해서 Interop DLL을 제거할 수 있으니 참고하십시오.
레지스트리 등록 및 Interop DLL 없이 COM 개체 사용하는 방법
; https://www.sysnet.pe.kr/2/0/1180
종합해 보면, 다음과 같은 단계를 거쳐서 COM+ 응용 프로그램을 생성 및 NT 서비스로 등록해 줄 수 있습니다.
string appName = "ClassLibraryAsAdmin";
string folderPath = Path.GetDirectoryName(typeof(Program).Assembly.Location);
string complusPath = Path.Combine(folderPath, appName + ".dll");
// COM+ Catalog 개체 생성
Guid guid = new Guid("{F618C514-DFB8-11D1-A2CF-00805FC79235}");
Type type = Type.GetTypeFromCLSID(guid);
object comObject = Activator.CreateInstance(type);
ICOMAdminCatalog2 catalog = comObject as ICOMAdminCatalog2;
ICatalogCollection apps = catalog.GetCollection("Applications") as ICatalogCollection;
apps.Populate();
// COM+ Application 생성
ICatalogObject catObject = apps.Add() as ICatalogObject;
catObject["Name"] = appName;
catObject["Activation"] = COMAdminActivationOptions.COMAdminActivationLocal;
catObject["ApplicationAccessChecksEnabled"] = false;
catObject["ShutdownAfter"] = 1;
catObject["Identity"] = @"nt authority\system";
apps.SaveChanges();
// COM+ Application 내에 구성 요소 추가
string dllPath = Path.ChangeExtension(modulePath, ".dll");
catalog.InstallComponent(appName, dllPath, string.Empty, string.Empty);
// "Local System" 권한의 NT 서비스로 등록
catalog.CreateServiceForApplication(appName, appName, "SERVICE_DEMAND_START", "SERVICE_ERROR_NORMAL",
string.Empty, @".\LOCALSYSTEM", null, false);
참고로, ICatalogObject의 속성에 어떤 값들을 집어넣어야 할 지는 다음의 문서를 보시면 됩니다.
Applications collection
; https://learn.microsoft.com/en-us/windows/win32/cossdk/applications
코드로 모두 작성해 보기는 했지만, 사실 대부분의 작업을 "regsvcs.exe"에서 해주기 때문에 복잡하게 할 것 없이 CreateServiceForApplication 메서드만 마지막에 호출하는 식으로 마무리하는 것이 좋습니다.
그래서 최종적으로 정리해 보면, 다음과 같은 등록 절차를 거치면 됩니다.
// GAC 등록
gacutil /i ClassLibraryAsAdmin.dll
gacutil /i ComBaseClass.dll
// 명시적인 regsvcs.exe 호출
C:\Windows\Microsoft.NET\Framework64\v2.0.50727\regsvcs ClassLibraryAsAdmin.dll
// CreateServiceForApplication를 호출해서 NT 서비스로 등록해주는 사용자 정의 exe 파일 실행
ComplusInstaller.exe
등록 과정이 다소 복잡해지는 문제가 있지만, 일단 이렇게 구성해 두면 이후부터는 '관리자 권한의 코드'를 실행하는 데 '사용자 동의' 창을 띄우는 불편함은 없어지니 충분한 가치가 있습니다.
마지막으로... 한가지 더 정리해야 할 것이 있는데요. 그런데, 굳이 기존의 "NT 서비스로 관리 코드를 실행하는 방법"을 쓰지 않고 COM+로 등록해 주는 것이 더 편한 이유가 있을까요?
개인적인 생각으로 2가지 정도를 꼽아보았습니다.
- 보다 편리한 통신: NT 서비스 만으로 구성한 경우, 관리 코드를 실행하기 위해 별도로 소켓이나 WCF를 열어두어야 하는데 그런 작업이 제거됩니다.
- 인스턴스 관리 용이: NT 서비스 만으로 구성한 경우, Start/Stop 등에 대한 관리를 해주어야 하는데 COM+로 해주면 개체 생성과 함께 NT 서비스가 자동 Start 되고 일정 시간의 Idle 시간이 흐른 이후 자동으로 Stop 상태로 전환됩니다.
첨부된 파일은 위에서 설명한 "ComplusInstaller" 코드까지 모두 포함한 테스트 프로젝트입니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]