Доступ к совместному файлу (UNC) из удаленного, недоверенного домена с учетными данными

Мы столкнулись с интересной ситуацией, которая нуждается в решении, и мои поиски оказались нил. Поэтому я обращаюсь к сообществу SO за помощью.

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

Как правило, один разрешает эту проблему одним из двух способов:

  • Сопоставьте общий доступ к файлу в качестве диска и поставьте учетные данные в это время. Обычно это делается с помощью команды NET USE или функций Win32, которые дублируют NET USE.
  • Доступ к файлу с UNC-путём, как если бы удаленный компьютер находился в домене, и убедитесь, что учетная запись, под которой выполняется программа, дублируется (включая пароль) на удаленном компьютере в качестве локального пользователя. В основном используется тот факт, что Windows автоматически будет предоставлять текущие учетные данные пользователя, когда пользователь попытается получить доступ к общему файлу.
  • Не используйте удаленный обмен файлами. Используйте FTP (или некоторые другие средства), чтобы перенести файл, работать с ним локально, а затем перенести его обратно.

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

Они открыты для третьего варианта, но администраторы удаленной сети настаивают на SFTP, а не на FTPS, а FtpWebRequest поддерживает только FTPS. SFTP больше подходит для брандмауэра и есть несколько библиотек, которые я мог бы использовать для этого подхода, но я бы предпочел уменьшить свои зависимости, если смогу.

Я искал MSDN для управляемого или win32 средства использования удаленного совместного использования файлов, но я не смог придумать ничего полезного.

И поэтому я спрашиваю: есть ли другой способ? Я пропустил сверхсекретную функцию win32, которая делает то, что я хочу? Или я должен использовать некоторый вариант варианта 3?

Ответ 1

Способом решения вашей проблемы является использование API Win32 под названием WNetUseConnection.
Используйте эту функцию для подключения к пути UNC с проверкой подлинности, а не для сопоставления диска.

Это позволит вам подключиться к удаленному компьютеру, даже если он не находится в одном домене, и даже если он имеет другое имя пользователя и пароль.

После использования WNetUseConnection вы сможете получить доступ к файлу через UNC-путь, как если бы вы были в том же домене. Лучший способ, вероятно, состоит в создании административных пакетов. Пример:\\имя_компьютера\c $\ program files\Folder\file.txt

Вот пример кода С#, который использует WNetUseConnection.

Обратите внимание: для NetResource вы должны передать значение null для lpLocalName и lpProvider. DwType должен быть RESOURCETYPE_DISK. LpRemoteName должно быть\\имя_компьютера.

Ответ 2

Для людей, которые ищут быстрое решение, вы можете использовать NetworkShareAccesser, который я написал недавно (на основе этого ответа (большое спасибо!)):

Применение:

using (NetworkShareAccesser.Access(REMOTE_COMPUTER_NAME, DOMAIN, USER_NAME, PASSWORD))
{
    File.Copy(@"C:\Some\File\To\copy.txt", @"\\REMOTE-COMPUTER\My\Shared\Target\file.txt");
}

ПРЕДУПРЕЖДЕНИЕ: Пожалуйста, убедитесь, что Dispose из NetworkShareAccesser вызывается (даже если приложение отключается!), в противном случае открытое соединение останется в Windows. Вы можете увидеть все открытые соединения, открыв приглашение cmd и введите net use.

Код:

/// <summary>
/// Provides access to a network share.
/// </summary>
public class NetworkShareAccesser : IDisposable
{
    private string _remoteUncName;
    private string _remoteComputerName;

    public string RemoteComputerName
    {
        get
        {
            return this._remoteComputerName;
        }
        set
        {
            this._remoteComputerName = value;
            this._remoteUncName = @"\\" + this._remoteComputerName;
        }
    }

    public string UserName
    {
        get;
        set;
    }
    public string Password
    {
        get;
        set;
    }

    #region Consts

    private const int RESOURCE_CONNECTED = 0x00000001;
    private const int RESOURCE_GLOBALNET = 0x00000002;
    private const int RESOURCE_REMEMBERED = 0x00000003;

    private const int RESOURCETYPE_ANY = 0x00000000;
    private const int RESOURCETYPE_DISK = 0x00000001;
    private const int RESOURCETYPE_PRINT = 0x00000002;

    private const int RESOURCEDISPLAYTYPE_GENERIC = 0x00000000;
    private const int RESOURCEDISPLAYTYPE_DOMAIN = 0x00000001;
    private const int RESOURCEDISPLAYTYPE_SERVER = 0x00000002;
    private const int RESOURCEDISPLAYTYPE_SHARE = 0x00000003;
    private const int RESOURCEDISPLAYTYPE_FILE = 0x00000004;
    private const int RESOURCEDISPLAYTYPE_GROUP = 0x00000005;

    private const int RESOURCEUSAGE_CONNECTABLE = 0x00000001;
    private const int RESOURCEUSAGE_CONTAINER = 0x00000002;


    private const int CONNECT_INTERACTIVE = 0x00000008;
    private const int CONNECT_PROMPT = 0x00000010;
    private const int CONNECT_REDIRECT = 0x00000080;
    private const int CONNECT_UPDATE_PROFILE = 0x00000001;
    private const int CONNECT_COMMANDLINE = 0x00000800;
    private const int CONNECT_CMD_SAVECRED = 0x00001000;

    private const int CONNECT_LOCALDRIVE = 0x00000100;

    #endregion

    #region Errors

    private const int NO_ERROR = 0;

    private const int ERROR_ACCESS_DENIED = 5;
    private const int ERROR_ALREADY_ASSIGNED = 85;
    private const int ERROR_BAD_DEVICE = 1200;
    private const int ERROR_BAD_NET_NAME = 67;
    private const int ERROR_BAD_PROVIDER = 1204;
    private const int ERROR_CANCELLED = 1223;
    private const int ERROR_EXTENDED_ERROR = 1208;
    private const int ERROR_INVALID_ADDRESS = 487;
    private const int ERROR_INVALID_PARAMETER = 87;
    private const int ERROR_INVALID_PASSWORD = 1216;
    private const int ERROR_MORE_DATA = 234;
    private const int ERROR_NO_MORE_ITEMS = 259;
    private const int ERROR_NO_NET_OR_BAD_PATH = 1203;
    private const int ERROR_NO_NETWORK = 1222;

    private const int ERROR_BAD_PROFILE = 1206;
    private const int ERROR_CANNOT_OPEN_PROFILE = 1205;
    private const int ERROR_DEVICE_IN_USE = 2404;
    private const int ERROR_NOT_CONNECTED = 2250;
    private const int ERROR_OPEN_FILES = 2401;

    #endregion

    #region PInvoke Signatures

    [DllImport("Mpr.dll")]
    private static extern int WNetUseConnection(
        IntPtr hwndOwner,
        NETRESOURCE lpNetResource,
        string lpPassword,
        string lpUserID,
        int dwFlags,
        string lpAccessName,
        string lpBufferSize,
        string lpResult
        );

    [DllImport("Mpr.dll")]
    private static extern int WNetCancelConnection2(
        string lpName,
        int dwFlags,
        bool fForce
        );

    [StructLayout(LayoutKind.Sequential)]
    private class NETRESOURCE
    {
        public int dwScope = 0;
        public int dwType = 0;
        public int dwDisplayType = 0;
        public int dwUsage = 0;
        public string lpLocalName = "";
        public string lpRemoteName = "";
        public string lpComment = "";
        public string lpProvider = "";
    }

    #endregion

    /// <summary>
    /// Creates a NetworkShareAccesser for the given computer name. The user will be promted to enter credentials
    /// </summary>
    /// <param name="remoteComputerName"></param>
    /// <returns></returns>
    public static NetworkShareAccesser Access(string remoteComputerName)
    {
        return new NetworkShareAccesser(remoteComputerName);
    }

    /// <summary>
    /// Creates a NetworkShareAccesser for the given computer name using the given domain/computer name, username and password
    /// </summary>
    /// <param name="remoteComputerName"></param>
    /// <param name="domainOrComuterName"></param>
    /// <param name="userName"></param>
    /// <param name="password"></param>
    public static NetworkShareAccesser Access(string remoteComputerName, string domainOrComuterName, string userName, string password)
    {
        return new NetworkShareAccesser(remoteComputerName,
                                        domainOrComuterName + @"\" + userName,
                                        password);
    }

    /// <summary>
    /// Creates a NetworkShareAccesser for the given computer name using the given username (format: domainOrComputername\Username) and password
    /// </summary>
    /// <param name="remoteComputerName"></param>
    /// <param name="userName"></param>
    /// <param name="password"></param>
    public static NetworkShareAccesser Access(string remoteComputerName, string userName, string password)
    {
        return new NetworkShareAccesser(remoteComputerName, 
                                        userName,
                                        password);
    }

    private NetworkShareAccesser(string remoteComputerName)
    {
        RemoteComputerName = remoteComputerName;               

        this.ConnectToShare(this._remoteUncName, null, null, true);
    }

    private NetworkShareAccesser(string remoteComputerName, string userName, string password)
    {
        RemoteComputerName = remoteComputerName;
        UserName = userName;
        Password = password;

        this.ConnectToShare(this._remoteUncName, this.UserName, this.Password, false);
    }

    private void ConnectToShare(string remoteUnc, string username, string password, bool promptUser)
    {
        NETRESOURCE nr = new NETRESOURCE
        {
            dwType = RESOURCETYPE_DISK,
            lpRemoteName = remoteUnc
        };

        int result;
        if (promptUser)
        {
            result = WNetUseConnection(IntPtr.Zero, nr, "", "", CONNECT_INTERACTIVE | CONNECT_PROMPT, null, null, null);
        }
        else
        {
            result = WNetUseConnection(IntPtr.Zero, nr, password, username, 0, null, null, null);
        }

        if (result != NO_ERROR)
        {
            throw new Win32Exception(result);
        }
    }

    private void DisconnectFromShare(string remoteUnc)
    {
        int result = WNetCancelConnection2(remoteUnc, CONNECT_UPDATE_PROFILE, false);
        if (result != NO_ERROR)
        {
            throw new Win32Exception(result);
        }
    }

    /// <summary>
    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
    /// </summary>
    /// <filterpriority>2</filterpriority>
    public void Dispose()
    {
        this.DisconnectFromShare(this._remoteUncName);
    }
}

Ответ 3

AFAIK вам не нужно сопоставить путь UNC к букве диска, чтобы установить учетные данные для сервера. Я регулярно использовал пакетные скрипты вроде:

net use \\myserver /user:username password

:: do something with \\myserver\the\file\i\want.xml

net use /delete \\my.server.com

Однако любая программа, работающая на той же учетной записи, что и ваша программа, все равно сможет получить доступ ко всему, к чему имеет доступ username:password. Возможным решением может быть выделение вашей программы в ее собственной учетной записи локального пользователя (доступ UNC является локальным для учетной записи, которая называется NET USE).

Примечание. Использование SMB через домены не очень хорошо использует технологию IMO. Если безопасность важна, то тот факт, что SMB не хватает шифрования, является, по сути, немного демпфером.

Ответ 4

Пока я не знаю себя, я определенно надеюсь, что № 2 неверно... Я бы хотел подумать, что Windows не собирается автоматически указывать мою регистрационную информацию (в последнюю очередь мой пароль!) на любую машину, не говоря уже о том, что не является частью моего доверия.

Независимо от того, изучили ли вы архитектуру олицетворения? Ваш код будет выглядеть примерно так:

using (System.Security.Principal.WindowsImpersonationContext context = System.Security.Principal.WindowsIdentity.Impersonate(token))
{
    // Do network operations here

    context.Undo();
}

В этом случае переменная token является IntPtr. Чтобы получить значение для этой переменной, вам придется вызывать неуправляемую функцию API Windows LogonUser. Быстрый переход на pinvoke.net дает нам следующую подпись:

[System.Runtime.InteropServices.DllImport("advapi32.dll", SetLastError = true)]
public static extern bool LogonUser(
    string lpszUsername,
    string lpszDomain,
    string lpszPassword,
    int dwLogonType,
    int dwLogonProvider,
    out IntPtr phToken
);

Имя пользователя, домен и пароль должны казаться довольно очевидными. Посмотрите на различные значения, которые могут быть переданы dwLogonType и dwLogonProvider, чтобы определить тот, который наилучшим образом соответствует вашим потребностям.

Этот код не был протестирован, так как у меня нет второго домена здесь, где я могу проверить, но это, надеюсь, поставит вас на правильный путь.

Ответ 5

Вместо WNetUseConnection я бы рекомендовал NetUseAdd. WNetUseConnection - это устаревшая функция, которая была заменена WNetUseConnection2 и WNetUseConnection3, но все эти функции создают сетевое устройство, которое видно в проводнике Windows. NetUseAdd является эквивалентом вызова сетевого использования в приглашении DOS для аутентификации на удаленном компьютере.

Если вы вызываете NetUseAdd, последующие попытки доступа к каталогу должны быть успешными.

Ответ 6

Большинство SFTP-серверов поддерживают SCP, что может быть намного проще для поиска библиотек. Вы даже можете просто вызвать существующего клиента из своего кода, например, pscp, включенного в PuTTY.

Если тип файла, с которым вы работаете, является чем-то простым, как текстовый или XML файл, вы даже можете зайти так далеко, чтобы написать свою собственную реализацию клиент/сервер, чтобы манипулировать файлом, используя что-то вроде .NET Remoting или web услуги.

Ответ 7

Я видел вариант 3, реализованный с помощью инструментов JScape довольно простым способом. Вы можете попробовать. Он не бесплатный, но он выполняет свою работу.

Ответ 8

Я посмотрел на MS, чтобы найти ответы. Первое решение предполагает, что учетная запись пользователя, выполняющая процесс приложения, имеет доступ к общей папке или диску (тот же домен). Убедитесь, что ваш DNS разрешен или попробуйте использовать IP-адрес. Просто выполните следующие действия:

 DirectoryInfo di = new DirectoryInfo(PATH);
 var files = di.EnumerateFiles("*.*", SearchOption.AllDirectories);

Если вы хотите, чтобы в разных доменах .NET 2.0 с учетными данными соответствовали этой модели:

WebRequest req = FileWebRequest.Create(new Uri(@"\\<server Name>\Dir\test.txt"));

        req.Credentials = new NetworkCredential(@"<Domain>\<User>", "<Password>");
        req.PreAuthenticate = true;

        WebResponse d = req.GetResponse();
        FileStream fs = File.Create("test.txt");

        // here you can check that the cast was successful if you want. 
        fs = d.GetResponseStream() as FileStream;
        fs.Close();