Previous Page Page 2 of 4 in the PowerShellACL category Next Page

Как-то давно Александр Станкевич просил у меня вариант скрипта, который бы менял владельца файла или папки из PowerShell. В своё время я занимался этим вопросом и результат моих исследований:

Что-то меня натолкнуло снова вернуться к этому вопросу. Учитывая проблематику, изложенных в предыдущих статьях, я перестал искать нативный способ изменения владельца в PowerShell через .NET и решил поискать его в WMI (что означает очередные мучения многострадального SecurityDescriptor :'( ). Итак, у WMI есть несколько классов для работы с ACL (AccessControlList) файлов и папок. Например:

Я для решения данной задачи решил использовать класс Win32_LogicalFileSecuritySetting (хотя, можно и Win32_Directory использовать но после мелкой доработки. Но об этом я выскажусь в конце статьи).

Итак, класс Win32_LogicalFileSecuritySetting имеет те же методы, что и остальные классы, работающие со списками ACL - GetSecurityDescriptor и SetSecurityDescriptor. Я уже неоднократно поднимал вопрос работы с SecurityDescriptor в PowerShell, поэтому приступим сразу к решению задачи.

SecurityDescriptor Structure

Как видно из картинки, нас будет интересовать объект Owner и ControlFlags. Объект DACL нас не будет интересовать совсем, поэтому работать с Win32_Ace нам не придётся, а только с Trustee:

$SD = ([WMIClass] "Win32_SecurityDescriptor").CreateInstance()
$Trustee = ([WMIClass] "Win32_Trustee").CreateInstance()

Далее следует стандартная процедура преобразования имени пользователя в SID и получение байтового массива из SID'а:

$SID = (new-object security.principal.ntaccount $user).translate([security.principal.securityidentifier])
[byte[]] $SIDArray = ,0 * $SID.BinaryLength
$SID.GetBinaryForm($SIDArray,0)

Теперь $SIDArray и имя пользователя запишем в Trustee и поместим этот объект в свойство Owner дескриптора безопасности:

$Trustee.Name = $user
$Trustee.SID = $SIDArray
$SD.Owner = $Trustee

Для того, чтобы заменить владельца папки нужно заполнить объект Control Flags, которые описаны здесь: http://msdn.microsoft.com/en-us/library/aa394402(VS.85).aspx. Не уверен, что стоит углубляться в этот момент (на практике очень редко приходится им пользоваться), поэтому скажу, что нас заинтересует флаг SE_SELF_RELATIVE. Заполняется он одной строчкой:

$SD.ControlFlags="0x8000"

Его можно записывать как десятичное число (32768), так и в HEX нотации. Я использую HEX. Вот и всё, дескриптор безопасности у нас готов. Теперь самое время получить ACL в формате SecurityDescriptor из имеющейся папки:

$wPrivilege = gwmi Win32_LogicalFileSecuritySetting -filter "path='$path'"

Вот теперь мы вплотную подошли к нашей проблеме. К слову говоря, если этот скрипт использовать в Windows Vista/Windows Server 2008 с повышенными привилегиями (запустив консоль в привилегированном режиме), то можно добавлять последнюю строчку с записью нового владельца в папку. В системах, где есть UAC нету такой проблемы, которая описана в ссылках, которые приведены в начале поста, поскольку при запуске консоли с повышенными привилегиями UAC включает для нас все необходимые привилегии (в частности SeRestorePrivilege и SeTakeOwnershipPrivilege, которые необходимы для этой операции). Но в более ранних ОС при запуске консоли PowerShell эти права не включаются и их нужно включать отдельно. Если в .NET нету нативного метода включения этих привилегий, то в WMI они есть и вот они:

$wPrivilege.psbase.Scope.Options.EnablePrivileges = $true

В Options помимо включения привилегий можно указывать имперсонализацию пользователя (Impersonate) и другие параметры. Чтобы посмотреть доступные свойства достаточно набрать в консоли:

$wPrivilege.psbase.scope.options | Get-Member

Когда привилегии включены, можно уже записывать дескриптор в папку при помощи метода SetSecurityDescriptor:

$wPrivilege.setsecuritydescriptor($SD)

Если имя папки указано верно, то в выводе ReturnValue должен вернуть значение 0, что означает, что владелец сменён! Rock и мы небольшим (но для PowerShell'а это уже много, учитывая что многие вещи в нём делаются в одну строчку ;) ) увеличением объёма кода можем полноценно изменять владельца файла или папки без установки дополнительных расширений, как PSCX или отдельных консольных утилит, как SubInAcl или SetAcl.

Теперь это всё окультурим в готовый скрипт:

function Set-Owner ($user, $Path) {
    if (!(Test-Path -LiteralPath $Path)) {Write-Warning "Указан неверный путь к папке"}
    else {
        # преобразовываем путь вида C:\Folder в C:\\Folder (к слешу пути добавляем ещё один
        # для корректной работы класса Win32_LogicalFileSecuritySetting и эскейпим другие символы
        $path = $path -replace "\\|'",'\$0'
        $Path = $Path -replace '\[', "$([char]91)"
        $Path = $Path -replace '\]', "$([char]93)"
        # т.к. DACL мы не записываем, то объявляем только классы SecurityDescriptor и Trustee
        $SD = ([WMIClass] "Win32_SecurityDescriptor").CreateInstance()
        $Trustee = ([WMIClass] "Win32_Trustee").CreateInstance()
        # преобразовываем имя пользователя в SID и заполняем необходимые поля в Trustee
        $SID = (new-object security.principal.ntaccount $user).translate([security.principal.securityidentifier])
        [byte[]] $SIDArray = ,0 * $SID.BinaryLength
        $SID.GetBinaryForm($SIDArray,0)
        $Trustee.Name = $user
        $Trustee.SID = $SIDArray
        $SD.Owner = $Trustee
        # здесь мы добавляем флаг управления
        $SD.ControlFlags="0x8000"
        # выбираем сведения о безопасности необходимой папки
        $wPrivilege = gwmi Win32_LogicalFileSecuritySetting -filter "path='$path'"
        # включаем привилегия для WMI. Для Windows Vista/Windows Server 2008,
        # при запуске скрипта с повышенными привилегиями данная строка не обязательна
        $wPrivilege.psbase.Scope.Options.EnablePrivileges = $true
        # записываем SecurityDescriptor с новым владельцем в папку
        $Return = $wPrivilege.setsecuritydescriptor($SD)
        # преобразовываем возвращаемый код в текстовое значение
        switch ($Return.ReturnValue) {
            "0" {"Успешно"}
            "2" {Write-Warning "Отказано в доступе"}
            "8" {Write-Warning "Неизвестная ошибка"}
            "9" {Write-Warning "Отсутствуют привилегии"}
            "21" {Write-Warning "Указан неправильный параметр"}
            "1307" {Write-Warning "Указанный пользователь не может быть владельцем данного объекта"}
            default {Write-Warning "Произошла неизвестная ошибка с кодом:" $Return.Value}
        }
    }
}

# эта часть совсем необязательна, я её включил лишь для наглядности 
# и полноты скрипта 
function Get-Owner ($path) {(Get-Acl $path).owner}

Примечание: При указании пути, который содержит пробелы, путь нужно заключать в кавычки!

и немного о стандартности использования методов SetSecurityDescriptor для различных объектов. Мне не понятно, почему в различных классах WMI используются различные именования свойств дескриптора безопасности, когда в этом явных причин нету? Например:

  • Win32_Share для дескриптора использует свойство Access метода SetShareInfo
  • Win32_Printer использует свойство Descriptor метода SetSecurityDescriptor
  • Win32_Directory использует SecurityDescriptor метода SetSecurityDescriptor (и для SetSecurityDescrptorEx)
  • Win32_LogicalFileSecuritySetting использует Descriptor метода SetSecurityDescriptor

Вот 4 WMI класса управления ACL списками различных объектов, с которыми я недавно работал и имеем 3 различных именования свойства дескриптора безопасности и Win32_Share использует даже другое название метода (SetShareInfo), хотя этот метод использует тот же Win32_SecurityDescriptor. Но это уже оффтопик и личные размышления. Вот :)

PowerShell |  ACL |  WMI
Monday, November 17, 2008 2:19:53 AM (FLE Standard Time, UTC+02:00)   Comments [0]    

 

В первой и второй части я рассказал про основные моменты управления принтерами в PowerShell и теперь хочу поговорить о правах на принтеры. Т.к. принтеры управляются с помощью классов WMI, то управление правами доступа к ним будет превращаться в очередную эпохальную эпопею, которую я исследовал при изучении безопасности Share Permissions (вот ссылка на эти статьи в моём прежнем блоге: http://vpodans.spaces.live.com/lists/cns!BB1419A2CFC1E008!178). Однако, с принтерами оказалось всё печальней :'( Мне так и не удалось заставить работать метод SetSecurityDescriptor. Итак, я расскажу о своих кратких исследованиях и в чём же мы имеем проблему.

Для чтения прав доступа принтера потребуется метод GetSecurityDescriptor:

[System32] $a=(gwmi win32_printer -filter "name='cutepdf writer'").getsecuritydescriptor() [System32] $a __GENUS : 2 __CLASS : __PARAMETERS __SUPERCLASS : __DYNASTY : __PARAMETERS __RELPATH : __PROPERTY_COUNT : 2 __DERIVATION : {} __SERVER : __NAMESPACE : __PATH : Descriptor : System.Management.ManagementBaseObject ReturnValue : 0 [System32] $a.Descriptor | fl [a-z]* ControlFlags : 32780 DACL : {System.Management.ManagementBaseObject, System.Management.ManagementBaseObject,System.Mana gement.ManagementBaseObject, System.Management.ManagementBaseObject...} Group : System.Management.ManagementBaseObject Owner : System.Management.ManagementBaseObject SACL : TIME_CREATED : [System32] $a.descriptor.dacl[0] | fl [a-z]* AccessMask : 983052 AceFlags : 0 AceType : 0 GuidInheritedObjectType : GuidObjectType : TIME_CREATED : Trustee : System.Management.ManagementBaseObject [System32] $a.descriptor.dacl[0].trustee | fl [a-z]* Domain : Thor Name : Admin SID : {1, 5, 0, 0...} SidLength : 28 SIDString : S-1-5-21-3020384060-3247076327-363933757-1000 TIME_CREATED : [System32]

Как видите, метод GetSecurityDescriptor вернул нам единственный параметр - Descriptor. Заглянув в Descriptor, нам нужно было найти параметр DACL, который уже содержит все пермишены. Подробнее материал изложен тут: WMI Security Descriptor Objects.

Примечание: методы GetSecurityDescriptor и SetSecurityDescriptor доступны только в ОС начиная от Windows Vista/Windows Server 2008 и выше.

Структура DACL содержит в себе как права доступа (AccessMask), так и сведения о пользователе, который имеет указаную AccessMask маску доступа (Trustee). Я попробовал создать идентичную структуру SecurityDescriptor (как описано в ссылке: WMI Security Descriptor Objects) и получил вот такой скрипт:

# задаём пользователя, которому хотим предоставить доступ
$user = "everyone"
# объявляем необходимые классы, которые описывают дескриптор безопасности
$SD = ([WMIClass] "Win32_SecurityDescriptor").CreateInstance()
$ace = ([WMIClass] "Win32_Ace").CreateInstance()
$Trustee = ([WMIClass] "Win32_Trustee").CreateInstance()
# преобразовываем имя пользователя в строковый SID и массив байтов для Win32_Trustee
$SID = (new-object security.principal.ntaccount $user).translate([security.principal.securityidentifier])
[byte[]] $SIDArray = ,0 * $SID.BinaryLength
$SID.GetBinaryForm($SIDArray,0)
# заполняем необходимыми данными класс Win32_Trustee
$Trustee.Name = $user
$Trustee.SID = $SIDArray
# заполняем поля класса Win32_Ace, которые описывают права доступа и заворачиваем
# пользователя в лице класса Win32_Trustee
# AccessMask в контексте принтера может принимать значения:
# 524288 - Take ownership
# 131072 - read permissions
# 262144 - change permissions
# 983088 - manage documents
# 983052 - manage printers
# 131080 - print + read permissions

$ace.AccessMask = 983052
$ace.AceType = 0
$ace.AceFlags = 0
$ace.Trustee = $Trustee
# заворачиваем полученный объект Win32_Ace в параметр DACL класса Win32_SecurityDescriptor
$SD.DACL = $ace
# получаем объект принтера, с которым собираемся работать
$Printer = gwmi win32_printer -filter "name='CutePDF Writer'"
# получаем свойства метода SetSecurityDescriptor, чтобы в соответствии с ними
# завернуть туда SecurityDescriptor
$inParams = $Printer.psbase.GetMethodParameters("SetSecurityDescriptor")
# заворачиваем SecurityDescriptor в параметр Descriptor метода SetSecurityDescriptor
$inParams.Descriptor = $SD
# применяем метод SetSecurityDescriptor
$Printer.SetSecurityDescriptor($inParams)

И попробуем его запустить:

[System32] $user = "everyone"
[System32] $SD = ([WMIClass] "Win32_SecurityDescriptor").CreateInstance()
[System32] $ace = ([WMIClass] "Win32_Ace").CreateInstance()
[System32] $Trustee = ([WMIClass] "Win32_Trustee").CreateInstance()
[System32] $SID = (new-object security.principal.ntaccount $user).translate([security.principal.securityidentifier])
[System32] [byte[]] $SIDArray = ,0 * $SID.BinaryLength[System32] $SID.GetBinaryForm($SIDArray,0)
[System32] $Trustee.Name = $user
[System32] $Trustee.SID = $SIDArray
[System32] $ace.AccessMask = 983052
[System32] $ace.AceType = 0
[System32] $ace.AceFlags = 0
[System32] $ace.Trustee = $Trustee
[System32] $SD.DACL = $ace
[System32] $Printer = gwmi win32_printer -filter "name='CutePDF Writer'"
[System32] $inParams = $Printer.psbase.GetMethodParameters("SetSecurityDescriptor")
[System32] $inParams.Descriptor = $SD
[System32] $Printer.SetSecurityDescriptor($inParams)   

__GENUS          : 2
__CLASS          : __PARAMETERS
__SUPERCLASS     :
__DYNASTY        : __PARAMETERS
__RELPATH        :
__PROPERTY_COUNT :
__DERIVATION     : {}
__SERVER         :
__NAMESPACE      :
__PATH           :
ReturnValue      : 2147749896


[System32]

Как видно, ни одна строка не вернула ошибок, а последняя строка вернула значение 2147749896. Прогулявшись по MSDN нашёл описание этой ошибки: Win32SDToSDDL Method of the Win32_SecurityDescriptorHelper Class (практически везде данное значение в контексте SecurityDescriptor интерпретируются так):

  • One of the parameters to the call is not correct.

один из параметров вызова указан неверно. Давайте попробуем проанализировать, что мы сформировали и сравним с данными, которые получили методом GetSecurityDescriptor:

[System32] $inparams 

__GENUS          : 2
__CLASS          : __PARAMETERS
__SUPERCLASS     :
__DYNASTY        : __PARAMETERS
__RELPATH        :
__PROPERTY_COUNT : 1
__DERIVATION     : {}
__SERVER         :
__NAMESPACE      :
__PATH           :
Descriptor       : System.Management.ManagementBaseObject


[System32] $inparams.Descriptor | fl [a-z]* ControlFlags :

DACL         : {System.Management.ManagementBaseObject}
Group        :
Owner        :
SACL         :
TIME_CREATED : 


[System32] $inparams.Descriptor.dacl[0] | fl [a-z]* 

AccessMask              : 983052
AceFlags                : 0
AceType                 : 0
GuidInheritedObjectType :
GuidObjectType          :
TIME_CREATED            :
Trustee                 : System.Management.ManagementBaseObject 


[System32] $inparams.Descriptor.dacl[0].trustee | fl [a-z]* 

Domain       :
Name         : everyone
SID          : {1, 1, 0, 0...}
SidLength    :
SIDString    :
TIME_CREATED : 
[System32]

Если сравнивать с первым листингом, где мы получали текущий SecurityDescriptor, то мы сохранили структуру SecurityDescriptor и в соответствующие поля записали нужные данные. При этом я также пытался для Trustee записать строковый SIDString и SIDLength:

$Trustee.SIDString = $SID.Value
$Trustee.SIDLength = $SID.BinaryLength

и получил уже такой Trustee:

[System32] $inparams.Descriptor.dacl[0].trustee | fl [a-z]* 

Domain       :
Name         : everyone
SID          : {1, 1, 0, 0...}
SidLength    : 12
SIDString    : S-1-1-0
TIME_CREATED : 


[System32]

От этого результат не изменился. Попробовал вызвать несколько иначе метод SetSecurityDescriptor:

$Printer.psbase.invokemethod("SetSecurityDescriptor", $inParams, $null)


и всё по старому. Я попытался нагуглить этот вопрос поисковой фразой "setsecuritydescriptor win32_printer powershell" (да-да, каюсь, я пользуюсь гуглем, как это ни прискорбно). И он мне выдал меньше одной страницы и 2 вменяемые ссылки, одна из которых ведёт на MSDN, а вторая на какой-то французский сайт (в котором я без переводчика плохо понимаю, а жаль) - http://powershell-scripting.com/index.php?option=com_joomlaboard&Itemid=76&func=view&id=2292&catid=6, но человек там творил что-то страшное и я понял, что там помощи не ждать.

Вобщем, этот вопрос пока остаётся открытым. Что ему не нравится, я пока не могу понять. Управление SecurityDescriptor в WMI - это не самая удачная модель управления безопасностью в WMI, но пока это почти единственный способ добиться результата - приходится с этим работать. Хотя, на первый взгляд может показаться, что там всё страшно и ужасно, но на самом деле, если разобрать предметно вопрос и поупражняться, всё оказывается вполне понятным, хоть и не совсем логичным и не всегда это хочет работать как положено :)

PowerShell |  ACL |  WMI
Friday, November 14, 2008 9:21:45 PM (FLE Standard Time, UTC+02:00)   Comments [0]    

 

Примечание: данный пост перепечатан в связи с закрытием бложиков на spaces.live.com, как имеющий какую-то ценность для автора и/или читателей.


Сегодня на форуме TechNet задали вопрос о том, как преобразовать числовое значение типа прав доступа к объекту в его текстовое значение (см. тут). В предыдущих постах, посвящённых управленю ACL из PowerShell я использовал этот приём, но не акцентировал на этом внимание. Поэтому я подумал, что пора поставить на этом вопросе жирную точку.

Итак, как я не раз писал ранее, для управления списком доступа к объектам (ACL) используются различные классы .NET, например:

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

[System.Security.AccessControl.FileSystemRights]1179817

где 1179817 - числовое значение, которое описывает тип доступа. В данном случае это число соответствует праву ReadAndExecute и Synchronize. Если ввести другое число, например 721343:

[vPodans] [System.Security.AccessControl.FileSystemRights]721343 Modify, TakeOwnership

то мы получим текстовое значение прав, а именно - Modify и TakeOwnership. Бывают случаи, когда не допускается указания прав в текстовом виде и требуется указание только в числовом виде. Обратное преобразование выполняется при помощи свойства Value__ :

[vPodans] [System.Security.AccessControl.FileSystemRights]721343 | gm TypeName: System.Security.AccessControl.FileSystemRights Name MemberType Definition ---- ---------- ---------- CompareTo Method System.Int32 CompareTo(Object target) Equals Method System.Boolean Equals(Object obj) GetHashCode Method System.Int32 GetHashCode() GetType Method System.Type GetType() GetTypeCode Method System.TypeCode GetTypeCode() ToString Method System.String ToString(), System.String ToString(String format, IFormatProvi.. value__ Property System.Int32 value__ {get;set;}

Это единственное свойство, которое хранится в данном объекте. Посмотрим, как это работает на практике:

[vPodans] ([System.Security.AccessControl.FileSystemRights]"FullControl").value__ 2032127

Вот так мы получили числовое значение права FullControl. По этой аналогии можно преобразовать типы доступа и к другим объектам, как реестр:

[vPodans] ([System.Security.AccessControl.RegistryRights]"FullControl").value__ 983103 [vPodans] [System.Security.AccessControl.RegistryRights]2 SetValue [vPodans]

Enjoy!

Sunday, October 26, 2008 2:49:00 PM (FLE Standard Time, UTC+02:00)   Comments [0]    

 

Примечание: данный пост перепечатан в связи с закрытием бложиков на spaces.live.com, как имеющий какую-то ценность для автора и/или читателей.

Примечание: существует обновлённый вариант этого поста с модернизированным скриптом: Управление безопасностью общих папок (сетевых шар) в PowerShell (часть 5)


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

  1. создание сетевой папки;
  2. удаление сетевой папки;
  3. получения перечня всех сетевых папок на сервере с выводом необходимой информации о них;
  4. установка ACL сетевой папки;
  5. добавление ACE к существующему ACL сетевой папки;
  6. удаление единичных ACE изи ACL сетевой папки;
  7. просмотр текущих списков ACL сетевой папки;
  8. экспорт всех сведений (включая списки ACL) сетевых папок в CSV файл;
  9. импорт всех сведений (включая списки ACL) сетевых папок из CSV файла.

Касательно последнего пункта, то хочу отметить, что импорт при отсутствии наличия папки для расшаривания создаст папку и расшрарит с данными из CSV файла. Сначала я приведу список команд, которые доступны при использовании скрипта и их синтаксис:

  1. New-Share Name Path Description
    где Name - сетевое имя для папки;
    Path - путь к физической папке;
    Description описание к сетевой папке. При наличии пробелов -  заключить в кавычки (не обязательный параметр);
  2. Remove-Share Name
    где Name - сетевое имя папки;
  3. Get-Share Name
    где Name - имя сетевой папки (не обязательный параметр);
  4. Set-SharePermission Name User AccessMask AceType
    где Name - имя сетевой папки;
    User - имя пользователя/группы, которой предоставляется доступ;
    AccessMask - маска доступа. Этот параметр должен иметь одно из значений FullControl/Change/Read;
    AceType - тип доступа. Этот параметр должен иметь одно из значений Allow/Deny;
  5. Add-SharePermission Name User AccessMask AceType
    где Name - имя сетевой папки;
    User - имя пользователя/группы, которой предоставляется доступ;
    AccessMask - маска доступа. Этот параметр должен иметь одно из значений FullControl/Change/Read;
    AceType - тип доступа. Этот параметр должен иметь одно из значений Allow/Deny;
  6. Remove-SharePermission User
    где User - имя пользователя/группы, которого следует удалить из ACL сетевой папки;
  7. Get-SharePermission Name
    где Name - имя сетевой папки (не обязательный параметр);
  8. Export-ShareInfo Path
    где Path - путь к CSV файлу (включая имя файла). Если в пути присутствуют пробелы, то путь заключить в кавычки;
  9. Import-ShareInfo Path
    где Path - путь к CSV файлу (включая имя файла). Если в пути присутствуют пробелы, то путь заключить в кавычки.

Ну и собственно сам скрипт с некоторыми комментариями по работе самих функций. Для желающих подробно изучить скрипт я не делал особо комментариев, т.к. построение кода описано в предыдущих частях по управлению безопасностью сетевых папок. Ссылки на предыдущие части:

  1. Управление общими сетевыми ресурсами (шарами) в PowerShell
  2. Управление безопасностью общих папок (сетевых шар) в PowerShell (часть 1)
  3. Управление безопасностью общих папок (сетевых шар) в PowerShell (часть 2)
  4. Управление безопасностью общих папок (сетевых шар) в PowerShell (часть 3)
######################################################## 
# ShareUtils.ps1 
# Version 0.0.0.5 
# 
# Functions for advanced share management 
# 
# Vadims Podans (c) 2008 
# http://vpodans.spaces.live.com/ 
######################################################## 

Write-Host "Vadims Podans's ShareUtils are installed"


# внутренняя функция, которая преобразовывает числовой код возврата операции записи ACL 
# в текстовое значение. 
function _ShareUtils_Get-Code ($share) { 
    switch ($Share.ReturnValue) { 
        "0" {"Успешно"} 
        "2" {"Отказано в доступе"} 
        "8" {"Неизвестная ошибка"} 
        "9" {"Указано недопустимое имя шары"} 
        "21" {"Указан неправильный параметр"} 
        "22" {"Сетевая шара уже существует"} 
        "23" {"Путь перенаправлен"} 
        "24" {"Указан неверный путь"} 
        "25" {"Сетевое имя не найдено"} 
    } 
} 

# основная функция экспорта сведений о сетевых папках в CSV файл. 
function Export-ShareInfo ($path, $name) {
    # если переменная $name пустая, то функция вовзращает все сетевые папки с типом DiskDrive 
    if ($name -ne $null) {
        $shares = Get-WmiObject Win32_Share -filter "name = '$name'" 
    } Else {$shares = Get-WmiObject Win32_Share -filter 'type = 0'} 
    $Shareinfo = @() 
    # цикл извлечения сведений о каждой сетевой папке в переменную $ShareInfo 
    foreach ($share in $shares) {
        $ShareSec = Get-WmiObject Win32_LogicalShareSecuritySetting  -filter "name='$($share.name)'"
        if($shareSec) {
            $sd = $sharesec.GetSecurityDescriptor()
            $ShareInfo += $SD.Descriptor.DACL | % {
                $_ | select @{e={$share.name};n='Name'},
                @{e={$share.Path};n='Path'},
                @{e={$share.Description};n='Description'},
                AccessMask,
                AceFlags,
                AceType,
                @{e={$_.trustee.Name};n='User'},
                @{e={$_.trustee.Domain};n='Domain'},
                @{e={$_.trustee.SIDString};n='SID'}
            }
        }
    } 
    # если переменная $path не передана, то сведения о сетевых папках передаётся в вызывющую 
    # функцию для последующей обработки, в частности добавления и удаления ACE из ACL 
    # списка сетевой папки 
    if ($path -eq $null) {$shareinfo}
    else { 
        # собственно сам экспорт содержимого $ShareInfo в CSV файл 
        $ShareInfo | select Name, Path, Description, User, Domain, SID, AccessMask, AceFlags, AceType | export-csv -noType $path 
        # если указан путь к CSV файлу, то после экспорта данных в файл проверяется, что файл действительно был создан 
        if (Test-Path $path) {Write-Host "Выполнено!"}
        else {Write-Warning "Не удалось создать файл $path. Возможно у вас не хватает прав или путь недоступен."}
    }
} 

# внутренняя функция для записи уже сформированной переменной $ShareInfo в ACL сетевой папки 
function _ShareUtils_WriteShare ($ShareInfo, $shares, $param) {
    $ShareInfo | select -unique name, Path, Description | ForEach-Object {
        $name = $_.name
        $path = $_.Path
        $description = $_.Description
        $SD = ([WMIClass] "Win32_SecurityDescriptor").CreateInstance()
        $ace = ([WMIClass] "Win32_Ace").CreateInstance()
        $Trustee = ([WMIClass] "Win32_Trustee").CreateInstance()
        $sd.DACL = @()
        $ShareInfo | where {$_.name -eq $name} | ForEach-Object {
            $SID = new-object security.principal.securityidentifier($_.SID)
            [byte[]] $SIDArray = ,0 * $SID.BinaryLength
            $SID.GetBinaryForm($SIDArray,0)
            $Trustee.Name = $_.user
            $Trustee.SID = $SIDArray
            $ace.AccessMask = $_.AccessMask
            $ace.AceType = $_.AceType
            $ace.AceFlags = $_.AceFlags
            $ace.trustee = $Trustee
            $sd.DACL += $ACE.psObject.baseobject
        }
        # здесь проверяется наличие промежуточного параметра $param, который после сборки определяет 
        # тип записи. Если $param пустой, то производится запись только в конкретную сетевую папку. Если 
        # же $param не пустой, то при записи и отсутствии сетевой папки она будет создана и в неё будут записаны 
        # данные из CSV файла. Конструкция после Else используется только при импорте данных о сетевых папках 
        # включая Access из заранее подготовленного CSV файла. 
        if ($param -eq $null) {
            $inParams = $shares.psbase.GetMethodParameters("SetShareInfo") 
            $inParams.Access = $SD 
            $write = $shares.psbase.invokemethod("setshareinfo", $inParams, $null) 
            Write-Host "Запись DACL сетевой папки:"
        _ShareUtils_Get-Code $write
        } else {
            $shares = ([WMIClass] "Win32_Share") 
            $inParams = $shares.psbase.GetMethodParameters("Create") 
            $inParams["Name"] = $_.name 
            $inParams["Type"] = 0 
            $inParams["Path"] = $_.Path 
            $inParams["Description"] = $_.Description 
            $inParams["Access"] = $SD.PsObject.BaseObject 
            $write = $shares.psbase.invokemethod("Create", $inParams, $null) 
            Write-Host "Обработка сетевой папки $name по пути $path:" 
            _ShareUtils_Get-Code $write 
        }
    }
} 

# основная функция для импорта данных о сетевых папках из CSV файла. Переменная $path 
# должна содержать путь к CSV файлу. Внутри функции проверяется, чтобы был указан 
# верный путь к CSV файлу. 
function Import-ShareInfo ($path) {
    if (Test-Path $path) {
        $param = "param" 
        $ShareInfo = Import-Csv $path 
        _ShareUtils_WriteShare $ShareInfo -param $param
    } else {Write-Warning "путь к CSV файлу указан неверный!"}
} 

# основная функция для компоновки объекта $AddInfo параметрами безопасности, которые 
# включают в себя как имя сетевой папки, имени пользователя, который должен иметь к ней 
# доступ, и типах доступа, как чтение/запись и действие разрешено/запрещено. Переменная 
# $param определяет действие с готовым объектом - отправить на запись сразу (при этом все 
# существующие разрешения будут удалены и заменены только данными из текущего объекта) 
# или вернуть обратно в вызывющую функцию, для присоединения этого объекта к уже имеющимся, 
# для окончательной компоновки объекта с полным списком ACL. 
function Set-SharePermission ($name, $user, $AceType, $AccessMask, $param) {
    $shares = gwmi Win32_share -Filter "name = '$name'"
    if ($shares -eq $null) {Write-Warning "Указанная сетевая шара не найдена"}
    else {
        # здесь я использовал хэш-таблицы для преобразования текстовых значений маски и типа
        # доступа, которые вводит пользователь в числовые значения, которые затем транслируются и
        # и помещаются в текущий объект с параметрами безопасности.
        $masks = @{FullControl = 2032127; Change = 1245631; Read = 1179817}
        $types = @{Allow = 0; Deny = 1}
        $AddInfo = New-Object System.Management.Automation.PSObject
        # здесь происходит инициализация свойств объекта. Значение каждого параметра приравнял к $null
        # для того, чтобы при отсутствии каких-либо данных они либо оставались пустыми, либо заполнялись
        # системой автоматически.
        $AddInfo | Add-Member NoteProperty Name  ([PSObject]$null)
        $AddInfo | Add-Member NoteProperty Path  ([PSObject]$null)
        $AddInfo | Add-Member NoteProperty Description  ([PSObject]$null)
        $AddInfo | Add-Member NoteProperty AccessMask  ([uint32]$null)
        $AddInfo | Add-Member NoteProperty AceFlags  ([uint32]$null)
        $AddInfo | Add-Member NoteProperty AceType  ([uint32]$null)
        $AddInfo | Add-Member NoteProperty User  ([PSObject]$null)
        $AddInfo | Add-Member NoteProperty Domain  ([PSObject]$null)
        $AddInfo | Add-Member NoteProperty SID  ([PSObject]$null)
        # собственно заполнение свойств созданного объекта данными, которые были переданы из вызывющей 
        # функции.
        $AddInfo.Name = $name
        $AddInfo.Path = $shares.Path
        $AddInfo.Description = $Shares.Description
        $AddInfo.User = $user
        $AddInfo.SID = (new-object security.principal.ntaccount $user).translate([security.principal.securityidentifier])
        $AddInfo.AccessMask = $masks.$AccessMask
        $AddInfo.AceType = $types.$AceType
        # тут так же использовалась временная переменная $param, которая определяет дальнейшее действие
        # с данным объектом - отправка объекта на запись (только при использовании функции Set-SharePermission),
        # либо возврат в вызываемую функцию (только при использовании функции Add-SharePermission).
        if ($param -ne $null) {
        $AddInfo} else {
            _ShareUtils_WriteShare $AddInfo $shares
        }
    }
}

# основная функция для добавления участников безопасности к имеющимуся списку ACL. Данная функция 
# сперва использует функцию экспорта для извлечения сведений об указанной сетевой папке, после чего 
# вызывается функция Set-SharePermission в качестве промежуточной функции, т.к. в неё передаётся перменная 
# $param, то вызываемая функция не будет записывать новый ACL, а вернёт скомпонованный объект $AddInfo. 
function Add-SharePermission ($name, $user, $AceType, $AccessMask) {
    $shares = gwmi Win32_share -Filter "name = '$name'"
    if ($shares -eq $null) {Write-Warning "Указанная сетевая шара не найдена"}
    else {
        # здесь нужно быть внимательным, т.к. нужно обязательно использовать обозначение массива @() для того,
        # чтобы переменная $ShareInfo смогла бы содержать массив объектов с параметрами безопасности. Один объект
        # содержит один ACE для каждого пользователя/группы. Если не использовать обозначение массива, то данная
        # переменная сможет содержать только один объект (т.е. только одного участника безопасности).
        $ShareInfo = @(Export-ShareInfo -name $name)
        $param = "param"
        $ShareInfoNew = Set-SharePermission $name $user $AceType $AccessMask $param
        # вот здесь происходит присоединение с нуля созданного объекта (ACE) к имеющемуся массиву текущих
        # ACE. Таким образом мы можем добавлять участников безопасности к ACL сетевой папки без удаления
        # текущих ACE.
        $ShareInfo += $ShareInfoNew
        _ShareUtils_WriteShare $ShareInfo $shares
    }
}

# основная функция для удаления единичного ACE из ACL сетевой папки. Процесс сводится к извлечению 
# текущего списка ACL и фильтрации ACE в этом списке по методу Not Equal. Всё, что не подпадает под 
# это действие записываются обратно в переменную, а всё, что подпало (указанный пользователь) обратно 
# в переменную $ShareInfo не записывается. 
function Remove-SharePermission ($name, $user) { 
    $shares = gwmi Win32_share -Filter "name = '$name'" 
    if ($shares -eq $null) {Write-Warning "Указанная сетевая шара не найдена"} 
    else {
        $ShareInfo = Export-ShareInfo -name $name 
        $ShareInfo = $shareInfo | where {$_.name -eq "$name" -and $_.user -ne "$user"} 
        _ShareUtils_WriteShare $ShareInfo $shares
    }
} 

# основная функция для создания новых сетевых папок на локальном компьютере. Здесь я использую упрощённый 
# вариант создания сетевой папки, но учитывая один большой нюанс я добавил одно действие. Суть проблемы 
# изложена тут: http://vpodans.spaces.live.com/blog/cns!BB1419A2CFC1E008!170.entry 
# поэтому при создании новой сетевой папки я вручную создаю с нуля список ACL, который содержит 
# только группу Everyone и с правом Allow Read. 
function New-Share ($name, $path, $Description) { 
    $Share = ([wmiClass] 'Win32_share').Create($path, $name, 0, $null, $Description) 
    Write-Host "Создание сетевой шары $name :" 
    $Return = _ShareUtils_Get-Code $share 
    $Return 
    if ($Return -eq "Успешно") { 
        # для использования скрипта в мультиязычных системах без лишних правок в скрипте я вместо именования 
        # группы Everyone я использовал трансляцию её уникального для всех систем SID в строковое значение, 
        # которое может отличаться в зависимости от языка системы. 
        $user = (new-object security.principal.securityidentifier "S-1-1-0").translate([security.principal.ntaccount]) 
        Set-SharePermission $name $user.value "Allow" "Read"
    } 
} 

# основная функция для удаления сетевой шары (равносильно Stop Sharing в консоли Shares). Сама папка 
# и её содержимое не удаляется. 
function Remove-Share ($name) { 
    $share = gwmi Win32_share -Filter "name = '$name'" 
    if ($share -eq $null) {Write-Warning "Указанная сетевая шара не найдена"} else { 
        $share.delete($null) 
        Write-Host "Удаление сетевой шары $name :" 
        _ShareUtils_Get-Code $share
    }
} 

# функция, которая возвращает на экран пользователю список всех сетевых папок на локальном компьютере. 
# можно так же получить сведения только об одной сетевой папке, которую нужно указать при вызове. 
function Get-Share ($name) { 
    if ($name -eq $null) {gwmi Win32_Share -Filter 'type = 0'}    else {gwmi Win32_Share -Filter "name='$name'"} 
} 

# основная функция для вывода на экран сведений о безопасности (содержимого списка ACL) как для всех 
# сетевых папок (если вызывается функция без параметров), так и для конкретной сетевой папки. Т.к. маски и типы 
# доступа приводятся в числовых значениях после вывода сведений выводится краткая справка по трансляции 
# данных значений. Считаю, что нету смысла писать транслятор, который перед выводом информации на экран 
# данных сам автоматически переводил бы в понятные текстовые значения. 
function Get-SharePermission ($name) { 
    Export-ShareInfo -name $name | select name, user, AccessMask, AceType | ft -a -group name 
    Write-Host "Данные колонки AccessMask имеют следующие значения: 
    2032127 - FullControl 
    1245631 - Change 
    1179817 - Read `n 
    Данные колонки AceType имеют следующие значения: 
    0 - Allow 
    1 - Deny 
    2- SystemAudit (группы Administrators и System имеют право Allow FullControl" -foregroundcolor "Yellow"
}

Вот так это всё выглядит. На первый взгляд много и страшно, но если прочитать все предыдушие статьи по данной теме, то данный код уже будет обретать некий смысл. На этом я предлагаю поставить жирную точку в вопросе управления сетевыми папками и безопасностью (Share Permissions) сетевых папок в PowerShell.

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

PowerShell |  ACL |  Shares |  WMI
Wednesday, July 16, 2008 2:26:00 PM (FLE Daylight Time, UTC+03:00)   Comments [0]    

 

Примечание: данный пост перепечатан в связи с закрытием бложиков на spaces.live.com, как имеющий какую-то ценность для автора и/или читателей.


В предыдущей части мы рассмотрели чтение Share Permissions, их редактирование и удаление ACE из полного списка ACL. Здесь осталось рассмотреть вопрос добавления участников безопасности в DACL сетевой шары. Этот процесс, к сожалению, не такой и простой, как может показаться, но тем не менее его тоже нужно решать. Для решения этой задачи нам нужно создать такой же объект с такими же свойствами как и содержимое $ShareInfo. Давайте посмотрим, какими свойствами обладают элементы массива $ShareInfo:

[C:\] $Shareinfo[0] | gm TypeName: Selected.System.Management.ManagementBaseObject Name MemberType Definition ---- ---------- ---------- Equals Method bool Equals(System.Object obj) GetHashCode Method int GetHashCode() GetType Method type GetType() ToString Method string ToString() AccessMask NoteProperty System.Int32 AccessMask=1179817 AceFlags NoteProperty System.UInt32 AceFlags=0 AceType NoteProperty System.UInt32 AceType=0 Description NoteProperty System.String Description= Domain NoteProperty System.String Domain=CONTOSO Name NoteProperty System.String Name=UserShare Path NoteProperty System.String Path=C:\Test SID NoteProperty System.String SID=S-1-5-21-3709200118-438321133-4282490648-513 User NoteProperty System.String User=Domain Users

Нас тут будут интересовать только NoteProperty. Давайте теперь исходя из этих данных создадим свой объект, который будет обладать вот этими свойствами. Тип объекта будет такой же - System.Management.Automation.PSObject  (без Custom). Новый объект создаётся командной New-Object, а члены объекта создаются командой Add-Member:

# создаём новый объект с типом System.Management.Automation.PSObject 
$AddInfo = new-object System.Management.Automation.PSObject 
# добавляем по очереди членов NoteProperty объекта как и в исходном варианте 
$AddInfo | add-member NoteProperty Name  ([PSObject]$null) 
$AddInfo | add-member NoteProperty Path  ([PSObject]$null) 
$AddInfo | add-member NoteProperty Description  ([PSObject]$null) 
$AddInfo | add-member NoteProperty AccessMask  ([uint32]$null) 
$AddInfo | add-member NoteProperty AceFlags  ([uint32]$null) 
$AddInfo | add-member NoteProperty AceType  ([uint32]$null) 
$AddInfo | add-member NoteProperty User  ([PSObject]$null) 
$AddInfo | add-member NoteProperty Domain  ([PSObject]$null) 
$AddInfo | add-member NoteProperty SID  ([PSObject]$null)

Теперь можно посмотреть на результаты нашей работы:

[C:\] $AddInfo Name : Path : Description : AccessMask : 0 AceFlags : 0 AceType : 0 User : Domain : SID :

Ну что ж, уже лучше, теперь можно заполнять эти поля в соответствии с нашими требованиями. Но чтобы не заполнять все поля вручную, предполагается, что для добавления нового участника безопасности в Share Permissions пользователь укажет только имя сетевой шары, имя пользователя, маску доступа и тип доступа (Allow/Deny). Поэтому нам потребуется вытащить информацию о текущей шаре (которая общая для всей шары, как имя, путь, описание и т.д.) и передать эти значения в новую переменную $AddInfo. Остальную часть информации мы обработаем на основании уже переданных параметров и запишем оставшиеся поля переменной $AddInfo, которая в конечном итоге будет содержать всю необходимую информацию. Т.к. уже неоднократно говорилось в предыдущих частях, что метод SetShareInfo перезаписывает полностью информацию о сетевой шаре, поэтому для сохранения существующих ACE мы произведём уже известным способом чтение существующих DACL и сделаем инкремент (добавим к существующим DACL нами созданный DACL). Когда все данные будут скомпонованы мы произведём запись обновлённого списка DACL в ACL шары. Итак, поехали:

# принимаем вводные параметры от пользователя из командной строки 
param ($share, $user, $AccessMask, $AceType) 
# предполагается, что данный скрипт выполняет только добавление участников безопасности 
# поэтому сразу создаём новый объект с необходимыми членами и указанием типа принимаемых 
# данных 
$AddInfo = New-Object System.Management.Automation.PSObject 
$AddInfo | Add-Member NoteProperty Name  ([PSObject]) 
$AddInfo | Add-Member NoteProperty Path  ([PSObject]) 
$AddInfo | Add-Member NoteProperty Description  ([PSObject]) 
$AddInfo | Add-Member NoteProperty AccessMask  ([uint32]) 
$AddInfo | Add-Member NoteProperty AceFlags  ([uint32]) 
$AddInfo | Add-Member NoteProperty AceType  ([uint32]) 
$AddInfo | Add-Member NoteProperty User  ([PSObject]) 
$AddInfo | Add-Member NoteProperty Domain  ([PSObject]) 
$AddInfo | Add-Member NoteProperty SID  ([PSObject]) 
# зная имя сетевой шары делаем её поиск и копируем информацию о имени, пути 
# и описании (поле Description) и выставим прочие параметры 
$CustomShare = Get-WmiObject Win32_Share -filter "name = '$share'" 
$AddInfo.Name = $share 
$AddInfo.Path = $CustomShare.Path 
$AddInfo.Description = $CustomShare.Description 
$AddInfo.Domain = $null 
$AddInfo.AceFlags = 3 
# далее заполняется информация о пользователе и его доступе, поэтому 
# дальше мы ничего не копируем, а обрабатываем уже переданные параметры: 
$AddInfo.User = $user 
# преобразовываем маску доступа из текстовой в численный формат с использованием 
# конструкции Switch.
switch ($AccessMask) {
    "Full"   {$AddInfo.AccessMask = 2032127}
    "Change" {$AddInfo.AccessMask = 1245631}
    "Read"   {$AddInfo.AccessMask = 1179817}
} 
# таким же образом обрабатываем и переменную $AceType: 
switch ($AceType) {
    "Allow"   {$AddInfo.AceType = 0}
    "Deny" {$AddInfo.AceType = 1}
}
# заполняем последнее поле SID путём трансляции имени пользователя/группы в его SID
$AddInfo.SID = (new-object security.principal.ntaccount $user).translate([security.principal.securityidentifier])
# теперь считываем текущий список DACL с указанной сетевой шары в переменную $ShareInfo
$shares = Get-WmiObject Win32_Share -filter "name='$share'"
$Shareinfo = @()
foreach ($share in $shares) {
    $shareSec = Get-WmiObject Win32_LogicalShareSecuritySetting  -filter "name='$($share.name)'"
    if($shareSec) {
        $sd = $sharesec.GetSecurityDescriptor()
        $ShareInfo += $sd.Descriptor.DACL | ForEach-Object {
            $_ | select @{e={$share.name};n='Name'},
            @{e={$share.Path};n='Path'},
            @{e={$share.Description};n='Description'},
            AccessMask,
            AceFlags,
            AceType,
            @{e={$_.trustee.Name};n='User'},
            @{e={$_.trustee.Domain};n='Domain'},
            @{e={$_.trustee.SIDString};n='SID'}
        }
    }
}
# теперь делаем добавление (инкремент) созданного нами массива объектов с зполненными полями к 
# существующему массиву объектов $ShareInfo 
$ShareInfo += $AddInfo 
# Можно для верности убедиться, что $ShareInfo обладает всей необходимой информацией, которую 
# теперь можно записать в шару. В принципе, эта строчка несёт в себе лишь отладочную информацию 
# и когда отладка будет завершена эту строчку можно будет удалить или закомментировать для 
# отладки скрипта в будущем. 
$ShareInfo 
# Если всё в порядке, то можно перезаписывать эти данные в DACL указанной шары: 
$ShareInfo | select -unique name, Path, Description | ForEach-Object { 
    $name = $_.name 
    $path = $_.Path 
    $description = $_.Description 
    "Processing : $name $path $description" 
    $SD = ([WMIClass] "Win32_SecurityDescriptor").CreateInstance() 
    $ace = ([WMIClass] "Win32_Ace").CreateInstance() 
    $Trustee = ([WMIClass] "Win32_Trustee").CreateInstance() 
    $sd.DACL = @() 
    $ShareInfo | where {$_.name -eq $name} | ForEach-Object { 
        $SID = new-object security.principal.securityidentifier($_.SID)
        [byte[]] $SIDArray = ,0 * $SID.BinaryLength
        $SID.GetBinaryForm($SIDArray,0)
        $Trustee.Name = $_.user
        $Trustee.SID = $SIDArray
        $ace.AccessMask = $_.AccessMask
        $ace.AceType = $_.AceType
        $ace.AceFlags = $_.AceFlags
        $ace.trustee = $Trustee
        $sd.DACL += $ACE.psObject.baseobject
    } 
    $inParams = $CustomShare.psbase.GetMethodParameters("SetShareInfo") 
    $inParams.Access = $SD 
    $CustomShare.psbase.invokemethod("setshareinfo", $inParams, $null) 
}

Формат запуска данного скрипта из командной строки CMD или меню Run будет следующим:

powershell %path%\AddUser.ps1 -share "Имя сетевой шары" -user "имя добавляемой группы" -mask "маска доступа, Full/Change/Read" -type "тип доступа, Allow/Deny"

эту команду следует выполнять в одну строчку. В качестве переменной %path% нужно указать путь к папке со скриптом, если он заранее не добавлен в системную переменную %path%. В качестве маски доступа нужно указать одно из 3-х значений, которое может быть Full, Change или Read. Более одного параметра указывать нельзя. Ну и в качестве типа доступа указать либо Allow, что даст доступ, либо Deny, что явно запретит доступ.

Ну вот как бы и всё на данном этапе. Здесь я много чего не пояснял, т.к. достаточно (в моём понимании) разобрал в предыдущих 3-х частях о работе с сетевыми шарами в PowerShell. Но это ещё не всё. Главный девиз PowerShell - быть удобным для использования и кратким для написания (это я сам придумал :) ), однако при работе с сетевыми шарами это совершенно не прослеживается и даже может создаться впечатление громоздкости (хотя тот, кто считает этот код громоздким может написать скрипт короче на VBS с использованием только WMI/.NET :) ), поэтому в следующей части я постараюсь исправить сей момент путём написания единого (относительно компактного по возможности) и представить его как готовое решение, которое в работе будет действительно удобным. Не отключайтесь, продолжение обязательно будет :)

p.s. Данный скрипт не обязательно является самым простым решением, т.к. возможно, что данную операцию можно провести более удобным и изящным способом (хотя, после чтения документации на MSDN мне так не кажется, что это возможно), но в любом случае адекватные замечания/поправки/дополнения к этому скрипту всячески приветствуются.

PowerShell |  ACL |  Shares |  WMI
Tuesday, July 08, 2008 2:12:00 PM (FLE Daylight Time, UTC+03:00)   Comments [0]    

 

Previous Page Page 2 of 4 in the PowerShellACL category Next Page
 · 

All content © 2008 - 2012, Vadims Podāns
"Spaces" Theme provided by: Vadims Podāns
About


E-mail - Send mail to the author(s)
Live Messenger -
For english language visitors
Библиотека
Календарик
<February 2012>
SunMonTueWedThuFriSat
2930311234
567891011
12131415161718
19202122232425
26272829123
45678910

Карта расположения посетителей
Favorites





Disclaimer
Вся информация на сайте предоставляется на условиях «как есть», без предоставления каких-либо гарантий и прав.

При использовании материалов c данного сайта ссылка на оригинальный источник обязательна.
Protected by Copyscape Online Plagiarism Scanner