Исполнение куска кода от имени конкретного пользователя

    Постановка задачи



    Представим следующую ситуацию, есть каталог A, к которому имеет доступ только пользователь userA, есть каталог B, соответственно доступ к нему имеет только пользователь userB, требуется скопировать файлы из каталога A в каталог B, учитывая что программа может быть запущена пользователями userC, userD, userF,… и т.д.

    Набросок решения



    В качестве одного из способов решения можно использовать следующую схему:
    программа запускается от имени любого пользователя, затем авторизуется под пользователем userA, читает файл в память, возвращается к начальному пользователю, авторизуется под пользователем userB, пишет файл в конечный каталог, возвращается к начальному пользователю. При больших размерах файлов можно придумать разбиение на блоки, но это уже не относится к данной задаче.


    Решение



    Понадобится три несложных класса(на самом деле достаточно и первого).

    
      [SecurityPermission(SecurityAction.InheritanceDemand, UnmanagedCode = true)]
      [SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
      internal class MySafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid
      {
        private MySafeTokenHandle()
          : base(true)
        {
        }
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
        override protected bool ReleaseHandle()
        {
          return NativeMethods.CloseHandle(handle);
        }
      }
    
      [SuppressUnmanagedCodeSecurity()]
      internal static class NativeMethods
      {
        #region P/Invoke
        internal const int LOGON32_LOGON_INTERACTIVE = unchecked((int)2);
        internal const int LOGON32_PROVIDER_DEFAULT = unchecked((int)0);
    
        [DllImport("advapi32.dll")]
        internal static extern int LogonUserA(String lpszUserName,
            String lpszDomain,
            String lpszPassword,
            int dwLogonType,
            int dwLogonProvider,
            out MySafeTokenHandle phToken);
        [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        internal static extern int DuplicateToken(MySafeTokenHandle hToken,
            int impersonationLevel,
            out MySafeTokenHandle hNewToken);
    
        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
        internal static extern bool CloseHandle(IntPtr handle);
        #endregion
      }
      /// <summary>
      /// Класс реализует перевод исполнения программы на права указанного пользователя
      /// </summary>
      [SecurityPermissionAttribute(SecurityAction.InheritanceDemand, UnmanagedCode = true)]
      public class Impersonation : IDisposable
      {
        WindowsImpersonationContext impersonationContext;
        /// <summary>
        /// Вход от имени указанного пользователя
        /// </summary>
        /// <param name="userName">Имя пользователя</param>
        /// <param name="domain">Домен</param>
        /// <param name="password">Пароль</param>
        /// <returns>false - если не удалось выполнить вход по указанным данным</returns>
        public void ImpersonateUser(String userName, String domain, String password)
        {
          WindowsIdentity tempWindowsIdentity;
          MySafeTokenHandle token;
          MySafeTokenHandle tokenDuplicate;
          if (NativeMethods.LogonUserA(userName, domain, password, NativeMethods.LOGON32_LOGON_INTERACTIVE, NativeMethods.LOGON32_PROVIDER_DEFAULT, out token) != 0)
          {
            using (token)
            {
              if (NativeMethods.DuplicateToken(token, 2, out tokenDuplicate) != 0)
              {
                using (tokenDuplicate)
                {
                  if (!tokenDuplicate.IsInvalid)
                  {
                    tempWindowsIdentity = new WindowsIdentity(tokenDuplicate.DangerousGetHandle());
                    impersonationContext = tempWindowsIdentity.Impersonate();
                    return;
                  }
                }
              }
            }
          }
          else
            throw new Exception("LogonUser failed: " + Marshal.GetLastWin32Error().ToString());
        }
        /// <summary>
        /// При освобождении рессурсов вернем предыдущего пользователя
        /// </summary>
        public void Dispose()
        {
          impersonationContext.Undo();
          GC.SuppressFinalize(this);
        }
      }
    


    Второй класс отвечающий за исполнение кода:
    
      /// <summary>
      /// Реализует исполнение указанного кода от имени конкретного пользователя
      /// </summary>
      public class ImpersonationExecutor
      {
        public string USR { get; set; }
        public string DOMAIN { get; set; }
        public string PWD { get; set; }
        /// <summary>
        /// Конструктор
        /// </summary>
        /// <param name="userName">Имя пользователя</param>
        /// <param name="domainName">Домен</param>
        /// <param name="password">Пароль</param>
        public ImpersonationExecutor(string userName, string domainName, string password)
        {
          USR = userName;
          DOMAIN = domainName;
          PWD = password;
        }
        /// <summary>
        /// Исполнение кода
        /// </summary>
        /// <typeparam name="T">Тип передаваемого аргумента</typeparam>
        /// <param name="action">Делегат</param>
        /// <param name="arg">Аргумент</param>
        public void ExecuteCode<T>(Action<T> action, T arg)
        {
          using (Impersonation user = new Impersonation())
          {
            user.ImpersonateUser(USR, DOMAIN, PWD);
            action(arg);
          }
        }
      }
    


    И наконец, очень маленькая фабрика:
    
    public class ImpersonationFactory
      {
        public static ImpersonationExecutor Create(string UserName, string Domain, string Password)
        {
          return new ImpersonationExecutor(UserName, Domain, Password);
        }
      }
    


    Пример использования


    В качестве примера решим задачу поставленную в постановке.
    
    string from = "from";
    string to = "to";
    byte[] buffer = null;
    ImpersonationFactory.Create("userA", "domainA", "passwordA").ExecuteCode<string>(delegate(string path) { buffer = File.ReadAllBytes(path); }, from);
    ImpersonationFactory.Create("userB", "domainB", "passwordB").ExecuteCode<string>(delegate(string path) { File.WriteAllBytes(path, buffer); }, to);
    


    Итог


    Задача решена. Как вариант можно было использовать разные приложения(службы/сервисы) работающие под разными пользователями, но гибкость потеряется. Буду рад увидеть другие способы.
    Поиском по хабру этого конкретного решения не видел.

    UPD: исправлен класс Impersonation, по комментарию хабрапользователя bobermaniac
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 11

      +13
      msdn.microsoft.com/en-us/library/system.security.principal.windowsimpersonationcontext.undo.aspx

      msdn.microsoft.com/en-us/library/system.runtime.interopservices.safehandle.aspx

      Это то, что лучше использовать вместо интеропов на CloseHandle и RevertToSelf.

      return false в функциях, которые могут провалиться по множеству причин — это очень плохой дизайн. Лучше бросать исключения.

      Почему LogonUserA, а не LogonUserW?

      Если в процессе получения или дублирования токена произойдет асинхронное исключение — то у вас хендлы потеряются. Используйте блок try… finally. А еще лучше, если работаете с нативными хендлами — изучите принципы работы CER или работайте с обертками.
        +2
        Большое спасибо за качественный комментарий, большая редкость на хабре. Подправил по материалу из ссылок.

        LogonUserA, а не LogonUserW потому что использую pinvoke.net при работе с WinAPI, и LogonUserW там нет.
          +1
          В таком случае, рекомендую разобраться в смысле совершенных вами действий. Работа с unmanaged-кодом требует определенной четкости выполния и понимания сути совершаемых телодвижений.

          В частности, как же выполняется p/invoke и как настраивать маршалер.
            0
            Если чего-то нет в pinvoke.net — самое время его туда добавить) В pinvoke оно все не само по себе взялось…
            • НЛО прилетело и опубликовало эту надпись здесь
              • НЛО прилетело и опубликовало эту надпись здесь
              +2
              взято отсюда:
              www.codeproject.com/KB/cs/zetaimpersonator.aspx
              потом ухудшено.
                +1
                Вы немного напутали с типами в примерах кода:

                public void ImpersonateUser(...)

                Метод типа void, но потом вы делаете

                if (user.ImpersonateUser(USR, DOMAIN, PWD))
                  0
                  Да, спасибо, после исправления забыл второй клас выложить.
                  0
                  А можно ли таким образом вывести во время работы программы диалог UAC и повысить таким образом привилегии программы, если она была запущена без администраторских прав?
                  • НЛО прилетело и опубликовало эту надпись здесь

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое