From 25ea5ee6ddd2e7260048700ca75e0d8a374d5624 Mon Sep 17 00:00:00 2001
From: "G.Reijn" <26114636+Gijsreyn@users.noreply.github.com>
Date: Fri, 16 Oct 2026 13:16:36 +0200
Subject: [PATCH 1/3] (GH-1755) Add new DSC resource to manage SecurityPolicy
---
.../code-review/security.instructions.md | 2 +-
.../code-review/windows.instructions.md | 2 +-
CHANGELOG.md | 14 +
Cargo.lock | 11 +
Cargo.toml | 6 +-
data.build.json | 2 +
.../configure-user-rights-assignment.md | 264 +++++++++++++
.../examples/get-user-rights-assignment.md | 132 +++++++
.../userrightsassignment.config.dsc.yaml | 23 ++
.../Windows/UserRightsAssignment/index.md | 373 ++++++++++++++++++
docs/reference/resources/overview.md | 2 +
.../.project.data.json | 14 +
resources/windows_security_policy/Cargo.toml | 18 +
resources/windows_security_policy/build.rs | 7 +
.../locales/en-us.toml | 36 ++
resources/windows_security_policy/src/lsa.rs | 234 +++++++++++
resources/windows_security_policy/src/main.rs | 217 ++++++++++
.../windows_security_policy/src/rights.rs | 106 +++++
resources/windows_security_policy/src/sid.rs | 338 ++++++++++++++++
.../windows_security_policy/src/types.rs | 150 +++++++
.../src/user_rights.rs | 295 ++++++++++++++
...ecurity_policy_user_right_export.tests.ps1 | 63 +++
...s_security_policy_user_right_get.tests.ps1 | 117 ++++++
...s_security_policy_user_right_set.tests.ps1 | 170 ++++++++
..._security_policy_user_right_test.tests.ps1 | 100 +++++
...ecurity_policy_user_right_whatif.tests.ps1 | 96 +++++
...windows_security_policy.dsc.manifests.json | 182 +++++++++
27 files changed, 2971 insertions(+), 3 deletions(-)
create mode 100644 docs/reference/resources/Microsoft/Windows/UserRightsAssignment/examples/configure-user-rights-assignment.md
create mode 100644 docs/reference/resources/Microsoft/Windows/UserRightsAssignment/examples/get-user-rights-assignment.md
create mode 100644 docs/reference/resources/Microsoft/Windows/UserRightsAssignment/examples/userrightsassignment.config.dsc.yaml
create mode 100644 docs/reference/resources/Microsoft/Windows/UserRightsAssignment/index.md
create mode 100644 resources/windows_security_policy/.project.data.json
create mode 100644 resources/windows_security_policy/Cargo.toml
create mode 100644 resources/windows_security_policy/build.rs
create mode 100644 resources/windows_security_policy/locales/en-us.toml
create mode 100644 resources/windows_security_policy/src/lsa.rs
create mode 100644 resources/windows_security_policy/src/main.rs
create mode 100644 resources/windows_security_policy/src/rights.rs
create mode 100644 resources/windows_security_policy/src/sid.rs
create mode 100644 resources/windows_security_policy/src/types.rs
create mode 100644 resources/windows_security_policy/src/user_rights.rs
create mode 100644 resources/windows_security_policy/tests/windows_security_policy_user_right_export.tests.ps1
create mode 100644 resources/windows_security_policy/tests/windows_security_policy_user_right_get.tests.ps1
create mode 100644 resources/windows_security_policy/tests/windows_security_policy_user_right_set.tests.ps1
create mode 100644 resources/windows_security_policy/tests/windows_security_policy_user_right_test.tests.ps1
create mode 100644 resources/windows_security_policy/tests/windows_security_policy_user_right_whatif.tests.ps1
create mode 100644 resources/windows_security_policy/windows_security_policy.dsc.manifests.json
diff --git a/.github/skills/code-review/security.instructions.md b/.github/skills/code-review/security.instructions.md
index 42de16d5c..9cc49439c 100644
--- a/.github/skills/code-review/security.instructions.md
+++ b/.github/skills/code-review/security.instructions.md
@@ -1,5 +1,5 @@
---
-applyTo: 'lib/dsc-lib/src/util.rs,lib/dsc-lib-security_context/**,lib/dsc-lib-registry/**,resources/registry/**,resources/windows_firewall/**,resources/windows_service/**,resources/dism_dsc/**'
+applyTo: 'lib/dsc-lib/src/util.rs,lib/dsc-lib-security_context/**,lib/dsc-lib-registry/**,resources/registry/**,resources/windows_firewall/**,resources/windows_service/**,resources/windows_security_policy/**,resources/dism_dsc/**'
description: 'Code review guidance for security-sensitive code (ACLs, policy, credentials, elevated resources)'
---
diff --git a/.github/skills/code-review/windows.instructions.md b/.github/skills/code-review/windows.instructions.md
index 2a6d5e545..ce4aa708a 100644
--- a/.github/skills/code-review/windows.instructions.md
+++ b/.github/skills/code-review/windows.instructions.md
@@ -1,5 +1,5 @@
---
-applyTo: 'resources/windows_firewall/**,resources/windows_service/**,resources/WindowsUpdate/**,resources/windows_personalization/**,resources/dism_dsc/**,resources/registry/**,lib/dsc-lib-registry/**,lib/dsc-lib-pal/**'
+applyTo: 'resources/windows_firewall/**,resources/windows_service/**,resources/windows_security_policy/**,resources/WindowsUpdate/**,resources/windows_personalization/**,resources/dism_dsc/**,resources/registry/**,lib/dsc-lib-registry/**,lib/dsc-lib-pal/**'
description: 'Code review guidance for Windows-specific code (FFI, COM, registry, DISM, services)'
---
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4ae6e6f41..f6fd2e0e4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -77,6 +77,19 @@ changes since the last release, see the [diff on GitHub][unreleased].
### Added
+- Added the `Microsoft.Windows/UserRightsAssignment` built-in resource for managing which accounts
+ hold a Windows user right or privilege (Local Security Policy > User Rights Assignment) natively
+ through the LSA account-rights APIs, as the first part of a replacement for the
+ `SecurityPolicyDsc` PowerShell module. Identities can be given as names or SIDs, the
+ `setAction` property chooses between replacing, adding or removing holders, and the resource
+ supports `get`, `set`, `test`, `export`, and `--what-if`.
+
+ Related work items
+
+ - Issues: [#1755][#1755]
+
+
+
- Added what-if support to the MCP server tools. The `invoke_dsc_config` tool now accepts a
`what_if` option for the `set` operation, and the `invoke_dsc_resource` tool accepts `what_if`
for the `set` and `delete` operations. This mirrors the `--what-if` flag on the `dsc config set`,
@@ -1982,4 +1995,5 @@ Version `3.0.0` is the first generally available release of DSC.
[#1557]: https://github.com/PowerShell/DSC/issues/1557
[#1558]: https://github.com/PowerShell/DSC/issues/1558
[#1562]: https://github.com/PowerShell/DSC/issues/1562
+[#1755]: https://github.com/PowerShell/DSC/issues/1755
diff --git a/Cargo.lock b/Cargo.lock
index 31ec37535..0e23ad34a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4513,6 +4513,17 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+[[package]]
+name = "windows_security_policy"
+version = "0.1.0"
+dependencies = [
+ "dsc-lib-security_context",
+ "rust-i18n",
+ "serde",
+ "serde_json",
+ "windows 0.62.2",
+]
+
[[package]]
name = "windows_service"
version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index d371a5b4d..9feb64e38 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -27,6 +27,7 @@ members = [
"resources/windows_service",
"resources/WindowsUpdate",
"resources/environment_variable",
+ "resources/windows_security_policy",
"adapters/group_policy_template",
"tools/dsctest",
"tools/test_group_resource",
@@ -62,6 +63,7 @@ default-members = [
"resources/windows_service",
"resources/WindowsUpdate",
"resources/environment_variable",
+ "resources/windows_security_policy",
"adapters/group_policy_template",
"tools/dsctest",
"tools/test_group_resource",
@@ -98,6 +100,7 @@ Windows = [
"resources/windows_service",
"resources/WindowsUpdate",
"resources/environment_variable",
+ "resources/windows_security_policy",
"adapters/group_policy_template",
"tools/dsctest",
"tools/test_group_resource",
@@ -290,11 +293,12 @@ urlencoding = { version = "2.1" }
which = { version = "8.0.6" }
# dsc-lib
ipnetwork = { version = "0.21" }
-# WindowsUpdate, windows_service, dsc-lib (ACL checks), dsc-lib-registry
+# WindowsUpdate, windows_service, windows_security_policy, dsc-lib (ACL checks), dsc-lib-registry
windows = { version = "0.62", features = [
"Win32_Foundation",
"Win32_NetworkManagement_WindowsFirewall",
"Win32_Security",
+ "Win32_Security_Authentication_Identity",
"Win32_Security_Authorization",
"Win32_System_Com",
"Win32_System_Memory",
diff --git a/data.build.json b/data.build.json
index a91dc0240..7fd4e6ed1 100644
--- a/data.build.json
+++ b/data.build.json
@@ -117,6 +117,8 @@
"windows_firewall.exe",
"windows_service.exe",
"windows_service.dsc.resource.json",
+ "windows_security_policy.exe",
+ "windows_security_policy.dsc.manifests.json",
"windows_feature.dsc.resource.json",
"wmi.dsc.resource.json",
"wmi.resource.ps1",
diff --git a/docs/reference/resources/Microsoft/Windows/UserRightsAssignment/examples/configure-user-rights-assignment.md b/docs/reference/resources/Microsoft/Windows/UserRightsAssignment/examples/configure-user-rights-assignment.md
new file mode 100644
index 000000000..5464f2c3a
--- /dev/null
+++ b/docs/reference/resources/Microsoft/Windows/UserRightsAssignment/examples/configure-user-rights-assignment.md
@@ -0,0 +1,264 @@
+---
+description: >
+ Example showing how to use the Microsoft.Windows/UserRightsAssignment resource in a DSC
+ configuration document to enforce user rights assignments.
+ms.date: 10/16/2026
+ms.topic: reference
+title: Configure user rights assignments
+---
+
+# Configure user rights assignments
+
+This example shows how you can use the `Microsoft.Windows/UserRightsAssignment` resource in a DSC
+configuration document to enforce user rights the way a hardening baseline requires, using each of
+the three `setAction` modes.
+
+> [!IMPORTANT]
+> All operations of this resource require an elevated (administrator) process context. Run your
+> terminal or PowerShell session as Administrator before using `dsc config set`.
+
+## Definition
+
+The configuration document for this example defines three instances of the
+`UserRightsAssignment` resource.
+
+The first instance makes `BUILTIN\Guests` the only account that is denied batch logon; because
+`setAction` isn't specified, it defaults to `replace`. The second instance ensures that
+`NT SERVICE\ALL SERVICES` can log on as a service without touching the other holders. The third
+instance ensures that `BUILTIN\Guests` can't log on locally, whoever else holds that right.
+
+:::code language="yaml" source="userrightsassignment.config.dsc.yaml":::
+
+Copy the configuration document and save it as `userrightsassignment.config.dsc.yaml`.
+
+## Setup
+
+The output in this example assumes that nobody is denied batch logon, that `ALL SERVICES` already
+holds `SeServiceLogonRight`, and that `Guests` holds `SeInteractiveLogonRight`. You can set the
+system to this starting state with a security template:
+
+```powershell
+@'
+[Unicode]
+Unicode=yes
+[Privilege Rights]
+SeDenyBatchLogonRight =
+SeInteractiveLogonRight = *S-1-5-32-544,*S-1-5-32-545,*S-1-5-32-546,*S-1-5-32-551
+[Version]
+signature="$CHICAGO$"
+Revision=1
+'@ | Set-Content -Path "$env:TEMP\setup.inf" -Encoding unicode
+secedit /configure /db "$env:TEMP\setup.sdb" /cfg "$env:TEMP\setup.inf" /areas USER_RIGHTS /quiet
+```
+
+## Test the configuration
+
+To see whether the system is already in the desired state, use the [dsc config test][01] command.
+
+```powershell
+dsc config test --file ./userrightsassignment.config.dsc.yaml
+```
+
+```yaml
+executionInformation:
+ # Elided for brevity
+metadata:
+ # Elided for brevity
+results:
+- executionInformation:
+ duration: PT0.0532118S
+ metadata:
+ Microsoft.DSC:
+ duration: PT0.0532118S
+ name: Deny batch logon to guests only
+ type: Microsoft.Windows/UserRightsAssignment
+ result:
+ desiredState:
+ right: SeDenyBatchLogonRight
+ identity:
+ - BUILTIN\Guests
+ actualState:
+ right: SeDenyBatchLogonRight
+ identity: []
+ _inDesiredState: false
+ inDesiredState: false
+ differingProperties:
+ - identity
+- executionInformation:
+ duration: PT0.0498742S
+ metadata:
+ Microsoft.DSC:
+ duration: PT0.0498742S
+ name: Services may log on as a service
+ type: Microsoft.Windows/UserRightsAssignment
+ result:
+ desiredState:
+ right: SeServiceLogonRight
+ identity:
+ - NT SERVICE\ALL SERVICES
+ setAction: add
+ actualState:
+ right: SeServiceLogonRight
+ identity:
+ - NT SERVICE\ALL SERVICES
+ - NT VIRTUAL MACHINE\Virtual Machines
+ _inDesiredState: true
+ inDesiredState: true
+ differingProperties:
+ - identity
+- executionInformation:
+ duration: PT0.0511306S
+ metadata:
+ Microsoft.DSC:
+ duration: PT0.0511306S
+ name: Guests may not log on locally
+ type: Microsoft.Windows/UserRightsAssignment
+ result:
+ desiredState:
+ right: SeInteractiveLogonRight
+ identity:
+ - BUILTIN\Guests
+ setAction: remove
+ actualState:
+ right: SeInteractiveLogonRight
+ identity:
+ - BUILTIN\Administrators
+ - BUILTIN\Backup Operators
+ - BUILTIN\Guests
+ - BUILTIN\Users
+ _inDesiredState: false
+ inDesiredState: false
+ differingProperties:
+ - identity
+messages: []
+hadErrors: false
+```
+
+The second instance is in the desired state because `ALL SERVICES` already holds the right. The
+resource decides `inDesiredState` from `setAction`; DSC still lists `identity` under
+`differingProperties` because the arrays differ.
+
+## Set the configuration
+
+To enforce the desired state, use the [dsc config set][02] command.
+
+```powershell
+dsc config set --file ./userrightsassignment.config.dsc.yaml
+```
+
+```yaml
+executionInformation:
+ # Elided for brevity
+metadata:
+ # Elided for brevity
+results:
+- executionInformation:
+ duration: PT0.0987421S
+ metadata:
+ Microsoft.DSC:
+ duration: PT0.0987421S
+ name: Deny batch logon to guests only
+ type: Microsoft.Windows/UserRightsAssignment
+ result:
+ beforeState:
+ right: SeDenyBatchLogonRight
+ identity: []
+ _inDesiredState: false
+ afterState:
+ right: SeDenyBatchLogonRight
+ identity:
+ - BUILTIN\Guests
+ changedProperties:
+ - identity
+ - _inDesiredState
+- executionInformation:
+ duration: PT0.0509873S
+ metadata:
+ Microsoft.DSC:
+ duration: PT0.0509873S
+ name: Services may log on as a service
+ type: Microsoft.Windows/UserRightsAssignment
+ result:
+ beforeState:
+ right: SeServiceLogonRight
+ identity:
+ - NT SERVICE\ALL SERVICES
+ - NT VIRTUAL MACHINE\Virtual Machines
+ _inDesiredState: true
+ afterState:
+ right: SeServiceLogonRight
+ identity:
+ - NT SERVICE\ALL SERVICES
+ - NT VIRTUAL MACHINE\Virtual Machines
+ _inDesiredState: true
+ changedProperties: null
+- executionInformation:
+ duration: PT0.0975219S
+ metadata:
+ Microsoft.DSC:
+ duration: PT0.0975219S
+ name: Guests may not log on locally
+ type: Microsoft.Windows/UserRightsAssignment
+ result:
+ beforeState:
+ right: SeInteractiveLogonRight
+ identity:
+ - BUILTIN\Administrators
+ - BUILTIN\Backup Operators
+ - BUILTIN\Guests
+ - BUILTIN\Users
+ _inDesiredState: false
+ afterState:
+ right: SeInteractiveLogonRight
+ identity:
+ - BUILTIN\Administrators
+ - BUILTIN\Backup Operators
+ - BUILTIN\Users
+ changedProperties:
+ - identity
+ - _inDesiredState
+messages: []
+hadErrors: false
+```
+
+DSC skipped the second instance because its test reported it in the desired state. The third
+instance only revoked the right from `Guests` and kept the other holders, because it uses
+`setAction: remove`.
+
+## Preview the changes
+
+To see which accounts would be granted or revoked without applying the change, add the
+`--what-if` option. The resource reports the projected holders and describes each change under
+`_metadata.whatIf`.
+
+```powershell
+dsc config set --file ./userrightsassignment.config.dsc.yaml --what-if
+```
+
+```yaml
+# Elided for brevity
+results:
+- name: Deny batch logon to guests only
+ type: Microsoft.Windows/UserRightsAssignment
+ result:
+ beforeState:
+ right: SeDenyBatchLogonRight
+ identity: []
+ _inDesiredState: false
+ afterState:
+ right: SeDenyBatchLogonRight
+ identity:
+ - BUILTIN\Guests
+ _metadata:
+ whatIf:
+ - Would grant 'SeDenyBatchLogonRight' to 'BUILTIN\Guests'
+ changedProperties:
+ - identity
+ - _inDesiredState
+ - _metadata
+# Elided for brevity
+```
+
+
+[01]: ../../../../../../cli/config/test.md
+[02]: ../../../../../../cli/config/set.md
diff --git a/docs/reference/resources/Microsoft/Windows/UserRightsAssignment/examples/get-user-rights-assignment.md b/docs/reference/resources/Microsoft/Windows/UserRightsAssignment/examples/get-user-rights-assignment.md
new file mode 100644
index 000000000..e824a6b1d
--- /dev/null
+++ b/docs/reference/resources/Microsoft/Windows/UserRightsAssignment/examples/get-user-rights-assignment.md
@@ -0,0 +1,132 @@
+---
+description: >
+ Example showing how to use the Microsoft.Windows/UserRightsAssignment resource with the dsc
+ resource commands to retrieve, test and export user rights assignments.
+ms.date: 10/16/2026
+ms.topic: reference
+title: Get user rights assignments
+---
+
+# Get user rights assignments
+
+This example shows how you can use the `Microsoft.Windows/UserRightsAssignment` resource to
+retrieve the accounts that hold a user right, test whether a right is assigned as expected, and
+export every right on the system.
+
+> [!IMPORTANT]
+> All operations of this resource require an elevated (administrator) process context. Run your
+> terminal or PowerShell session as Administrator before using the `dsc resource` commands.
+
+## Get the holders of a right
+
+To retrieve the accounts that hold a right, use the [dsc resource get][01] command. The only
+required property is `right`.
+
+```powershell
+dsc resource get --resource Microsoft.Windows/UserRightsAssignment --input '{"right":"SeServiceLogonRight"}'
+```
+
+```yaml
+actualState:
+ right: SeServiceLogonRight
+ identity:
+ - NT SERVICE\ALL SERVICES
+ - NT VIRTUAL MACHINE\Virtual Machines
+```
+
+The resource returns every holder as `DOMAIN\Name`. Accounts without a domain, such as `Everyone`,
+are returned by name only, and a SID that no longer resolves to an account is returned as the SID
+string.
+
+## Test the holders of a right
+
+To check whether a right is assigned the way you expect, use the [dsc resource test][02] command.
+The resource compares accounts by SID, so you can write the identities in any accepted form. The
+following command checks that `Guests` is the only account denied batch logon:
+
+```powershell
+dsc resource test --resource Microsoft.Windows/UserRightsAssignment --input '{"right":"SeDenyBatchLogonRight","identity":["S-1-5-32-546"]}'
+```
+
+```yaml
+desiredState:
+ right: SeDenyBatchLogonRight
+ identity:
+ - S-1-5-32-546
+actualState:
+ right: SeDenyBatchLogonRight
+ identity: []
+ _inDesiredState: false
+inDesiredState: false
+differingProperties:
+- identity
+```
+
+To check that an account holds a right regardless of the other holders, add `setAction: add`:
+
+```powershell
+dsc resource test --resource Microsoft.Windows/UserRightsAssignment --input '{"right":"SeChangeNotifyPrivilege","identity":["Everyone"],"setAction":"add"}'
+```
+
+```yaml
+desiredState:
+ right: SeChangeNotifyPrivilege
+ identity:
+ - Everyone
+ setAction: add
+actualState:
+ right: SeChangeNotifyPrivilege
+ identity:
+ - BUILTIN\Administrators
+ - BUILTIN\Backup Operators
+ - BUILTIN\Users
+ - Everyone
+ - NT AUTHORITY\LOCAL SERVICE
+ - NT AUTHORITY\NETWORK SERVICE
+ _inDesiredState: true
+inDesiredState: true
+differingProperties:
+- identity
+```
+
+The resource reports `inDesiredState: true` because `Everyone` holds the right. DSC still lists
+`identity` under `differingProperties` because the arrays differ; rely on `inDesiredState` when
+you use `add` or `remove`.
+
+## Export every right
+
+To export the holders of every user right, use the [dsc resource export][03] command. The output
+is a configuration document with one instance per right, which you can use as the starting point
+for a baseline.
+
+```powershell
+dsc resource export --resource Microsoft.Windows/UserRightsAssignment
+```
+
+```yaml
+$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
+resources:
+- name: Microsoft.Windows/UserRightsAssignment-0
+ type: Microsoft.Windows/UserRightsAssignment
+ properties:
+ right: SeTrustedCredManAccessPrivilege
+ identity: []
+- name: Microsoft.Windows/UserRightsAssignment-1
+ type: Microsoft.Windows/UserRightsAssignment
+ properties:
+ right: SeNetworkLogonRight
+ identity:
+ - BUILTIN\Administrators
+ - BUILTIN\Backup Operators
+ - BUILTIN\Users
+ - Everyone
+# Elided for brevity
+```
+
+Applying an exported document with `dsc config set` makes every right match the exported holders,
+because `setAction` defaults to `replace`.
+
+
+[01]: ../../../../../../cli/resource/get.md
+[02]: ../../../../../../cli/resource/test.md
+[03]: ../../../../../../cli/resource/export.md
diff --git a/docs/reference/resources/Microsoft/Windows/UserRightsAssignment/examples/userrightsassignment.config.dsc.yaml b/docs/reference/resources/Microsoft/Windows/UserRightsAssignment/examples/userrightsassignment.config.dsc.yaml
new file mode 100644
index 000000000..d3f0a3210
--- /dev/null
+++ b/docs/reference/resources/Microsoft/Windows/UserRightsAssignment/examples/userrightsassignment.config.dsc.yaml
@@ -0,0 +1,23 @@
+# yaml-language-server: $schema=https://aka.ms/dsc/schemas/v3/bundled/config/document.vscode.json
+$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
+resources:
+ - name: Deny batch logon to guests only
+ type: Microsoft.Windows/UserRightsAssignment
+ properties:
+ right: SeDenyBatchLogonRight
+ identity:
+ - BUILTIN\Guests
+ - name: Services may log on as a service
+ type: Microsoft.Windows/UserRightsAssignment
+ properties:
+ right: SeServiceLogonRight
+ identity:
+ - NT SERVICE\ALL SERVICES
+ setAction: add
+ - name: Guests may not log on locally
+ type: Microsoft.Windows/UserRightsAssignment
+ properties:
+ right: SeInteractiveLogonRight
+ identity:
+ - BUILTIN\Guests
+ setAction: remove
diff --git a/docs/reference/resources/Microsoft/Windows/UserRightsAssignment/index.md b/docs/reference/resources/Microsoft/Windows/UserRightsAssignment/index.md
new file mode 100644
index 000000000..98812a7bf
--- /dev/null
+++ b/docs/reference/resources/Microsoft/Windows/UserRightsAssignment/index.md
@@ -0,0 +1,373 @@
+---
+description: Microsoft.Windows/UserRightsAssignment resource reference documentation
+ms.date: 10/16/2026
+ms.topic: reference
+title: Microsoft.Windows/UserRightsAssignment
+---
+
+# Microsoft.Windows/UserRightsAssignment
+
+## Synopsis
+
+Manage which accounts hold a Windows user right or privilege.
+
+## Metadata
+
+```yaml
+Version : 0.1.0
+Kind : resource
+Tags : [Windows, Security, UserRights]
+Author : Microsoft
+```
+
+## Instance definition syntax
+
+```yaml
+resources:
+ - name:
+ type: Microsoft.Windows/UserRightsAssignment
+ properties:
+ # Key properties
+ right: string
+ # Instance properties
+ identity:
+ - string
+ setAction: replace | add | remove
+```
+
+## Description
+
+The `Microsoft.Windows/UserRightsAssignment` resource enables you to idempotently manage the
+accounts that hold a user right or privilege on a Windows system. These are the settings under
+**Local Policies > User Rights Assignment** in the Local Security Policy console, which
+`secedit` exports as the `[Privilege Rights]` section of a security template. Each instance of the
+resource represents one right, such as `SeServiceLogonRight` (Log on as a service), and lists the
+accounts that hold it.
+
+The resource can:
+
+- Retrieve the accounts that hold a right.
+- Make the listed accounts the only holders of a right, grant a right to additional accounts, or
+ revoke a right from specific accounts.
+- Test whether a right is held by exactly, at least, or none of the listed accounts.
+- Export the holders of every right on the system.
+
+The resource reads and writes the policy through the Local Security Authority (LSA) account
+rights APIs, not through `secedit`, so it needs no template files and works on any system
+language. Accounts are compared by security identifier (SID), so `Administrators`,
+`BUILTIN\Administrators` and `S-1-5-32-544` all refer to the same holder.
+
+Windows defines the following user rights:
+
+| Constant | Local Security Policy name |
+|:--------------------------------------------|:--------------------------------------------------------------------|
+| `SeTrustedCredManAccessPrivilege` | Access Credential Manager as a trusted caller |
+| `SeNetworkLogonRight` | Access this computer from the network |
+| `SeTcbPrivilege` | Act as part of the operating system |
+| `SeMachineAccountPrivilege` | Add workstations to domain |
+| `SeIncreaseQuotaPrivilege` | Adjust memory quotas for a process |
+| `SeInteractiveLogonRight` | Allow log on locally |
+| `SeRemoteInteractiveLogonRight` | Allow log on through Remote Desktop Services |
+| `SeBackupPrivilege` | Back up files and directories |
+| `SeChangeNotifyPrivilege` | Bypass traverse checking |
+| `SeSystemtimePrivilege` | Change the system time |
+| `SeTimeZonePrivilege` | Change the time zone |
+| `SeCreatePagefilePrivilege` | Create a pagefile |
+| `SeCreateTokenPrivilege` | Create a token object |
+| `SeCreateGlobalPrivilege` | Create global objects |
+| `SeCreatePermanentPrivilege` | Create permanent shared objects |
+| `SeCreateSymbolicLinkPrivilege` | Create symbolic links |
+| `SeDebugPrivilege` | Debug programs |
+| `SeDenyNetworkLogonRight` | Deny access to this computer from the network |
+| `SeDenyBatchLogonRight` | Deny log on as a batch job |
+| `SeDenyServiceLogonRight` | Deny log on as a service |
+| `SeDenyInteractiveLogonRight` | Deny log on locally |
+| `SeDenyRemoteInteractiveLogonRight` | Deny log on through Remote Desktop Services |
+| `SeEnableDelegationPrivilege` | Enable computer and user accounts to be trusted for delegation |
+| `SeRemoteShutdownPrivilege` | Force shutdown from a remote system |
+| `SeAuditPrivilege` | Generate security audits |
+| `SeImpersonatePrivilege` | Impersonate a client after authentication |
+| `SeIncreaseWorkingSetPrivilege` | Increase a process working set |
+| `SeIncreaseBasePriorityPrivilege` | Increase scheduling priority |
+| `SeLoadDriverPrivilege` | Load and unload device drivers |
+| `SeLockMemoryPrivilege` | Lock pages in memory |
+| `SeBatchLogonRight` | Log on as a batch job |
+| `SeServiceLogonRight` | Log on as a service |
+| `SeSecurityPrivilege` | Manage auditing and security log |
+| `SeRelabelPrivilege` | Modify an object label |
+| `SeSystemEnvironmentPrivilege` | Modify firmware environment values |
+| `SeDelegateSessionUserImpersonatePrivilege` | Obtain an impersonation token for another user in the same session |
+| `SeManageVolumePrivilege` | Perform volume maintenance tasks |
+| `SeProfileSingleProcessPrivilege` | Profile single process |
+| `SeSystemProfilePrivilege` | Profile system performance |
+| `SeUndockPrivilege` | Remove computer from docking station |
+| `SeAssignPrimaryTokenPrivilege` | Replace a process level token |
+| `SeRestorePrivilege` | Restore files and directories |
+| `SeShutdownPrivilege` | Shut down the system |
+| `SeSyncAgentPrivilege` | Synchronize directory service data |
+| `SeTakeOwnershipPrivilege` | Take ownership of files or other objects |
+
+> [!NOTE]
+> This resource is installed with DSC itself on Windows systems.
+>
+> You can update this resource by updating DSC. When you update DSC, the updated version of this
+> resource is automatically available.
+
+## Requirements
+
+- The resource is only usable on a Windows system.
+- All operations, including **Get**, **Test** and **Export**, require an elevated (administrator)
+ process context because the LSA only lets administrators enumerate account rights. DSC refuses
+ to invoke the resource from a non-elevated process.
+- When user rights are managed by Group Policy, the domain or local Group Policy setting
+ overwrites the local assignment at the next policy refresh. Manage the right through Group
+ Policy instead, or exclude it from Group Policy.
+
+## Capabilities
+
+The resource has the following capabilities:
+
+- `get` - You can use the resource to retrieve the accounts that hold a right.
+- `set` - You can use the resource to enforce the holders of a right.
+- `test` - The resource implements its own test. It compares the current and desired holders by
+ SID and honours [setAction](#setaction), so any accepted spelling of an account matches.
+- `export` - You can use the resource to export the holders of every right on the system.
+- `whatIf` - You can use the resource with the `--what-if` option of the `dsc config set` and
+ `dsc resource set` commands to see which accounts would be granted or revoked without applying
+ the change.
+
+For more information about resource capabilities, see [DSC resource capabilities][01].
+
+## Examples
+
+1. [Get user rights assignments][02] - Shows how to retrieve, test and export user rights with the
+ `dsc resource` commands.
+1. [Configure user rights assignments][03] - Shows how to enforce user rights using a DSC
+ configuration document.
+
+## Properties
+
+The following list describes the properties for the resource.
+
+- **Key properties:** The following properties uniquely identify an
+ instance. If two instances of a resource have the same values for their key properties, the
+ instances are conflicting. For more information about key properties, see the "Key resource
+ properties" section in [DSC resource properties][04].
+
+ - [right](#right) - The user right or privilege to manage.
+
+- **Instance properties:** The following properties are optional.
+ They define the desired state for an instance of the resource.
+
+ - [identity](#identity) - The accounts that hold the right.
+ - [setAction](#setaction) - How the `identity` list is applied.
+
+- **Read-only properties:** The resource returns the following
+ properties, but they aren't configurable. For more information about read-only properties, see
+ the "Read-only resource properties" section in [DSC resource properties][05].
+
+ - [_inDesiredState](#_indesiredstate) - Whether the current holders satisfy the desired state.
+
+### right
+
+Expand for right property metadata
+
+```yaml
+Type : string
+IsRequired : true
+IsKey : true
+IsReadOnly : false
+IsWriteOnly : false
+Enum : [SeTrustedCredManAccessPrivilege, SeNetworkLogonRight, SeTcbPrivilege, SeMachineAccountPrivilege, SeIncreaseQuotaPrivilege, SeInteractiveLogonRight, SeRemoteInteractiveLogonRight, SeBackupPrivilege, SeChangeNotifyPrivilege, SeSystemtimePrivilege, SeTimeZonePrivilege, SeCreatePagefilePrivilege, SeCreateTokenPrivilege, SeCreateGlobalPrivilege, SeCreatePermanentPrivilege, SeCreateSymbolicLinkPrivilege, SeDebugPrivilege, SeDenyNetworkLogonRight, SeDenyBatchLogonRight, SeDenyServiceLogonRight, SeDenyInteractiveLogonRight, SeDenyRemoteInteractiveLogonRight, SeEnableDelegationPrivilege, SeRemoteShutdownPrivilege, SeAuditPrivilege, SeImpersonatePrivilege, SeIncreaseWorkingSetPrivilege, SeIncreaseBasePriorityPrivilege, SeLoadDriverPrivilege, SeLockMemoryPrivilege, SeBatchLogonRight, SeServiceLogonRight, SeSecurityPrivilege, SeRelabelPrivilege, SeSystemEnvironmentPrivilege, SeDelegateSessionUserImpersonatePrivilege, SeManageVolumePrivilege, SeProfileSingleProcessPrivilege, SeSystemProfilePrivilege, SeUndockPrivilege, SeAssignPrimaryTokenPrivilege, SeRestorePrivilege, SeShutdownPrivilege, SeSyncAgentPrivilege, SeTakeOwnershipPrivilege]
+```
+
+
+
+The constant of the user right or privilege to manage, such as `SeServiceLogonRight`. These are
+the names that the LSA, `secedit` templates, Group Policy and security benchmarks use; the table
+in the [Description](#description) maps them to the names shown in the Local Security Policy
+console. The resource returns the value exactly as you specified it, and **Export** returns the
+canonical casing.
+
+### identity
+
+Expand for identity property metadata
+
+```yaml
+Type : array
+ItemsType : string
+ItemsMustBeUnique : true
+IsRequired : false
+IsKey : false
+IsReadOnly : false
+IsWriteOnly : false
+```
+
+
+
+The accounts that hold the right. This property is required for the **Set** and **Test**
+operations. Each entry can be any of the following:
+
+- An account name with its domain, such as `CONTOSO\svc-sql`, `BUILTIN\Administrators` or
+ `NT SERVICE\MSSQLSERVER`.
+- A bare account name that the local system can resolve, such as `Administrators`.
+- A well-known name, such as `Everyone` or `NT AUTHORITY\LOCAL SERVICE`.
+- A SID string, such as `S-1-5-32-544`, or the form that `secedit` exports, `*S-1-5-32-544`.
+
+The resource resolves every entry to a SID. An account name that doesn't exist is an error. A SID
+that doesn't resolve to an account is accepted, because Windows keeps rights assigned to deleted
+accounts. Entries that resolve to the same SID count once.
+
+On **Get** and **Export**, the resource returns each holder as `DOMAIN\Name`, as a bare name when
+the account has no domain (for example `Everyone`), or as the SID string when the SID no longer
+resolves to an account. An empty array means that no one holds the right.
+
+### setAction
+
+Expand for setAction property metadata
+
+```yaml
+Type : string
+IsRequired : false
+IsKey : false
+IsReadOnly : false
+IsWriteOnly : true
+DefaultValue : replace
+Enum : [replace, add, remove]
+```
+
+
+
+Defines how the **Set** operation applies the `identity` list and how the **Test** operation
+judges it. The following values are valid:
+
+| Value | Set | Test is in the desired state when |
+|:----------|:-------------------------------------------------------------------------|:----------------------------------------------------|
+| `replace` | The listed accounts become the only holders; other holders are revoked. | The holders are exactly the listed accounts. |
+| `add` | The listed accounts are granted the right; other holders are kept. | Every listed account holds the right. |
+| `remove` | The listed accounts lose the right; other holders are kept. | None of the listed accounts hold the right. |
+
+With `replace`, an empty `identity` list revokes the right from everyone. With `add` or `remove`,
+an empty list changes nothing.
+
+Because this property is write-only, DSC doesn't compare it and it never appears in the actual
+state. When you use `add` or `remove`, the `differingProperties` list that DSC reports may still
+name `identity` even though the resource reports the instance as in the desired state; rely on
+`inDesiredState` for the verdict.
+
+### _inDesiredState
+
+Expand for _inDesiredState property metadata
+
+```yaml
+Type : boolean
+IsRequired : false
+IsKey : false
+IsReadOnly : true
+IsWriteOnly : false
+```
+
+
+
+Returned by the **Test** operation only. Indicates whether the current holders of the right
+satisfy `identity` under the chosen `setAction`.
+
+## Instance validating schema
+
+The following snippet contains the JSON Schema that validates an instance of the resource. The
+`enum` for `right` is abbreviated; it contains the 45 constants listed in the
+[Description](#description).
+
+```json
+{
+ "type": "object",
+ "required": ["right"],
+ "additionalProperties": false,
+ "properties": {
+ "right": {
+ "type": "string",
+ "enum": ["SeTrustedCredManAccessPrivilege", "SeNetworkLogonRight", "..."]
+ },
+ "identity": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "minLength": 1
+ },
+ "uniqueItems": true
+ },
+ "setAction": {
+ "type": "string",
+ "writeOnly": true,
+ "default": "replace",
+ "enum": ["replace", "add", "remove"]
+ },
+ "_inDesiredState": {
+ "type": "boolean",
+ "readOnly": true
+ },
+ "_metadata": {
+ "type": "object",
+ "readOnly": true,
+ "properties": {
+ "whatIf": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+## Exit codes
+
+The resource returns the following exit codes from operations:
+
+- [0](#exit-code-0) - Success
+- [1](#exit-code-1) - Invalid arguments
+- [2](#exit-code-2) - Invalid input
+- [3](#exit-code-3) - Security policy error
+- [4](#exit-code-4) - Elevation required
+
+### Exit code 0
+
+Indicates the resource operation completed without errors.
+
+### Exit code 1
+
+Indicates the resource operation failed because required arguments were missing or the operation
+name was not recognized.
+
+### Exit code 2
+
+Indicates the resource operation failed because the JSON input could not be deserialized into a
+valid instance, the `right` isn't a known user right, an entry in `identity` isn't a valid SID or
+an existing account, or `identity` was omitted from a **Set** or **Test** operation.
+
+### Exit code 3
+
+Indicates the resource operation failed due to an error raised by the Local Security Authority or
+another Windows API, or the result could not be serialized.
+
+### Exit code 4
+
+Indicates the resource operation failed because the process isn't elevated. Reading or changing
+user rights assignments requires an elevated process.
+
+## See also
+
+- [Microsoft.Windows/Registry resource][06]
+- [DSC resource capabilities][01]
+- [DSC resource properties][07]
+
+
+[01]: ../../../../../concepts/resources/capabilities.md
+[02]: ./examples/get-user-rights-assignment.md
+[03]: ./examples/configure-user-rights-assignment.md
+[04]: ../../../../../concepts/resources/properties.md#key-resource-properties
+[05]: ../../../../../concepts/resources/properties.md#read-only-resource-properties
+[06]: ../Registry/index.md
+[07]: ../../../../../concepts/resources/properties.md
diff --git a/docs/reference/resources/overview.md b/docs/reference/resources/overview.md
index c6324dfa9..99a7dd1f4 100644
--- a/docs/reference/resources/overview.md
+++ b/docs/reference/resources/overview.md
@@ -24,6 +24,7 @@ This document lists the available resources and links to the reference documenta
- [Microsoft.Windows/RebootPending](./Microsoft/Windows/RebootPending/index.md)
- [Microsoft.Windows/Registry](./Microsoft/Windows/Registry/index.md)
- [Microsoft.Windows/Service](./Microsoft/Windows/Service/index.md)
+- [Microsoft.Windows/UserRightsAssignment](./Microsoft/Windows/UserRightsAssignment/index.md)
- [Microsoft.Windows/WindowsPowerShell](./Microsoft/Windows/WindowsPowerShell/index.md)
- [Microsoft.Windows/WMI](./Microsoft/Windows/WMI/index.md)
@@ -66,6 +67,7 @@ The following built-in resources change the state of a machine directly:
- [Microsoft.Windows/OptionalFeatureList](./Microsoft/Windows/OptionalFeatureList/index.md)
- [Microsoft.Windows/Registry](./Microsoft/Windows/Registry/index.md)
- [Microsoft.Windows/Service](./Microsoft/Windows/Service/index.md)
+- [Microsoft.Windows/UserRightsAssignment](./Microsoft/Windows/UserRightsAssignment/index.md)
## Built-in debugging resources
diff --git a/resources/windows_security_policy/.project.data.json b/resources/windows_security_policy/.project.data.json
new file mode 100644
index 000000000..c2aabf7d0
--- /dev/null
+++ b/resources/windows_security_policy/.project.data.json
@@ -0,0 +1,14 @@
+{
+ "Name": "windows_security_policy",
+ "Kind": "Resource",
+ "IsRust": true,
+ "SupportedPlatformOS": "Windows",
+ "Binaries": [
+ "windows_security_policy"
+ ],
+ "CopyFiles": {
+ "Windows": [
+ "windows_security_policy.dsc.manifests.json"
+ ]
+ }
+}
diff --git a/resources/windows_security_policy/Cargo.toml b/resources/windows_security_policy/Cargo.toml
new file mode 100644
index 000000000..79c12abca
--- /dev/null
+++ b/resources/windows_security_policy/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "windows_security_policy"
+version = "0.1.0"
+edition = "2024"
+
+[package.metadata.i18n]
+available-locales = ["en-us"]
+default-locale = "en-us"
+load-path = "locales"
+
+[dependencies]
+rust-i18n = { workspace = true }
+serde = { workspace = true }
+serde_json = { workspace = true }
+
+[target.'cfg(windows)'.dependencies]
+windows = { workspace = true }
+dsc-lib-security_context = { workspace = true }
diff --git a/resources/windows_security_policy/build.rs b/resources/windows_security_policy/build.rs
new file mode 100644
index 000000000..315b846a1
--- /dev/null
+++ b/resources/windows_security_policy/build.rs
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+fn main() {
+ // Always rebuild if translations are updated.
+ println!("cargo::rerun-if-changed=locales");
+}
diff --git a/resources/windows_security_policy/locales/en-us.toml b/resources/windows_security_policy/locales/en-us.toml
new file mode 100644
index 000000000..8f41dedcf
--- /dev/null
+++ b/resources/windows_security_policy/locales/en-us.toml
@@ -0,0 +1,36 @@
+_version = 1
+
+[main]
+missingOperation = "Missing operation. Usage: windows_security_policy [--input ] [--what-if]"
+unknownOperation = "Unknown operation: '%{operation}'. Expected: get, set, test, or export"
+missingTarget = "Missing resource target. Expected: user-right"
+unknownTarget = "Unknown resource target: '%{target}'. Expected: user-right"
+missingInput = "Missing --input argument"
+missingInputValue = "Missing value for --input argument"
+invalidJson = "Invalid JSON input: %{error}"
+serializeFailed = "Failed to serialize output: %{error}"
+windowsOnly = "This resource is only supported on Windows"
+elevationRequired = "Managing Windows user rights assignments requires elevation. Run DSC from an elevated process."
+
+[rights]
+unknownRight = "Unknown user right '%{right}'. Specify a user right constant such as 'SeServiceLogonRight'."
+
+[sid]
+emptyIdentity = "An entry in 'identity' is empty"
+invalidSid = "'%{identity}' is not a valid SID string"
+unknownAccount = "The account '%{identity}' could not be found. Specify 'DOMAIN\\Name', a well-known account name, or a SID string."
+lookupNameFailed = "Failed to look up the account '%{identity}': %{error}"
+lookupSidFailed = "Failed to look up the name for a SID: %{error}"
+sidToStringFailed = "Failed to convert a SID to a string: %{error}"
+
+[lsa]
+openPolicyFailed = "Failed to open the local security policy: %{error}"
+stringTooLong = "The value '%{value}' is too long for the Local Security Authority"
+enumerateFailed = "Failed to enumerate the accounts holding '%{right}': %{error}"
+addFailed = "Failed to grant '%{right}': %{error}"
+removeFailed = "Failed to revoke '%{right}': %{error}"
+
+[userRights]
+identityRequired = "'identity' is required for the set and test operations"
+whatIfGrant = "Would grant '%{right}' to '%{identity}'"
+whatIfRevoke = "Would revoke '%{right}' from '%{identity}'"
diff --git a/resources/windows_security_policy/src/lsa.rs b/resources/windows_security_policy/src/lsa.rs
new file mode 100644
index 000000000..b12478a78
--- /dev/null
+++ b/resources/windows_security_policy/src/lsa.rs
@@ -0,0 +1,234 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Local Security Authority policy access: enumerate, grant and revoke account rights.
+//!
+//! Every buffer handed out by an `Lsa*` API is owned by the LSA and must be released with
+//! `LsaFreeMemory`; the RAII guards below do that on drop so early returns cannot leak.
+
+use crate::sid::Sid;
+use crate::types::SecurityPolicyError;
+use rust_i18n::t;
+use std::ffi::c_void;
+use std::ptr;
+use windows::Win32::Foundation::{
+ NTSTATUS, STATUS_ACCESS_DENIED, STATUS_NO_MORE_ENTRIES, STATUS_NO_SUCH_PRIVILEGE, STATUS_OBJECT_NAME_NOT_FOUND,
+ STATUS_SUCCESS, WIN32_ERROR,
+};
+use windows::Win32::Security::Authentication::Identity::{
+ LSA_ENUMERATION_INFORMATION, LSA_HANDLE, LSA_OBJECT_ATTRIBUTES, LSA_UNICODE_STRING, LsaAddAccountRights, LsaClose,
+ LsaEnumerateAccountsWithUserRight, LsaFreeMemory, LsaNtStatusToWinError, LsaOpenPolicy, LsaRemoveAccountRights,
+ POLICY_CREATE_ACCOUNT, POLICY_LOOKUP_NAMES, POLICY_VIEW_LOCAL_INFORMATION,
+};
+use windows::core::PWSTR;
+
+/// Access needed to enumerate account rights and look up names.
+pub const POLICY_READ: u32 = POLICY_VIEW_LOCAL_INFORMATION.cast_unsigned() | POLICY_LOOKUP_NAMES.cast_unsigned();
+/// Access needed to grant and revoke account rights (adding a right may create an LSA account object).
+pub const POLICY_WRITE: u32 = POLICY_READ | POLICY_CREATE_ACCOUNT.cast_unsigned();
+
+/// The Win32 error message for an `NTSTATUS`.
+fn nt_error(status: NTSTATUS) -> String {
+ // SAFETY: pure conversion with no preconditions.
+ let win32 = unsafe { LsaNtStatusToWinError(status) };
+ windows::core::Error::from_hresult(WIN32_ERROR(win32).to_hresult()).to_string()
+}
+
+/// Map an `NTSTATUS` to a resource error. Access denied means the process is not elevated.
+fn check(status: NTSTATUS, context: impl FnOnce(String) -> String) -> Result<(), SecurityPolicyError> {
+ if status == STATUS_SUCCESS {
+ return Ok(());
+ }
+ if status == STATUS_ACCESS_DENIED {
+ return Err(SecurityPolicyError::ElevationRequired);
+ }
+ Err(SecurityPolicyError::Policy(context(nt_error(status))))
+}
+
+/// An `LSA_UNICODE_STRING` together with the UTF-16 buffer it points into.
+pub struct LsaString {
+ _buffer: Vec,
+ raw: LSA_UNICODE_STRING,
+}
+
+impl LsaString {
+ /// Build an LSA string. `Length` counts bytes without the terminator, `MaximumLength` with it.
+ ///
+ /// # Errors
+ ///
+ /// Returns `InvalidInput` when the value does not fit the 16-bit length fields.
+ pub fn new(value: &str) -> Result {
+ let mut buffer: Vec = value.encode_utf16().chain(std::iter::once(0)).collect();
+ let too_long = || SecurityPolicyError::InvalidInput(t!("lsa.stringTooLong", value = value).to_string());
+ let length = u16::try_from((buffer.len() - 1) * 2).map_err(|_| too_long())?;
+ let maximum_length = u16::try_from(buffer.len() * 2).map_err(|_| too_long())?;
+ let raw = LSA_UNICODE_STRING {
+ Length: length,
+ MaximumLength: maximum_length,
+ Buffer: PWSTR(buffer.as_mut_ptr()),
+ };
+ // Moving `buffer` moves the Vec header only; the heap pointer `raw.Buffer` holds stays valid.
+ Ok(Self { _buffer: buffer, raw })
+ }
+
+ pub fn as_raw(&self) -> &LSA_UNICODE_STRING {
+ &self.raw
+ }
+}
+
+/// An array allocated by an `Lsa*` API and released with `LsaFreeMemory`.
+struct LsaBuffer {
+ ptr: *mut T,
+ count: usize,
+}
+
+impl LsaBuffer {
+ fn as_slice(&self) -> &[T] {
+ if self.ptr.is_null() || self.count == 0 {
+ &[]
+ } else {
+ // SAFETY: the LSA allocated `count` contiguous items at `ptr`; they stay valid until
+ // `LsaFreeMemory` runs in `drop`.
+ unsafe { std::slice::from_raw_parts(self.ptr, self.count) }
+ }
+ }
+}
+
+impl Drop for LsaBuffer {
+ fn drop(&mut self) {
+ if !self.ptr.is_null() {
+ // SAFETY: `ptr` came from an `Lsa*` API and is released exactly once.
+ unsafe {
+ let _ = LsaFreeMemory(Some(self.ptr.cast()));
+ }
+ }
+ }
+}
+
+/// An open handle to the local security policy, closed on drop.
+pub struct LsaPolicy(LSA_HANDLE);
+
+impl LsaPolicy {
+ /// Open the local policy with the given access mask (`POLICY_READ` or `POLICY_WRITE`).
+ ///
+ /// # Errors
+ ///
+ /// Returns `ElevationRequired` when access is denied, or a policy error.
+ pub fn open(access: u32) -> Result {
+ let attributes = LSA_OBJECT_ATTRIBUTES::default();
+ let mut handle = LSA_HANDLE::default();
+ // SAFETY: an all-zero attributes block is what the API requires; `None` selects the local
+ // system; `handle` is a valid out pointer.
+ let status = unsafe { LsaOpenPolicy(None, &attributes, access, &mut handle) };
+ check(status, |error| t!("lsa.openPolicyFailed", error = error).to_string())?;
+ Ok(Self(handle))
+ }
+
+ /// The SIDs of every account that holds `right`. An unheld right yields an empty list.
+ ///
+ /// # Errors
+ ///
+ /// Returns `InvalidInput` for a right the LSA does not know, `ElevationRequired` when the
+ /// process is not elevated, or a policy error.
+ pub fn holders(&self, right: &str) -> Result, SecurityPolicyError> {
+ let name = LsaString::new(right)?;
+ let mut buffer: *mut c_void = ptr::null_mut();
+ let mut count: u32 = 0;
+ // SAFETY: `name` outlives the call; the out pointers are valid.
+ let status = unsafe {
+ LsaEnumerateAccountsWithUserRight(self.0, Some(ptr::from_ref(name.as_raw())), &mut buffer, &mut count)
+ };
+ if status == STATUS_NO_MORE_ENTRIES {
+ return Ok(Vec::new());
+ }
+ if status == STATUS_NO_SUCH_PRIVILEGE {
+ return Err(SecurityPolicyError::InvalidInput(t!("rights.unknownRight", right = right).to_string()));
+ }
+ check(status, |error| t!("lsa.enumerateFailed", right = right, error = error).to_string())?;
+ let entries = LsaBuffer:: {
+ ptr: buffer.cast(),
+ count: count as usize,
+ };
+ // Copy every SID out before `entries` drops.
+ // SAFETY: each entry holds a valid SID that lives as long as `entries`.
+ let sids = entries.as_slice().iter().map(|entry| unsafe { Sid::from_psid(entry.Sid) }).collect();
+ Ok(sids)
+ }
+
+ /// Grant `right` to the account with `sid`.
+ ///
+ /// # Errors
+ ///
+ /// Returns `ElevationRequired` or a policy error.
+ pub fn add_right(&self, sid: &Sid, right: &str) -> Result<(), SecurityPolicyError> {
+ let name = LsaString::new(right)?;
+ // SAFETY: `sid` and `name` are valid for the duration of the call.
+ let status = unsafe { LsaAddAccountRights(self.0, sid.as_psid(), std::slice::from_ref(name.as_raw())) };
+ check(status, |error| t!("lsa.addFailed", right = right, error = error).to_string())
+ }
+
+ /// Revoke `right` from the account with `sid`. Revoking from an account that holds no rights
+ /// at all is treated as already done.
+ ///
+ /// # Errors
+ ///
+ /// Returns `ElevationRequired` or a policy error.
+ pub fn remove_right(&self, sid: &Sid, right: &str) -> Result<(), SecurityPolicyError> {
+ let name = LsaString::new(right)?;
+ // `allrights` is always false: `true` would strip every right from the account.
+ // SAFETY: `sid` and `name` are valid for the duration of the call.
+ let status =
+ unsafe { LsaRemoveAccountRights(self.0, sid.as_psid(), false, Some(std::slice::from_ref(name.as_raw()))) };
+ if status == STATUS_OBJECT_NAME_NOT_FOUND {
+ return Ok(());
+ }
+ check(status, |error| t!("lsa.removeFailed", right = right, error = error).to_string())
+ }
+}
+
+impl Drop for LsaPolicy {
+ fn drop(&mut self) {
+ // SAFETY: the handle was returned by `LsaOpenPolicy` and is closed exactly once.
+ unsafe {
+ let _ = LsaClose(self.0);
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::sid::parse_string_sid;
+ use dsc_lib_security_context::{SecurityContext, get_security_context};
+
+ #[test]
+ fn lsa_strings_count_bytes() {
+ let value = LsaString::new("SeServiceLogonRight").unwrap();
+ assert_eq!(value.as_raw().Length, 38);
+ assert_eq!(value.as_raw().MaximumLength, 40);
+ // SAFETY: the buffer is null-terminated and owned by `value`.
+ let text = unsafe { value.as_raw().Buffer.to_string() }.unwrap();
+ assert_eq!(text, "SeServiceLogonRight");
+ let empty = LsaString::new("").unwrap();
+ assert_eq!(empty.as_raw().Length, 0);
+ assert_eq!(empty.as_raw().MaximumLength, 2);
+ }
+
+ #[test]
+ fn read_policy_opens_in_any_context() {
+ LsaPolicy::open(POLICY_READ).unwrap();
+ }
+
+ #[test]
+ fn enumeration_follows_the_security_context() {
+ let policy = LsaPolicy::open(POLICY_READ).unwrap();
+ let holders = policy.holders("SeChangeNotifyPrivilege");
+ if get_security_context() == SecurityContext::Admin {
+ let holders = holders.unwrap();
+ assert!(holders.contains(&parse_string_sid("S-1-1-0").unwrap()), "Everyone should hold SeChangeNotifyPrivilege");
+ assert!(matches!(policy.holders("SeNotARight"), Err(SecurityPolicyError::InvalidInput(_))));
+ } else {
+ assert!(matches!(holders, Err(SecurityPolicyError::ElevationRequired)));
+ }
+ }
+}
diff --git a/resources/windows_security_policy/src/main.rs b/resources/windows_security_policy/src/main.rs
new file mode 100644
index 000000000..d197155b3
--- /dev/null
+++ b/resources/windows_security_policy/src/main.rs
@@ -0,0 +1,217 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#[cfg_attr(not(windows), allow(dead_code))]
+mod rights;
+#[cfg_attr(not(windows), allow(dead_code))]
+mod types;
+
+#[cfg(windows)]
+mod lsa;
+#[cfg(windows)]
+mod sid;
+#[cfg(windows)]
+mod user_rights;
+
+use rust_i18n::t;
+use std::process::ExitCode;
+
+rust_i18n::i18n!("locales", fallback = "en-us");
+
+const EXIT_SUCCESS: u8 = 0;
+const EXIT_INVALID_ARGS: u8 = 1;
+const EXIT_INVALID_INPUT: u8 = 2;
+const EXIT_SECURITY_POLICY_ERROR: u8 = 3;
+const EXIT_ELEVATION_REQUIRED: u8 = 4;
+
+/// Write a JSON error object to stderr: `{"error":""}`
+fn write_error(message: &str) {
+ eprintln!("{}", serde_json::json!({"error": message}));
+}
+
+#[cfg(not(windows))]
+fn main() -> ExitCode {
+ write_error(&t!("main.windowsOnly"));
+ ExitCode::from(EXIT_SECURITY_POLICY_ERROR)
+}
+
+#[cfg(windows)]
+fn main() -> ExitCode {
+ use types::UserRightsAssignment;
+
+ let args: Vec = std::env::args().collect();
+
+ if args.len() < 2 {
+ write_error(&t!("main.missingOperation"));
+ return ExitCode::from(EXIT_INVALID_ARGS);
+ }
+
+ let operation = args[1].as_str();
+ if !matches!(operation, "get" | "set" | "test" | "export") {
+ write_error(&t!("main.unknownOperation", operation = operation));
+ return ExitCode::from(EXIT_INVALID_ARGS);
+ }
+
+ let Some(target) = args.get(2) else {
+ write_error(&t!("main.missingTarget"));
+ return ExitCode::from(EXIT_INVALID_ARGS);
+ };
+
+ let input_json = match parse_input_arg(&args) {
+ Ok(input) => input,
+ Err(code) => return code,
+ };
+ let what_if = parse_what_if_flag(&args);
+
+ match (operation, target.as_str()) {
+ ("get", "user-right") => with_input(input_json, |input: UserRightsAssignment| user_rights::get(&input)),
+ ("set", "user-right") => with_input(input_json, |input: UserRightsAssignment| user_rights::set(&input, what_if)),
+ ("test", "user-right") => with_input(input_json, |input: UserRightsAssignment| user_rights::test(&input)),
+ ("export", "user-right") => finish_many(user_rights::export()),
+ (_, target) => {
+ write_error(&t!("main.unknownTarget", target = target));
+ ExitCode::from(EXIT_INVALID_ARGS)
+ }
+ }
+}
+
+/// Deserialize the required JSON input, or return the exit code to use.
+#[cfg(windows)]
+fn require_input(input_json: Option) -> Result {
+ let Some(json) = input_json else {
+ write_error(&t!("main.missingInput"));
+ return Err(ExitCode::from(EXIT_INVALID_ARGS));
+ };
+ serde_json::from_str(&json).map_err(|error| {
+ write_error(&t!("main.invalidJson", error = error.to_string()));
+ ExitCode::from(EXIT_INVALID_INPUT)
+ })
+}
+
+/// Run an operation that needs input and print its single result.
+#[cfg(windows)]
+fn with_input(input_json: Option, operation: impl FnOnce(T) -> Result) -> ExitCode
+where
+ T: serde::de::DeserializeOwned,
+ U: serde::Serialize,
+{
+ match require_input::(input_json) {
+ Ok(input) => finish(operation(input)),
+ Err(code) => code,
+ }
+}
+
+/// Serialize a value to JSON and print it to stdout, or return the exit code to use.
+#[cfg(windows)]
+fn print_json(value: &impl serde::Serialize) -> Result<(), ExitCode> {
+ match serde_json::to_string(value) {
+ Ok(json) => {
+ println!("{json}");
+ Ok(())
+ }
+ Err(error) => {
+ write_error(&t!("main.serializeFailed", error = error.to_string()));
+ Err(ExitCode::from(EXIT_SECURITY_POLICY_ERROR))
+ }
+ }
+}
+
+/// Map a resource error to its documented exit code.
+#[cfg(windows)]
+fn exit_code_for(error: &types::SecurityPolicyError) -> ExitCode {
+ use types::SecurityPolicyError;
+
+ ExitCode::from(match error {
+ SecurityPolicyError::ElevationRequired => EXIT_ELEVATION_REQUIRED,
+ SecurityPolicyError::InvalidInput(_) => EXIT_INVALID_INPUT,
+ SecurityPolicyError::Policy(_) => EXIT_SECURITY_POLICY_ERROR,
+ })
+}
+
+/// Print a single result as JSON, or write the error and return its exit code.
+#[cfg(windows)]
+fn finish(result: Result) -> ExitCode {
+ match result {
+ Ok(value) => match print_json(&value) {
+ Ok(()) => ExitCode::from(EXIT_SUCCESS),
+ Err(code) => code,
+ },
+ Err(error) => {
+ write_error(&error.to_string());
+ exit_code_for(&error)
+ }
+ }
+}
+
+/// Print each exported instance on its own line, or write the error and return its exit code.
+#[cfg(windows)]
+fn finish_many(result: Result, types::SecurityPolicyError>) -> ExitCode {
+ match result {
+ Ok(values) => {
+ for value in &values {
+ if let Err(code) = print_json(value) {
+ return code;
+ }
+ }
+ ExitCode::from(EXIT_SUCCESS)
+ }
+ Err(error) => {
+ write_error(&error.to_string());
+ exit_code_for(&error)
+ }
+ }
+}
+
+/// Parse the `--input ` argument. Arguments 1 and 2 are the operation and the target.
+#[cfg(windows)]
+fn parse_input_arg(args: &[String]) -> Result