diff --git a/include/wil/token_helpers.h b/include/wil/token_helpers.h index bce91206..e49ef2c1 100644 --- a/include/wil/token_helpers.h +++ b/include/wil/token_helpers.h @@ -555,6 +555,11 @@ namespace details SID_IDENTIFIER_AUTHORITY IdentifierAuthority; DWORD SubAuthority[AuthorityCount]; + static constexpr size_t byte_size() + { + return 8 + 4 * AuthorityCount; + } + PSID get() { return reinterpret_cast(this); @@ -608,6 +613,472 @@ constexpr auto make_static_nt_sid(Ts&&... subAuthorities) return make_static_sid(SECURITY_NT_AUTHORITY, wistd::forward(subAuthorities)...); } +#ifdef _HAS_CXX20 + +// Class-type NTTPs: MSVC defines __cpp_nontype_template_args; Clang supports it in C++20+ mode without the macro. +#if __cpp_nontype_template_args >= 201911L || (defined(__clang__) && __clang_major__ >= 12 && _HAS_CXX20) +#define __WIL_HAS_CLASS_NTTP 1 +#endif + +/// @cond +namespace details +{ +#ifdef __WIL_HAS_CLASS_NTTP + // Fixed-size string for use as a non-type template parameter (C++20). + template + struct fixed_string + { + char data[N]{}; + + consteval fixed_string(const char (&str)[N]) + { + for (size_t i = 0; i < N; ++i) + data[i] = str[i]; + } + + static constexpr size_t length = N - 1; + }; + + // Count '-' characters in a fixed_string. + template + consteval size_t count_dashes() + { + size_t count = 0; + for (size_t i = 0; i < S.length; ++i) + { + if (S.data[i] == '-') + ++count; + } + return count; + } + + // Parse an unsigned decimal integer from a string starting at pos, advancing pos past the digits. + consteval uint64_t parse_uint(const char* str, size_t len, size_t& pos) + { + uint64_t value = 0; + while (pos < len && str[pos] >= '0' && str[pos] <= '9') + { + value = value * 10 + static_cast(str[pos] - '0'); + ++pos; + } + return value; + } + + // Parse a SID string "S-1-{authority}-{sub1}-{sub2}-..." into a static_sid_t. + template + consteval auto parse_sid_string() + { + // "S-R-IA" has 2 dashes; each sub-authority adds one more. + constexpr size_t subAuthCount = count_dashes() - 2; + static_assert(subAuthCount <= SID_MAX_SUB_AUTHORITIES, "too many sub authorities in SID string"); + + static_sid_t result{}; + size_t pos = 0; + + // Expect 'S' or 's' + ++pos; // skip 'S' + ++pos; // skip '-' + + // Revision + result.Revision = static_cast(parse_uint(S.data, S.length, pos)); + ++pos; // skip '-' + + // Identifier authority (big-endian 6-byte value; common authorities fit in the low bytes) + uint64_t authority = parse_uint(S.data, S.length, pos); + result.IdentifierAuthority = {}; + result.IdentifierAuthority.Value[5] = static_cast(authority & 0xFF); + result.IdentifierAuthority.Value[4] = static_cast((authority >> 8) & 0xFF); + result.IdentifierAuthority.Value[3] = static_cast((authority >> 16) & 0xFF); + result.IdentifierAuthority.Value[2] = static_cast((authority >> 24) & 0xFF); + result.IdentifierAuthority.Value[1] = static_cast((authority >> 32) & 0xFF); + result.IdentifierAuthority.Value[0] = static_cast((authority >> 40) & 0xFF); + + result.SubAuthorityCount = static_cast(subAuthCount); + + // Sub-authorities + for (size_t i = 0; i < subAuthCount; ++i) + { + ++pos; // skip '-' + result.SubAuthority[i] = static_cast(parse_uint(S.data, S.length, pos)); + } + + return result; + } +#endif // __WIL_HAS_CLASS_NTTP + + // Byte-level constexpr writers for building self-relative security descriptors. + constexpr void write_byte(uint8_t* dest, size_t& offset, uint8_t value) + { + dest[offset++] = value; + } + + constexpr void write_word(uint8_t* dest, size_t& offset, uint16_t value) + { + dest[offset++] = static_cast(value & 0xFF); + dest[offset++] = static_cast((value >> 8) & 0xFF); + } + + constexpr void write_dword(uint8_t* dest, size_t& offset, uint32_t value) + { + dest[offset++] = static_cast(value & 0xFF); + dest[offset++] = static_cast((value >> 8) & 0xFF); + dest[offset++] = static_cast((value >> 16) & 0xFF); + dest[offset++] = static_cast((value >> 24) & 0xFF); + } + + template + constexpr void write_sid(uint8_t* dest, size_t& offset, const static_sid_t& sid) + { + write_byte(dest, offset, sid.Revision); + write_byte(dest, offset, sid.SubAuthorityCount); + for (int i = 0; i < 6; ++i) + { + write_byte(dest, offset, sid.IdentifierAuthority.Value[i]); + } + for (size_t i = 0; i < N; ++i) + { + write_dword(dest, offset, sid.SubAuthority[i]); + } + } + + template + constexpr size_t sid_byte_size(const static_sid_t&) + { + return 8 + 4 * N; + } + + // A typed ACE containing the ACE header fields and an embedded SID. + // Layout mirrors ACCESS_ALLOWED_ACE / ACCESS_DENIED_ACE (same binary layout). + template + struct static_ace_t + { + uint8_t AceType; + uint8_t AceFlags; + uint32_t Mask; + static_sid_t Sid; + + static constexpr size_t byte_size() + { + // ACE_HEADER (Type:1 + Flags:1 + Size:2) + Mask (4) + SID (8 + 4*N) + return 4 + 4 + 8 + 4 * SubAuthorityCount; + } + }; + + template + constexpr void write_ace(uint8_t* dest, size_t& offset, const static_ace_t& ace) + { + auto aceSize = static_cast(static_ace_t::byte_size()); + // ACE_HEADER + write_byte(dest, offset, ace.AceType); + write_byte(dest, offset, ace.AceFlags); + write_word(dest, offset, aceSize); + // Mask + write_dword(dest, offset, ace.Mask); + // SID (replaces the SidStart DWORD in the Win32 ACE struct) + write_sid(dest, offset, ace.Sid); + } + + // Compute the total byte size of a list of ACEs from their types. + template + constexpr size_t total_ace_size() + { + return (0 + ... + Aces::byte_size()); + } + + // Sentinel type used when no group SID is desired. + struct no_sid_t + { + static constexpr size_t byte_size() + { + return 0; + } + }; + + constexpr size_t sid_byte_size(const no_sid_t&) + { + return 0; + } + + constexpr void write_sid(uint8_t*, size_t&, const no_sid_t&) + { + } + + // The result type: a fixed-size byte array that is layout-compatible with SECURITY_DESCRIPTOR_RELATIVE. + template + struct static_security_descriptor_t + { + alignas(uint32_t) uint8_t data[TotalSize]{}; + + PSECURITY_DESCRIPTOR get() + { + return reinterpret_cast(data); + } + + PSECURITY_DESCRIPTOR get() const + { + return reinterpret_cast(const_cast(data)); + } + }; + + // Tagged wrapper types for sd_owner() / sd_group() convenience helpers. + template + struct sd_owner_t + { + SidType sid; + + static constexpr size_t byte_size() + { + return SidType::byte_size(); + } + }; + + template + struct sd_group_t + { + SidType sid; + + static constexpr size_t byte_size() + { + return SidType::byte_size(); + } + }; + + template + constexpr void write_sid(uint8_t* dest, size_t& offset, const sd_owner_t& owner) + { + write_sid(dest, offset, owner.sid); + } + + template + constexpr void write_sid(uint8_t* dest, size_t& offset, const sd_group_t& group) + { + write_sid(dest, offset, group.sid); + } + + // Runtime/constexpr check: deny ACEs must precede allow ACEs in the variadic pack. + constexpr bool deny_before_allow_check(const uint8_t* types, size_t count) + { + bool seenAllow = false; + for (size_t i = 0; i < count; ++i) + { + if (types[i] == ACCESS_ALLOWED_ACE_TYPE) + { + seenAllow = true; + } + else if (types[i] == ACCESS_DENIED_ACE_TYPE && seenAllow) + { + return false; // deny after allow + } + } + return true; + } +} // namespace details +/// @endcond + +//! Sentinel value for omitting the group SID in make_self_relative_sd. +inline constexpr details::no_sid_t no_sid{}; + +#ifdef __WIL_HAS_CLASS_NTTP +/** Constructs a static SID from a SID string literal at compile time. +The string must be in standard SID notation: "S-{revision}-{authority}-{sub1}-{sub2}-..." +@code +constexpr auto localSystem = wil::make_static_sid<"S-1-5-18">(); +constexpr auto admins = wil::make_static_sid<"S-1-5-32-544">(); +auto sd = wil::make_self_relative_sd( + wil::sd_owner(wil::make_static_sid<"S-1-5-32-544">()), + wil::sd_group(wil::no_sid), + wil::make_allow_ace(GENERIC_READ, wil::make_static_sid<"S-1-5-11">())); +@endcode +@return A static_sid_t with the parsed SID. +*/ +template +consteval auto make_static_sid() +{ + return details::parse_sid_string(); +} +#endif // __WIL_HAS_CLASS_NTTP + +//! Tags a SID as the owner for use with make_self_relative_sd. +template +constexpr auto sd_owner(const T& sid) +{ + return details::sd_owner_t{sid}; +} + +//! Tags a SID (or no_sid) as the group for use with make_self_relative_sd. +template +constexpr auto sd_group(const T& sid) +{ + return details::sd_group_t{sid}; +} + +#ifdef __WIL_HAS_CLASS_NTTP +//! Tags a SID parsed from a string literal as the owner. Example: `wil::sd_owner<"S-1-5-18">()` +template +consteval auto sd_owner() +{ + return details::sd_owner_t{details::parse_sid_string()}; +} + +//! Tags a SID parsed from a string literal as the group. Example: `wil::sd_group<"S-1-5-32-545">()` +template +consteval auto sd_group() +{ + return details::sd_group_t{details::parse_sid_string()}; +} + +//! Constructs a constexpr ACCESS_ALLOWED ACE from a SID string literal. +template +consteval auto make_allow_ace(uint32_t mask) +{ + return details::static_ace_t{ACCESS_ALLOWED_ACE_TYPE, uint8_t{0}, mask, details::parse_sid_string()}; +} + +//! Constructs a constexpr ACCESS_ALLOWED ACE with inheritance flags from a SID string literal. +template +consteval auto make_allow_ace(uint8_t flags, uint32_t mask) +{ + return details::static_ace_t{ACCESS_ALLOWED_ACE_TYPE, flags, mask, details::parse_sid_string()}; +} + +//! Constructs a constexpr ACCESS_DENIED ACE from a SID string literal. +template +consteval auto make_deny_ace(uint32_t mask) +{ + return details::static_ace_t{ACCESS_DENIED_ACE_TYPE, uint8_t{0}, mask, details::parse_sid_string()}; +} + +//! Constructs a constexpr ACCESS_DENIED ACE with inheritance flags from a SID string literal. +template +consteval auto make_deny_ace(uint8_t flags, uint32_t mask) +{ + return details::static_ace_t{ACCESS_DENIED_ACE_TYPE, flags, mask, details::parse_sid_string()}; +} +#endif // __WIL_HAS_CLASS_NTTP +/** Constructs a constexpr ACCESS_ALLOWED ACE with the given access mask and SID. +@code +auto ace = wil::make_allow_ace(GENERIC_READ, wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID)); +@endcode +@param mask The ACCESS_MASK for this ACE. +@param sid A static SID identifying the trustee. +@return A static_ace_t suitable for passing to make_self_relative_sd. +*/ +template +constexpr auto make_allow_ace(ACCESS_MASK mask, const details::static_sid_t& sid) +{ + return details::static_ace_t{ACCESS_ALLOWED_ACE_TYPE, 0, mask, sid}; +} + +/** Constructs a constexpr ACCESS_ALLOWED ACE with inheritance flags. +@param flags ACE inheritance flags (e.g. CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE). +@param mask The ACCESS_MASK for this ACE. +@param sid A static SID identifying the trustee. +*/ +template +constexpr auto make_allow_ace(uint8_t flags, uint32_t mask, const details::static_sid_t& sid) +{ + return details::static_ace_t{ACCESS_ALLOWED_ACE_TYPE, flags, mask, sid}; +} + +/** Constructs a constexpr ACCESS_DENIED ACE with the given access mask and SID. +@param mask The ACCESS_MASK for this ACE. +@param sid A static SID identifying the trustee. +*/ +template +constexpr auto make_deny_ace(uint32_t mask, const details::static_sid_t& sid) +{ + return details::static_ace_t{ACCESS_DENIED_ACE_TYPE, 0, mask, sid}; +} + +/** Constructs a constexpr ACCESS_DENIED ACE with inheritance flags. +@param flags ACE inheritance flags (e.g. CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE). +@param mask The ACCESS_MASK for this ACE. +@param sid A static SID identifying the trustee. +*/ +template +constexpr auto make_deny_ace(uint8_t flags, uint32_t mask, const details::static_sid_t& sid) +{ + return details::static_ace_t{ACCESS_DENIED_ACE_TYPE, flags, mask, sid}; +} + +/** Constructs a constexpr self-relative SECURITY_DESCRIPTOR from an owner SID, group SID, and a set of ACEs. +The resulting structure is a contiguous byte array laid out as a valid self-relative security descriptor +that can be passed directly to any Win32 API accepting PSECURITY_DESCRIPTOR. + +Deny ACEs must precede allow ACEs in the parameter list (enforced at compile time). +Pass wil::no_sid to omit the group SID. + +@code +constexpr auto sd = wil::make_self_relative_sd( + wil::sd_owner(wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS)), + wil::sd_group(wil::no_sid), + wil::make_deny_ace(GENERIC_WRITE, wil::make_static_nt_sid(SECURITY_WORLD_RID)), + wil::make_allow_ace(GENERIC_READ | GENERIC_WRITE, + wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID))); +@endcode + +@param owner The owner SID (a static_sid_t, sd_owner_t, or no_sid). +@param group The group SID (a static_sid_t, sd_group_t, or no_sid). +@param aces One or more ACEs built with make_allow_ace or make_deny_ace. +@return A static_security_descriptor_t containing a valid self-relative SECURITY_DESCRIPTOR. +*/ +template +constexpr auto make_self_relative_sd(const OwnerSid& owner, const GroupSid& group, const Aces&... aces) +{ + static_assert(sizeof...(aces) > 0, "at least one ACE is required"); + + constexpr size_t sdHeaderSize = 20; // SECURITY_DESCRIPTOR_RELATIVE header + constexpr size_t aclHeaderSize = 8; // ACL header + constexpr size_t ownerSize = OwnerSid::byte_size(); + constexpr size_t groupSize = GroupSid::byte_size(); + constexpr size_t acesSize = details::total_ace_size(); + constexpr size_t totalSize = sdHeaderSize + ownerSize + groupSize + aclHeaderSize + acesSize; + + // Deny ACEs must precede allow ACEs — will fail constexpr evaluation if violated. + uint8_t aceTypes[] = {aces.AceType...}; + WI_ASSERT(details::deny_before_allow_check(aceTypes, sizeof...(aces))); + + details::static_security_descriptor_t result{}; + size_t offset = 0; + + // SECURITY_DESCRIPTOR_RELATIVE header + constexpr uint16_t control = SE_SELF_RELATIVE | SE_DACL_PRESENT; + constexpr uint32_t offsetOwner = (ownerSize > 0) ? static_cast(sdHeaderSize) : 0; + constexpr uint32_t offsetGroup = (groupSize > 0) ? static_cast(sdHeaderSize + ownerSize) : 0; + constexpr uint32_t offsetSacl = 0; // no SACL + constexpr uint32_t offsetDacl = static_cast(sdHeaderSize + ownerSize + groupSize); + + details::write_byte(result.data, offset, SECURITY_DESCRIPTOR_REVISION); // Revision + details::write_byte(result.data, offset, 0); // Sbz1 + details::write_word(result.data, offset, control); + details::write_dword(result.data, offset, offsetOwner); + details::write_dword(result.data, offset, offsetGroup); + details::write_dword(result.data, offset, offsetSacl); + details::write_dword(result.data, offset, offsetDacl); + + // Owner SID + details::write_sid(result.data, offset, owner); + + // Group SID + details::write_sid(result.data, offset, group); + + // DACL: ACL header + constexpr auto aclSize = static_cast(aclHeaderSize + acesSize); + constexpr auto aceCount = static_cast(sizeof...(aces)); + details::write_byte(result.data, offset, ACL_REVISION); // AclRevision + details::write_byte(result.data, offset, 0); // Sbz1 + details::write_word(result.data, offset, aclSize); // AclSize + details::write_word(result.data, offset, aceCount); // AceCount + details::write_word(result.data, offset, 0); // Sbz2 + + // ACEs + (details::write_ace(result.data, offset, aces), ...); + + return result; +} + +#endif // _HAS_CXX20 + /** Determines whether a specified security identifier (SID) is enabled in an access token. This function determines whether a security identifier, described by a given set of subauthorities, is enabled in the given access token. Note that only up to eight subauthorities can be passed to this function. diff --git a/tests/TokenHelpersTests.cpp b/tests/TokenHelpersTests.cpp index c99907b2..c6a81a99 100644 --- a/tests/TokenHelpersTests.cpp +++ b/tests/TokenHelpersTests.cpp @@ -280,6 +280,230 @@ TEST_CASE("TokenHelpersTests::StaticSid", "[token_helpers]") REQUIRE(*GetSidSubAuthority(staticSid.get(), 1) == DOMAIN_ALIAS_RID_GUESTS); } +#ifdef _HAS_CXX20 +TEST_CASE("TokenHelpersTests::SelfRelativeSD_BasicAllowAce", "[token_helpers]") +{ + auto ownerSid = wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS); + auto sd = wil::make_self_relative_sd( + ownerSid, + wil::no_sid, + wil::make_allow_ace(GENERIC_READ, wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID))); + + REQUIRE(IsValidSecurityDescriptor(sd.get())); + + // Verify owner + PSID pOwner = nullptr; + BOOL ownerDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorOwner(sd.get(), &pOwner, &ownerDefaulted)); + REQUIRE(pOwner != nullptr); + REQUIRE(EqualSid(pOwner, ownerSid.get())); + + // Verify no group + PSID pGroup = nullptr; + BOOL groupDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorGroup(sd.get(), &pGroup, &groupDefaulted)); + REQUIRE(pGroup == nullptr); + + // Verify DACL present + BOOL daclPresent = FALSE; + PACL pDacl = nullptr; + BOOL daclDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorDacl(sd.get(), &daclPresent, &pDacl, &daclDefaulted)); + REQUIRE(daclPresent); + REQUIRE(pDacl != nullptr); + + // Verify ACE + LPVOID pAce = nullptr; + REQUIRE(GetAce(pDacl, 0, &pAce)); + auto* ace = static_cast(pAce); + REQUIRE(ace->Header.AceType == ACCESS_ALLOWED_ACE_TYPE); + REQUIRE(ace->Mask == GENERIC_READ); + auto aceSid = wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID); + REQUIRE(EqualSid(reinterpret_cast(&ace->SidStart), aceSid.get())); +} + +TEST_CASE("TokenHelpersTests::SelfRelativeSD_DenyAndAllowAces", "[token_helpers]") +{ + auto sd = wil::make_self_relative_sd( + wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS), + wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_USERS), + wil::make_deny_ace(GENERIC_WRITE, wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_GUESTS)), + wil::make_allow_ace(GENERIC_READ | GENERIC_WRITE, wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID))); + + REQUIRE(IsValidSecurityDescriptor(sd.get())); + + // Verify group SID is present + PSID pGroup = nullptr; + BOOL groupDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorGroup(sd.get(), &pGroup, &groupDefaulted)); + REQUIRE(pGroup != nullptr); + auto expectedGroup = wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_USERS); + REQUIRE(EqualSid(pGroup, expectedGroup.get())); + + // Verify DACL with 2 ACEs + BOOL daclPresent = FALSE; + PACL pDacl = nullptr; + BOOL daclDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorDacl(sd.get(), &daclPresent, &pDacl, &daclDefaulted)); + REQUIRE(daclPresent); + REQUIRE(pDacl->AceCount == 2); + + // First ACE: deny + LPVOID pAce = nullptr; + REQUIRE(GetAce(pDacl, 0, &pAce)); + REQUIRE(static_cast(pAce)->Header.AceType == ACCESS_DENIED_ACE_TYPE); + REQUIRE(static_cast(pAce)->Mask == GENERIC_WRITE); + + // Second ACE: allow + REQUIRE(GetAce(pDacl, 1, &pAce)); + REQUIRE(static_cast(pAce)->Header.AceType == ACCESS_ALLOWED_ACE_TYPE); + REQUIRE(static_cast(pAce)->Mask == (GENERIC_READ | GENERIC_WRITE)); +} + +TEST_CASE("TokenHelpersTests::SelfRelativeSD_InheritanceFlags", "[token_helpers]") +{ + auto sd = wil::make_self_relative_sd( + wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS), + wil::no_sid, + wil::make_allow_ace( + static_cast(CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE), + GENERIC_READ, + wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID))); + + REQUIRE(IsValidSecurityDescriptor(sd.get())); + + BOOL daclPresent = FALSE; + PACL pDacl = nullptr; + BOOL daclDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorDacl(sd.get(), &daclPresent, &pDacl, &daclDefaulted)); + + LPVOID pAce = nullptr; + REQUIRE(GetAce(pDacl, 0, &pAce)); + auto* ace = static_cast(pAce); + REQUIRE((ace->Header.AceFlags & CONTAINER_INHERIT_ACE) != 0); + REQUIRE((ace->Header.AceFlags & OBJECT_INHERIT_ACE) != 0); +} + +TEST_CASE("TokenHelpersTests::SelfRelativeSD_Constexpr", "[token_helpers]") +{ + // Verify the SD can be constructed at compile time using sd_owner/sd_group helpers + constexpr auto sd = wil::make_self_relative_sd( + wil::sd_owner(wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS)), + wil::sd_group(wil::no_sid), + wil::make_allow_ace(GENERIC_READ, wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID))); + + // Validate at runtime that the constexpr result is a valid SD + auto mutableSd = sd; + REQUIRE(IsValidSecurityDescriptor(mutableSd.get())); +} + +TEST_CASE("TokenHelpersTests::SelfRelativeSD_OwnerGroupHelpers", "[token_helpers]") +{ + // sd_owner + sd_group with real SIDs + auto sd = wil::make_self_relative_sd( + wil::sd_owner(wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS)), + wil::sd_group(wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_USERS)), + wil::make_allow_ace(GENERIC_READ, wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID))); + + REQUIRE(IsValidSecurityDescriptor(sd.get())); + + // Verify owner + PSID pOwner = nullptr; + BOOL ownerDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorOwner(sd.get(), &pOwner, &ownerDefaulted)); + auto expectedOwner = wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS); + REQUIRE(EqualSid(pOwner, expectedOwner.get())); + + // Verify group + PSID pGroup = nullptr; + BOOL groupDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorGroup(sd.get(), &pGroup, &groupDefaulted)); + REQUIRE(pGroup != nullptr); + auto expectedGroup = wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_USERS); + REQUIRE(EqualSid(pGroup, expectedGroup.get())); +} + +#ifdef __WIL_HAS_CLASS_NTTP +TEST_CASE("TokenHelpersTests::SelfRelativeSD_StringSid", "[token_helpers]") +{ + // Parse SID from string literal at compile time + constexpr auto localSystem = wil::make_static_sid<"S-1-5-18">(); + + // Verify sub-authority count and values + auto mutableSid = localSystem; + REQUIRE(IsValidSid(mutableSid.get())); + REQUIRE(*GetSidSubAuthorityCount(mutableSid.get()) == 1); + REQUIRE(*GetSidSubAuthority(mutableSid.get(), 0) == 18); + + // Use string SIDs in make_self_relative_sd via sd_owner + auto sd = wil::make_self_relative_sd( + wil::sd_owner(wil::make_static_sid<"S-1-5-32-544">()), // BUILTIN\Administrators + wil::sd_group(wil::no_sid), + wil::make_allow_ace(GENERIC_READ, wil::make_static_sid<"S-1-5-11">())); // Authenticated Users + + REQUIRE(IsValidSecurityDescriptor(sd.get())); + + // Verify owner matches the equivalent numeric SID + PSID pOwner = nullptr; + BOOL ownerDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorOwner(sd.get(), &pOwner, &ownerDefaulted)); + auto expectedOwner = wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS); + REQUIRE(EqualSid(pOwner, expectedOwner.get())); + + // Verify ACE SID matches the equivalent numeric SID + BOOL daclPresent = FALSE; + PACL pDacl = nullptr; + BOOL daclDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorDacl(sd.get(), &daclPresent, &pDacl, &daclDefaulted)); + LPVOID pAce = nullptr; + REQUIRE(GetAce(pDacl, 0, &pAce)); + auto expectedAceSid = wil::make_static_nt_sid(SECURITY_AUTHENTICATED_USER_RID); + REQUIRE(EqualSid(reinterpret_cast(&static_cast(pAce)->SidStart), expectedAceSid.get())); +} + +TEST_CASE("TokenHelpersTests::SelfRelativeSD_AllStringSyntax", "[token_helpers]") +{ + // Full SD using only string-based SIDs — the most compact form + constexpr auto sd = wil::make_self_relative_sd( + wil::sd_owner<"S-1-5-32-544">(), // BUILTIN\Administrators + wil::sd_group<"S-1-5-32-545">(), // BUILTIN\Users + wil::make_deny_ace<"S-1-5-7">(GENERIC_WRITE), // deny ANONYMOUS LOGON + wil::make_allow_ace<"S-1-5-11">(GENERIC_READ)); // allow Authenticated Users + + auto mutableSd = sd; + REQUIRE(IsValidSecurityDescriptor(mutableSd.get())); + + // Verify owner = S-1-5-32-544 + PSID pOwner = nullptr; + BOOL ownerDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorOwner(mutableSd.get(), &pOwner, &ownerDefaulted)); + auto expectedOwner = wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS); + REQUIRE(EqualSid(pOwner, expectedOwner.get())); + + // Verify group = S-1-5-32-545 + PSID pGroup = nullptr; + BOOL groupDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorGroup(mutableSd.get(), &pGroup, &groupDefaulted)); + auto expectedGroup = wil::make_static_nt_sid(SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_USERS); + REQUIRE(EqualSid(pGroup, expectedGroup.get())); + + // Verify DACL: deny then allow + BOOL daclPresent = FALSE; + PACL pDacl = nullptr; + BOOL daclDefaulted = FALSE; + REQUIRE(GetSecurityDescriptorDacl(mutableSd.get(), &daclPresent, &pDacl, &daclDefaulted)); + REQUIRE(pDacl->AceCount == 2); + + LPVOID pAce = nullptr; + REQUIRE(GetAce(pDacl, 0, &pAce)); + REQUIRE(static_cast(pAce)->Header.AceType == ACCESS_DENIED_ACE_TYPE); + REQUIRE(GetAce(pDacl, 1, &pAce)); + REQUIRE(static_cast(pAce)->Header.AceType == ACCESS_ALLOWED_ACE_TYPE); +} +#endif // __WIL_HAS_CLASS_NTTP + +#endif // _HAS_CXX20 + #if (_WIN32_WINNT >= _WIN32_WINNT_WIN8) TEST_CASE("TokenHelpersTests::TestMembership", "[token_helpers]") {