diff --git a/changelog.md b/changelog.md index 3e9ed6276d73..20177aefddc4 100644 --- a/changelog.md +++ b/changelog.md @@ -148,6 +148,9 @@ provided by the operating system. (instead of skipping them sometimes as it was before). - Added optional `followSymlinks` argument to `setFilePermissions`. +- Added `os.isAdmin` to tell whether the caller's process is a member of the + Administrators local group (on Windows) or a root (on POSIX). + - Added `random.initRand()` overload with no argument which uses the current time as a seed. - Added experimental `linenoise.readLineStatus` to get line and status (e.g. ctrl-D or ctrl-C). diff --git a/lib/pure/os.nim b/lib/pure/os.nim index b900546a010e..df9a1180c5cc 100644 --- a/lib/pure/os.nim +++ b/lib/pure/os.nim @@ -1688,6 +1688,36 @@ proc setFilePermissions*(filename: string, permissions: set[FilePermission], var res2 = setFileAttributesA(filename, res) if res2 == - 1'i32: raiseOSError(osLastError(), $(filename, permissions)) +proc isAdmin*: bool {.noWeirdTarget.} = + ## Returns whether the caller's process is a member of the Administrators local + ## group (on Windows) or a root (on POSIX), via `geteuid() == 0`. + when defined(windows): + # Rewrite of the example from Microsoft Docs: + # https://docs.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership#examples + # and corresponding PostgreSQL function: + # https://doxygen.postgresql.org/win32security_8c.html#ae6b61e106fa5d6c5d077a9d14ee80569 + var ntAuthority = SID_IDENTIFIER_AUTHORITY(value: SECURITY_NT_AUTHORITY) + var administratorsGroup: PSID + if not isSuccess(allocateAndInitializeSid(addr ntAuthority, + BYTE(2), + SECURITY_BUILTIN_DOMAIN_RID, + DOMAIN_ALIAS_RID_ADMINS, + 0, 0, 0, 0, 0, 0, + addr administratorsGroup)): + raiseOSError(osLastError(), "could not get SID for Administrators group") + + defer: + if freeSid(administratorsGroup) != nil: + raiseOSError(osLastError(), "failed to free SID for Administrators group") + + var b: WINBOOL + if not isSuccess(checkTokenMembership(0, administratorsGroup, addr b)): + raiseOSError(osLastError(), "could not check access token membership") + + return isSuccess(b) + else: + return geteuid() == 0 + proc createSymlink*(src, dest: string) {.noWeirdTarget.} = ## Create a symbolic link at `dest` which points to the item specified ## by `src`. On most operating systems, will fail if a link already exists. diff --git a/lib/windows/winlean.nim b/lib/windows/winlean.nim index 92055adbf0ac..1cdb20fe73f7 100644 --- a/lib/windows/winlean.nim +++ b/lib/windows/winlean.nim @@ -25,6 +25,7 @@ when useWinUnicode: else: type WinChar* = char +# See https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types type Handle* = int LONG* = int32 @@ -33,6 +34,7 @@ type WINBOOL* = int32 ## `WINBOOL` uses opposite convention as posix, !=0 meaning success. # xxx this should be distinct int32, distinct would make code less error prone + PBOOL* = ptr WINBOOL DWORD* = int32 PDWORD* = ptr DWORD LPINT* = ptr int32 @@ -40,6 +42,7 @@ type PULONG_PTR* = ptr uint HDC* = Handle HGLRC* = Handle + BYTE* = cuchar SECURITY_ATTRIBUTES* {.final, pure.} = object nLength*: int32 @@ -136,6 +139,10 @@ const HANDLE_FLAG_INHERIT* = 0x00000001'i32 +proc isSuccess*(a: WINBOOL): bool {.inline.} = + ## Returns true if `a != 0`. Windows uses a different convention than POSIX, + ## where `a == 0` is commonly used on success. + a != 0 proc getVersionExW*(lpVersionInfo: ptr OSVERSIONINFO): WINBOOL {. stdcall, dynlib: "kernel32", importc: "GetVersionExW", sideEffect.} proc getVersionExA*(lpVersionInfo: ptr OSVERSIONINFO): WINBOOL {. @@ -1129,5 +1136,42 @@ proc setFileTime*(hFile: Handle, lpCreationTime: LPFILETIME, lpLastAccessTime: LPFILETIME, lpLastWriteTime: LPFILETIME): WINBOOL {.stdcall, dynlib: "kernel32", importc: "SetFileTime".} +type + # https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid_identifier_authority + SID_IDENTIFIER_AUTHORITY* {.importc, header: "".} = object + value* {.importc: "Value"}: array[6, BYTE] + # https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid + SID* {.importc, header: "".} = object + Revision: BYTE + SubAuthorityCount: BYTE + IdentifierAuthority: SID_IDENTIFIER_AUTHORITY + SubAuthority: ptr ptr DWORD + PSID* = ptr SID + +const + # https://docs.microsoft.com/en-us/windows/win32/secauthz/sid-components + # https://github.com/mirror/mingw-w64/blob/84c950bdab7c999ace49fe8383856be77f88c4a8/mingw-w64-headers/include/winnt.h#L2994 + SECURITY_NT_AUTHORITY* = [BYTE(0), BYTE(0), BYTE(0), BYTE(0), BYTE(0), BYTE(5)] + SECURITY_BUILTIN_DOMAIN_RID* = 32 + DOMAIN_ALIAS_RID_ADMINS* = 544 + +proc allocateAndInitializeSid*(pIdentifierAuthority: ptr SID_IDENTIFIER_AUTHORITY, + nSubAuthorityCount: BYTE, + nSubAuthority0: DWORD, + nSubAuthority1: DWORD, + nSubAuthority2: DWORD, + nSubAuthority3: DWORD, + nSubAuthority4: DWORD, + nSubAuthority5: DWORD, + nSubAuthority6: DWORD, + nSubAuthority7: DWORD, + pSid: ptr PSID): WINBOOL + {.stdcall, dynlib: "Advapi32", importc: "AllocateAndInitializeSid".} +proc checkTokenMembership*(tokenHandle: Handle, sidToCheck: PSID, + isMember: PBOOL): WINBOOL + {.stdcall, dynlib: "Advapi32", importc: "CheckTokenMembership".} +proc freeSid*(pSid: PSID): PSID + {.stdcall, dynlib: "Advapi32", importc: "FreeSid".} + when defined(nimHasStyleChecks): {.pop.} # {.push styleChecks: off.} diff --git a/tests/stdlib/tos.nim b/tests/stdlib/tos.nim index db64f6254d30..6651a25db766 100644 --- a/tests/stdlib/tos.nim +++ b/tests/stdlib/tos.nim @@ -655,3 +655,10 @@ block: # normalizeExe doAssert "foo/../bar".dup(normalizeExe) == "foo/../bar" when defined(windows): doAssert "foo".dup(normalizeExe) == "foo" + +block: # isAdmin + let isAzure = existsEnv("TF_BUILD") # xxx factor with testament.specs.isAzure + # In Azure on Windows tests run as an admin user + if isAzure and defined(windows): doAssert isAdmin() + # In Azure on POSIX tests run as a normal user + if isAzure and defined(posix): doAssert not isAdmin()