Browse Source

Merge pull request #902 from Kelimion/run_as_user

Add support to core:windows to add/delete users.
gingerBill 4 years ago
parent
commit
3e54cddf64

+ 54 - 0
core/sys/windows/advapi32.odin

@@ -10,3 +10,57 @@ foreign advapi32 {
 	                         DesiredAccess: DWORD,
 	                         TokenHandle: ^HANDLE) -> BOOL ---
 }
+
+// Necessary to create a token to impersonate a user with for CreateProcessAsUser
+@(default_calling_convention="stdcall")
+foreign advapi32 {
+	LogonUserW :: proc(
+		lpszUsername: LPCWSTR,
+		lpszDomain: LPCWSTR,
+		lpszPassword: LPCWSTR,
+		dwLogonType: Logon32_Type,
+		dwLogonProvider: Logon32_Provider,
+		phToken: ^HANDLE,
+	) -> BOOL ---
+
+	// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lookupaccountnamew
+	// To look up the SID to use with DeleteProfileW.
+	LookupAccountNameW :: proc(
+		lpSystemName: wstring,
+		lpAccountName: wstring,
+		Sid: ^SID,
+		cbSid: ^DWORD,
+		ReferencedDomainName: wstring,
+		cchReferencedDomainName: ^DWORD,
+		peUse: ^SID_TYPE,
+	) -> BOOL ---
+
+	CreateProcessWithLogonW :: proc(
+		lpUsername: wstring,
+		lpDomain: wstring,
+		lpPassword: wstring,
+		dwLogonFlags: DWORD,
+		lpApplicationName: wstring,
+		lpCommandLine: wstring,
+		dwCreationFlags: DWORD,
+		lpEnvironment: LPVOID,
+		lpCurrentDirectory: wstring,
+		lpStartupInfo: LPSTARTUPINFO,
+		lpProcessInformation: LPPROCESS_INFORMATION,
+	) -> BOOL ---
+
+	// https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessasuserw
+	CreateProcessAsUserW :: proc(
+		hToken: HANDLE,
+		lpApplicationName: wstring,
+		lpCommandLine: wstring,
+		lpProcessAttributes: LPSECURITY_ATTRIBUTES,
+		lpThreadAttributes: LPSECURITY_ATTRIBUTES,
+		bInheritHandles: BOOL,
+		dwCreationFlags: DWORD,
+		lpEnvironment: LPVOID,
+		lpCurrentDirectory: wstring,
+		lpStartupInfo: LPSTARTUPINFO,
+		lpProcessInformation: LPPROCESS_INFORMATION,
+	) -> BOOL ---;
+}

+ 37 - 0
core/sys/windows/netapi32.odin

@@ -0,0 +1,37 @@
+package sys_windows
+
+foreign import netapi32 "system:Netapi32.lib"
+
+@(default_calling_convention="stdcall")
+foreign netapi32 {
+	NetUserAdd :: proc(
+		servername: wstring,
+		level: DWORD,
+		user_info: ^USER_INFO_1, // Perhaps make this a #raw_union with USER_INFO1..4 when we need the other levels.
+		parm_err: ^DWORD,
+	) -> NET_API_STATUS ---;
+	NetUserDel :: proc(
+		servername: wstring,
+		username: wstring,
+	) -> NET_API_STATUS ---;
+	NetUserGetInfo :: proc(
+		servername: wstring,
+		username: wstring,
+		level: DWORD,
+		user_info: ^USER_INFO_1,
+	) -> NET_API_STATUS ---;
+	NetLocalGroupAddMembers :: proc(
+		servername: wstring,
+		groupname: wstring,
+		level: DWORD,
+		group_members_info: ^LOCALGROUP_MEMBERS_INFO_0, // Actually a variably sized array of these.
+		totalentries: DWORD,
+	) -> NET_API_STATUS ---;
+	NetLocalGroupDelMembers :: proc(
+		servername: wstring,
+		groupname: wstring,
+		level: DWORD,
+		group_members_info: ^LOCALGROUP_MEMBERS_INFO_0, // Actually a variably sized array of these.
+		totalentries: DWORD,
+	) -> NET_API_STATUS ---;
+}

+ 448 - 2
core/sys/windows/types.odin

@@ -570,7 +570,8 @@ PROCESS_INFORMATION :: struct {
 	dwThreadId: DWORD,
 }
 
-STARTUPINFO :: struct {
+// FYI: This is STARTUPINFOW, not STARTUPINFOA
+STARTUPINFO :: struct #packed {
 	cb: DWORD,
 	lpReserved: LPWSTR,
 	lpDesktop: LPWSTR,
@@ -580,7 +581,7 @@ STARTUPINFO :: struct {
 	dwXSize: DWORD,
 	dwYSize: DWORD,
 	dwXCountChars: DWORD,
-	dwYCountCharts: DWORD,
+	dwYCountChars: DWORD,
 	dwFillAttribute: DWORD,
 	dwFlags: DWORD,
 	wShowWindow: WORD,
@@ -788,3 +789,448 @@ OSVERSIONINFOEXW :: struct {
     wProductType:        UCHAR,
     wReserved:           UCHAR,
 };
+
+// https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-quota_limits
+// Used in LogonUserExW
+PQUOTA_LIMITS :: struct {
+	PagedPoolLimit: SIZE_T,
+	NonPagedPoolLimit: SIZE_T,
+	MinimumWorkingSetSize: SIZE_T,
+	MaximumWorkingSetSize: SIZE_T,
+	PagefileLimit: SIZE_T,
+	TimeLimit: LARGE_INTEGER,
+};
+
+Logon32_Type :: enum DWORD {
+	INTERACTIVE       = 2,
+	NETWORK           = 3,
+	BATCH             = 4,
+	SERVICE           = 5,
+	UNLOCK            = 7,
+	NETWORK_CLEARTEXT = 8,
+	NEW_CREDENTIALS   = 9,
+}
+
+Logon32_Provider :: enum DWORD {
+	DEFAULT = 0,
+	WINNT35 = 1,
+	WINNT40 = 2,
+	WINNT50 = 3,
+	VIRTUAL = 4,
+}
+
+// https://docs.microsoft.com/en-us/windows/win32/api/profinfo/ns-profinfo-profileinfow
+// Used in LoadUserProfileW
+
+PROFILEINFOW :: struct {
+	dwSize: DWORD,
+	dwFlags: DWORD,
+	lpUserName: LPWSTR,
+	lpProfilePath: LPWSTR,
+	lpDefaultPath: LPWSTR,
+	lpServerName: LPWSTR,
+	lpPolicyPath: LPWSTR,
+  	hProfile: HANDLE,
+};
+
+// Used in LookupAccountNameW
+SID_NAME_USE :: distinct DWORD;
+
+SID_TYPE :: enum SID_NAME_USE {
+  User = 1,
+  Group,
+  Domain,
+  Alias,
+  WellKnownGroup,
+  DeletedAccount,
+  Invalid,
+  Unknown,
+  Computer,
+  Label,
+  LogonSession
+}
+
+SECURITY_MAX_SID_SIZE :: 68;
+
+// https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid
+SID :: struct #packed {
+	Revision: byte,
+	SubAuthorityCount: byte,
+	IdentifierAuthority: SID_IDENTIFIER_AUTHORITY,
+	SubAuthority: [15]DWORD, // Array of DWORDs
+};
+#assert(size_of(SID) == SECURITY_MAX_SID_SIZE);
+
+SID_IDENTIFIER_AUTHORITY :: struct #packed {
+    Value: [6]u8,
+};
+
+// For NetAPI32
+// https://github.com/tpn/winsdk-10/blob/master/Include/10.0.14393.0/shared/lmerr.h
+// https://github.com/tpn/winsdk-10/blob/master/Include/10.0.14393.0/shared/LMaccess.h
+
+UNLEN      :: 256;        // Maximum user name length
+LM20_UNLEN ::  20;        // LM 2.0 Maximum user name length
+
+GNLEN      :: UNLEN;      // Group name
+LM20_GNLEN :: LM20_UNLEN; // LM 2.0 Group name
+
+PWLEN      :: 256;        // Maximum password length
+LM20_PWLEN ::  14;        // LM 2.0 Maximum password length
+
+USER_PRIV :: enum DWORD {
+	Guest = 0,
+	User  = 1,
+	Admin = 2,
+	Mask  = 0x3,
+}
+
+USER_INFO_FLAG :: enum DWORD {
+	Script                          = 0,  // 1 <<  0: 0x0001,
+	AccountDisable                  = 1,  // 1 <<  1: 0x0002,
+	HomeDir_Required                = 3,  // 1 <<  3: 0x0008,
+	Lockout                         = 4,  // 1 <<  4: 0x0010,
+	Passwd_NotReqd                  = 5,  // 1 <<  5: 0x0020,
+	Passwd_Cant_Change              = 6,  // 1 <<  6: 0x0040,
+	Encrypted_Text_Password_Allowed = 7,  // 1 <<  7: 0x0080,
+
+    Temp_Duplicate_Account          = 8,  // 1 <<  8: 0x0100,
+    Normal_Account                  = 9,  // 1 <<  9: 0x0200,
+    InterDomain_Trust_Account       = 11, // 1 << 11: 0x0800,
+    Workstation_Trust_Account       = 12, // 1 << 12: 0x1000,
+    Server_Trust_Account            = 13, // 1 << 13: 0x2000,
+}
+USER_INFO_FLAGS :: distinct bit_set[USER_INFO_FLAG];
+
+USER_INFO_1 :: struct #packed {
+	name: LPWSTR,
+	password: LPWSTR,     // Max password length is defined in LM20_PWLEN.
+	password_age: DWORD,
+	priv: USER_PRIV,
+	home_dir: LPWSTR,
+	comment: LPWSTR,
+	flags: USER_INFO_FLAGS,
+	script_path: LPWSTR,
+};
+#assert(size_of(USER_INFO_1) == 50);
+
+LOCALGROUP_MEMBERS_INFO_0 :: struct #packed {
+	sid: ^SID,
+};
+
+NET_API_STATUS :: enum DWORD {
+	Success = 0,
+	ERROR_ACCESS_DENIED = 5,
+	MemberInAlias = 1378,
+	NetNotStarted = 2102,
+	UnknownServer = 2103,
+	ShareMem = 2104,
+	NoNetworkResource = 2105,
+	RemoteOnly = 2106,
+	DevNotRedirected = 2107,
+	ServerNotStarted = 2114,
+	ItemNotFound = 2115,
+	UnknownDevDir = 2116,
+	RedirectedPath = 2117,
+	DuplicateShare = 2118,
+	NoRoom = 2119,
+	TooManyItems = 2121,
+	InvalidMaxUsers = 2122,
+	BufTooSmall = 2123,
+	RemoteErr = 2127,
+	LanmanIniError = 2131,
+	NetworkError = 2136,
+	WkstaInconsistentState = 2137,
+	WkstaNotStarted = 2138,
+	BrowserNotStarted = 2139,
+	InternalError = 2140,
+	BadTransactConfig = 2141,
+	InvalidAPI = 2142,
+	BadEventName = 2143,
+	DupNameReboot = 2144,
+	CfgCompNotFound = 2146,
+	CfgParamNotFound = 2147,
+	LineTooLong = 2149,
+	QNotFound = 2150,
+	JobNotFound = 2151,
+	DestNotFound = 2152,
+	DestExists = 2153,
+	QExists = 2154,
+	QNoRoom = 2155,
+	JobNoRoom = 2156,
+	DestNoRoom = 2157,
+	DestIdle = 2158,
+	DestInvalidOp = 2159,
+	ProcNoRespond = 2160,
+	SpoolerNotLoaded = 2161,
+	DestInvalidState = 2162,
+	QInvalidState = 2163,
+	JobInvalidState = 2164,
+	SpoolNoMemory = 2165,
+	DriverNotFound = 2166,
+	DataTypeInvalid = 2167,
+	ProcNotFound = 2168,
+	ServiceTableLocked = 2180,
+	ServiceTableFull = 2181,
+	ServiceInstalled = 2182,
+	ServiceEntryLocked = 2183,
+	ServiceNotInstalled = 2184,
+	BadServiceName = 2185,
+	ServiceCtlTimeout = 2186,
+	ServiceCtlBusy = 2187,
+	BadServiceProgName = 2188,
+	ServiceNotCtrl = 2189,
+	ServiceKillProc = 2190,
+	ServiceCtlNotValid = 2191,
+	NotInDispatchTbl = 2192,
+	BadControlRecv = 2193,
+	ServiceNotStarting = 2194,
+	AlreadyLoggedOn = 2200,
+	NotLoggedOn = 2201,
+	BadUsername = 2202,
+	BadPassword = 2203,
+	UnableToAddName_W = 2204,
+	UnableToAddName_F = 2205,
+	UnableToDelName_W = 2206,
+	UnableToDelName_F = 2207,
+	LogonsPaused = 2209,
+	LogonServerConflict = 2210,
+	LogonNoUserPath = 2211,
+	LogonScriptError = 2212,
+	StandaloneLogon = 2214,
+	LogonServerNotFound = 2215,
+	LogonDomainExists = 2216,
+	NonValidatedLogon = 2217,
+	ACFNotFound = 2219,
+	GroupNotFound = 2220,
+	UserNotFound = 2221,
+	ResourceNotFound = 2222,
+	GroupExists = 2223,
+	UserExists = 2224,
+	ResourceExists = 2225,
+	NotPrimary = 2226,
+	ACFNotLoaded = 2227,
+	ACFNoRoom = 2228,
+	ACFFileIOFail = 2229,
+	ACFTooManyLists = 2230,
+	UserLogon = 2231,
+	ACFNoParent = 2232,
+	CanNotGrowSegment = 2233,
+	SpeGroupOp = 2234,
+	NotInCache = 2235,
+	UserInGroup = 2236,
+	UserNotInGroup = 2237,
+	AccountUndefined = 2238,
+	AccountExpired = 2239,
+	InvalidWorkstation = 2240,
+	InvalidLogonHours = 2241,
+	PasswordExpired = 2242,
+	PasswordCantChange = 2243,
+	PasswordHistConflict = 2244,
+	PasswordTooShort = 2245,
+	PasswordTooRecent = 2246,
+	InvalidDatabase = 2247,
+	DatabaseUpToDate = 2248,
+	SyncRequired = 2249,
+	UseNotFound = 2250,
+	BadAsgType = 2251,
+	DeviceIsShared = 2252,
+	SameAsComputerName = 2253,
+	NoComputerName = 2270,
+	MsgAlreadyStarted = 2271,
+	MsgInitFailed = 2272,
+	NameNotFound = 2273,
+	AlreadyForwarded = 2274,
+	AddForwarded = 2275,
+	AlreadyExists = 2276,
+	TooManyNames = 2277,
+	DelComputerName = 2278,
+	LocalForward = 2279,
+	GrpMsgProcessor = 2280,
+	PausedRemote = 2281,
+	BadReceive = 2282,
+	NameInUse = 2283,
+	MsgNotStarted = 2284,
+	NotLocalName = 2285,
+	NoForwardName = 2286,
+	RemoteFull = 2287,
+	NameNotForwarded = 2288,
+	TruncatedBroadcast = 2289,
+	InvalidDevice = 2294,
+	WriteFault = 2295,
+	DuplicateName = 2297,
+	DeleteLater = 2298,
+	IncompleteDel = 2299,
+	MultipleNets = 2300,
+	NetNameNotFound = 2310,
+	DeviceNotShared = 2311,
+	ClientNameNotFound = 2312,
+	FileIdNotFound = 2314,
+	ExecFailure = 2315,
+	TmpFile = 2316,
+	TooMuchData = 2317,
+	DeviceShareConflict = 2318,
+	BrowserTableIncomplete = 2319,
+	NotLocalDomain = 2320,
+	IsDfsShare = 2321,
+	DevInvalidOpCode = 2331,
+	DevNotFound = 2332,
+	DevNotOpen = 2333,
+	BadQueueDevString = 2334,
+	BadQueuePriority = 2335,
+	NoCommDevs = 2337,
+	QueueNotFound = 2338,
+	BadDevString = 2340,
+	BadDev = 2341,
+	InUseBySpooler = 2342,
+	CommDevInUse = 2343,
+	InvalidComputer = 2351,
+	MaxLenExceeded = 2354,
+	BadComponent = 2356,
+	CantType = 2357,
+	TooManyEntries = 2362,
+	ProfileFileTooBig = 2370,
+	ProfileOffset = 2371,
+	ProfileCleanup = 2372,
+	ProfileUnknownCmd = 2373,
+	ProfileLoadErr = 2374,
+	ProfileSaveErr = 2375,
+	LogOverflow = 2377,
+	LogFileChanged = 2378,
+	LogFileCorrupt = 2379,
+	SourceIsDir = 2380,
+	BadSource = 2381,
+	BadDest = 2382,
+	DifferentServers = 2383,
+	RunSrvPaused = 2385,
+	ErrCommRunSrv = 2389,
+	ErrorExecingGhost = 2391,
+	ShareNotFound = 2392,
+	InvalidLana = 2400,
+	OpenFiles = 2401,
+	ActiveConns = 2402,
+	BadPasswordCore = 2403,
+	DevInUse = 2404,
+	LocalDrive = 2405,
+	AlertExists = 2430,
+	TooManyAlerts = 2431,
+	NoSuchAlert = 2432,
+	BadRecipient = 2433,
+	AcctLimitExceeded = 2434,
+	InvalidLogSeek = 2440,
+	BadUasConfig = 2450,
+	InvalidUASOp = 2451,
+	LastAdmin = 2452,
+	DCNotFound = 2453,
+	LogonTrackingError = 2454,
+	NetlogonNotStarted = 2455,
+	CanNotGrowUASFile = 2456,
+	TimeDiffAtDC = 2457,
+	PasswordMismatch = 2458,
+	NoSuchServer = 2460,
+	NoSuchSession = 2461,
+	NoSuchConnection = 2462,
+	TooManyServers = 2463,
+	TooManySessions = 2464,
+	TooManyConnections = 2465,
+	TooManyFiles = 2466,
+	NoAlternateServers = 2467,
+	TryDownLevel = 2470,
+	UPSDriverNotStarted = 2480,
+	UPSInvalidConfig = 2481,
+	UPSInvalidCommPort = 2482,
+	UPSSignalAsserted = 2483,
+	UPSShutdownFailed = 2484,
+	BadDosRetCode = 2500,
+	ProgNeedsExtraMem = 2501,
+	BadDosFunction = 2502,
+	RemoteBootFailed = 2503,
+	BadFileCheckSum = 2504,
+	NoRplBootSystem = 2505,
+	RplLoadrNetBiosErr = 2506,
+	RplLoadrDiskErr = 2507,
+	ImageParamErr = 2508,
+	TooManyImageParams = 2509,
+	NonDosFloppyUsed = 2510,
+	RplBootRestart = 2511,
+	RplSrvrCallFailed = 2512,
+	CantConnectRplSrvr = 2513,
+	CantOpenImageFile = 2514,
+	CallingRplSrvr = 2515,
+	StartingRplBoot = 2516,
+	RplBootServiceTerm = 2517,
+	RplBootStartFailed = 2518,
+	RPL_CONNECTED = 2519,
+	BrowserConfiguredToNotRun = 2550,
+	RplNoAdaptersStarted = 2610,
+	RplBadRegistry = 2611,
+	RplBadDatabase = 2612,
+	RplRplfilesShare = 2613,
+	RplNotRplServer = 2614,
+	RplCannotEnum = 2615,
+	RplWkstaInfoCorrupted = 2616,
+	RplWkstaNotFound = 2617,
+	RplWkstaNameUnavailable = 2618,
+	RplProfileInfoCorrupted = 2619,
+	RplProfileNotFound = 2620,
+	RplProfileNameUnavailable = 2621,
+	RplProfileNotEmpty = 2622,
+	RplConfigInfoCorrupted = 2623,
+	RplConfigNotFound = 2624,
+	RplAdapterInfoCorrupted = 2625,
+	RplInternal = 2626,
+	RplVendorInfoCorrupted = 2627,
+	RplBootInfoCorrupted = 2628,
+	RplWkstaNeedsUserAcct = 2629,
+	RplNeedsRPLUSERAcct = 2630,
+	RplBootNotFound = 2631,
+	RplIncompatibleProfile = 2632,
+	RplAdapterNameUnavailable = 2633,
+	RplConfigNotEmpty = 2634,
+	RplBootInUse = 2635,
+	RplBackupDatabase = 2636,
+	RplAdapterNotFound = 2637,
+	RplVendorNotFound = 2638,
+	RplVendorNameUnavailable = 2639,
+	RplBootNameUnavailable = 2640,
+	RplConfigNameUnavailable = 2641,
+	DfsInternalCorruption = 2660,
+	DfsVolumeDataCorrupt = 2661,
+	DfsNoSuchVolume = 2662,
+	DfsVolumeAlreadyExists = 2663,
+	DfsAlreadyShared = 2664,
+	DfsNoSuchShare = 2665,
+	DfsNotALeafVolume = 2666,
+	DfsLeafVolume = 2667,
+	DfsVolumeHasMultipleServers = 2668,
+	DfsCantCreateJunctionPoint = 2669,
+	DfsServerNotDfsAware = 2670,
+	DfsBadRenamePath = 2671,
+	DfsVolumeIsOffline = 2672,
+	DfsNoSuchServer = 2673,
+	DfsCyclicalName = 2674,
+	DfsNotSupportedInServerDfs = 2675,
+	DfsDuplicateService = 2676,
+	DfsCantRemoveLastServerShare = 2677,
+	DfsVolumeIsInterDfs = 2678,
+	DfsInconsistent = 2679,
+	DfsServerUpgraded = 2680,
+	DfsDataIsIdentical = 2681,
+	DfsCantRemoveDfsRoot = 2682,
+	DfsChildOrParentInDfs = 2683,
+	DfsInternalError = 2690,
+	SetupAlreadyJoined = 2691,
+	SetupNotJoined = 2692,
+	SetupDomainController = 2693,
+	DefaultJoinRequired = 2694,
+	InvalidWorkgroupName = 2695,
+	NameUsesIncompatibleCodePage = 2696,
+	ComputerAccountNotFound = 2697,
+	PersonalSku = 2698,
+	SetupCheckDNSConfig = 2699,
+	PasswordMustChange = 2701,
+	AccountLockedOut = 2702,
+	PasswordTooLong = 2703,
+	PasswordNotComplexEnough = 2704,
+	PasswordFilterError = 2705,
+}

+ 28 - 0
core/sys/windows/userenv.odin

@@ -7,4 +7,32 @@ foreign userenv {
 	GetUserProfileDirectoryW :: proc(hToken: HANDLE,
 	                                 lpProfileDir: LPWSTR,
 	                                 lpcchSize: ^DWORD) -> BOOL ---
+	LoadUserProfileW :: proc(
+		hToken: HANDLE,
+		lpProfileInfo: ^PROFILEINFOW,
+	) -> BOOL ---
+
+	// https://docs.microsoft.com/en-us/windows/win32/api/userenv/nf-userenv-createprofile
+	// The caller must have administrator privileges to call this function.
+	CreateProfile :: proc(
+		pszUserSid: LPCWSTR,
+		pszUserName: LPCWSTR,
+		pszProfilePath: wstring,
+		cchProfilePath: DWORD,
+	) -> u32 ---
+
+	// https://docs.microsoft.com/en-us/windows/win32/api/userenv/nf-userenv-deleteprofilew
+	// The caller must have administrative privileges to delete a user's profile.
+	DeleteProfileW :: proc(
+		lpSidString: LPCWSTR,
+		lpProfilePath: LPCWSTR,
+		lpComputerName: LPCWSTR,
+	) -> BOOL ---
+
+	// https://docs.microsoft.com/en-us/windows/win32/api/sddl/nf-sddl-convertsidtostringsida
+	// To turn a SID into a string SID to use with CreateProfile & DeleteProfileW.
+	ConvertSidToStringSidW :: proc(
+		Sid: ^SID,
+	  	StringSid: ^LPCWSTR,
+	) -> BOOL ---
 }

+ 374 - 0
core/sys/windows/util.odin

@@ -1,5 +1,8 @@
 package sys_windows
 
+import "core:strings"
+import "core:sys/win32"
+
 LOWORD :: #force_inline proc "contextless" (x: DWORD) -> WORD {
 	return WORD(x & 0xffff);
 }
@@ -81,3 +84,374 @@ utf16_to_utf8 :: proc(s: []u16, allocator := context.temp_allocator) -> string {
 	return wstring_to_utf8(raw_data(s), len(s), allocator);
 }
 
+// AdvAPI32, NetAPI32 and UserENV helpers.
+
+allowed_username :: proc(username: string) -> bool {
+/*
+	User account names are limited to 20 characters and group names are limited to 256 characters.
+	In addition, account names cannot be terminated by a period and they cannot include commas or any of the following printable characters:
+	", /, , [, ], :, |, <, >, +, =, ;, ?, *. Names also cannot include characters in the range 1-31, which are nonprintable.
+*/
+
+	_DISALLOWED :: "\"/ []:|<>+=;?*,";
+
+	if len(username) > LM20_UNLEN || len(username) == 0 {
+		return false;
+	}
+	if username[len(username)-1] == '.' {
+		return false;
+	}
+
+	for r in username {
+		if r > 0 && r < 32 {
+			return false;
+		}
+	}
+	if strings.contains_any(username, _DISALLOWED) {
+		return false;
+	}
+
+	return true;
+}
+
+// Returns .Success on success.
+_add_user :: proc(servername: string, username: string, password: string) -> (ok: NET_API_STATUS) {
+
+	servername_w: wstring;
+	username_w:   []u16;
+	password_w:   []u16;
+
+	if len(servername) == 0 {
+		// Create account on this computer
+		servername_w = nil;
+	} else {
+		server := utf8_to_utf16(servername, context.temp_allocator);
+		servername_w = &server[0];
+	}
+
+	if len(username) == 0 || len(username) > LM20_UNLEN {
+		return .BadUsername;
+	}
+	if !allowed_username(username) {
+		return .BadUsername;
+	}
+	if len(password) == 0 || len(password) > LM20_PWLEN {
+		return .BadPassword;
+	}
+
+	username_w = utf8_to_utf16(username, context.temp_allocator);
+	password_w = utf8_to_utf16(password, context.temp_allocator);
+
+
+	level  := DWORD(1);
+	parm_err: DWORD;
+
+	user_info := USER_INFO_1{
+		name         = &username_w[0],
+		password     = &password_w[0], // Max password length is defined in LM20_PWLEN.
+		password_age = 0,              // Ignored
+		priv         = .User,
+		home_dir     = nil,            // We'll set it later
+		comment      = nil,
+		flags        = {.Script, .Normal_Account},
+		script_path  = nil,
+	};
+
+	ok = NetUserAdd(
+		servername_w,
+		level,
+		&user_info,
+		&parm_err,
+	);
+
+	return;
+}
+
+get_computer_name_and_account_sid :: proc(username: string) -> (computer_name: string, sid := SID{}, ok: bool) {
+
+	username_w := utf8_to_utf16(username, context.temp_allocator);
+	cbsid: DWORD;
+	computer_name_size: DWORD;
+	pe_use := SID_TYPE.User;
+
+	res := LookupAccountNameW(
+		nil, // Look on this computer first
+		&username_w[0],
+		&sid,
+		&cbsid,
+		nil,
+		&computer_name_size,
+		&pe_use,
+	);
+	if computer_name_size == 0 {
+		// User didn't exist, or we'd have a size here.
+		return "", {}, false;
+	}
+
+	cname_w := make([]u16, min(computer_name_size, 1), context.temp_allocator);
+
+	res = LookupAccountNameW(
+		nil,
+		&username_w[0],
+		&sid,
+		&cbsid,
+		&cname_w[0],
+		&computer_name_size,
+		&pe_use,
+	);
+
+	if !res {
+		return "", {}, false;
+	}
+	computer_name = utf16_to_utf8(cname_w, context.temp_allocator);
+
+	ok = true;
+	return;
+}
+
+get_sid :: proc(username: string, sid: ^SID) -> (ok: bool) {
+
+	username_w := utf8_to_utf16(username, context.temp_allocator);
+	cbsid: DWORD;
+	computer_name_size: DWORD;
+	pe_use := SID_TYPE.User;
+
+	res := LookupAccountNameW(
+		nil, // Look on this computer first
+		&username_w[0],
+		sid,
+		&cbsid,
+		nil,
+		&computer_name_size,
+		&pe_use,
+	);
+	if computer_name_size == 0 {
+		// User didn't exist, or we'd have a size here.
+		return false;
+	}
+
+	cname_w := make([]u16, min(computer_name_size, 1), context.temp_allocator);
+
+	res = LookupAccountNameW(
+		nil,
+		&username_w[0],
+		sid,
+		&cbsid,
+		&cname_w[0],
+		&computer_name_size,
+		&pe_use,
+	);
+
+	if !res {
+		return false;
+	}
+	ok = true;
+	return;
+}
+
+add_user_to_group :: proc(sid: ^SID, group: string) -> (ok: NET_API_STATUS) {
+	group_member := LOCALGROUP_MEMBERS_INFO_0{
+		sid = sid,
+	};
+	group_name := utf8_to_utf16(group, context.temp_allocator);
+	ok = NetLocalGroupAddMembers(
+		nil,
+		&group_name[0],
+		0,
+		&group_member,
+		1,
+	);
+	return;
+}
+
+add_del_from_group :: proc(sid: ^SID, group: string) -> (ok: NET_API_STATUS) {
+	group_member := LOCALGROUP_MEMBERS_INFO_0{
+		sid = sid,
+	};
+	group_name := utf8_to_utf16(group, context.temp_allocator);
+	ok = NetLocalGroupDelMembers(
+		nil,
+		&group_name[0],
+		0,
+		&group_member,
+		1,
+	);
+	return;
+}
+
+add_user_profile :: proc(username: string) -> (ok: bool, profile_path: string) {
+	username_w := utf8_to_utf16(username, context.temp_allocator);
+
+	sid := SID{};
+	ok = get_sid(username, &sid);
+	if ok == false {
+		return false, "";
+	}
+
+	sb: wstring;
+	res := ConvertSidToStringSidW(&sid, &sb);
+	if res == false {
+		return false, "";
+	}
+	defer win32.local_free(sb);
+
+	pszProfilePath := make([]u16, 257, context.temp_allocator);
+	res2 := CreateProfile(
+		sb,
+		&username_w[0],
+		&pszProfilePath[0],
+		257,
+	);
+	if res2 != 0 {
+		return false, "";
+	}
+	profile_path = wstring_to_utf8(&pszProfilePath[0], 257);
+
+	return true, profile_path;
+}
+
+
+delete_user_profile :: proc(username: string) -> (ok: bool) {
+	sid := SID{};
+	ok = get_sid(username, &sid);
+	if ok == false {
+		return false;
+	}
+
+	sb: wstring;
+	res := ConvertSidToStringSidW(&sid, &sb);
+	if res == false {
+		return false;
+	}
+	defer win32.local_free(sb);
+
+	res2 := DeleteProfileW(
+		sb,
+		nil,
+		nil,
+	);
+	return bool(res2);
+}
+
+add_user :: proc(servername: string, username: string, password: string) -> (ok: bool) {
+	/*
+		Convenience function that creates a new user, adds it to the group Users and creates a profile directory for it.
+		Requires elevated privileges (run as administrator).
+
+		TODO: Add a bool that governs whether to delete the user if adding to group and/or creating profile fail?
+		TODO: SecureZeroMemory the password after use.
+	*/
+
+	res := _add_user(servername, username, password);
+	if res != .Success {
+		return false;
+	}
+
+	// Grab the SID to add the user to the Users group.
+	sid: SID;
+	ok2 := get_sid(username, &sid);
+	if ok2 == false {
+		return false;
+	}
+
+	ok3 := add_user_to_group(&sid, "Users");
+	if ok3 != .Success {
+		return false;
+	}
+
+	return true;
+}
+
+delete_user :: proc(servername: string, username: string) -> (ok: bool) {
+	/*
+		Convenience function that deletes a user.
+		Requires elevated privileges (run as administrator).
+
+		TODO: Add a bool that governs whether to delete the profile from this wrapper?
+	*/
+
+	servername_w: wstring;
+	if len(servername) == 0 {
+		// Delete account on this computer
+		servername_w = nil;
+	} else {
+		server := utf8_to_utf16(servername, context.temp_allocator);
+		servername_w = &server[0];
+	}
+	username_w := utf8_to_utf16(username);
+
+	res := NetUserDel(
+		servername_w,
+		&username_w[0],
+	);
+	if res != .Success {
+		return false;
+	}
+	return true;
+}
+
+run_as_user :: proc(username, password, application, commandline: string, pi: ^PROCESS_INFORMATION, wait := true) -> (ok: bool) {
+	/*
+		Needs to be run as an account which has the "Replace a process level token" privilege.
+		This can be added to an account from: Control Panel -> Administrative Tools -> Local Security Policy.
+		The path to this policy is as follows: Local Policies -> User Rights Assignment -> Replace a process level token.
+		A reboot may be required for this change to take effect and impersonating a user to work.
+
+		TODO: SecureZeroMemory the password after use.
+
+	*/
+
+	username_w    := utf8_to_utf16(username);
+	domain_w      := utf8_to_utf16(".");
+	password_w    := utf8_to_utf16(password);
+	app_w         := utf8_to_utf16(application);
+
+	commandline_w: []u16 = {0};
+	if len(commandline) > 0 {
+		commandline_w = utf8_to_utf16(commandline);
+	}
+
+	user_token: HANDLE;
+
+	ok = bool(LogonUserW(
+		lpszUsername    = &username_w[0],
+		lpszDomain      = &domain_w[0],
+		lpszPassword    = &password_w[0],
+		dwLogonType     = .NEW_CREDENTIALS,
+		dwLogonProvider = .WINNT50,
+		phToken         = &user_token,
+	));
+
+	if !ok {
+		return false;
+		// err := GetLastError();
+		// fmt.printf("GetLastError: %v\n", err);
+	}
+	si := STARTUPINFO{};
+	si.cb = size_of(STARTUPINFO);
+	pi := pi;
+
+	ok = bool(CreateProcessAsUserW(
+		user_token,
+		&app_w[0],
+		&commandline_w[0],
+		nil,	// lpProcessAttributes,
+		nil,	// lpThreadAttributes,
+		false,	// bInheritHandles,
+    	0,		// creation flags
+    	nil,	// environment,
+    	nil,	// current directory: inherit from parent if nil
+    	&si,
+    	pi,
+    ));
+    if ok {
+    	if wait {
+	    	WaitForSingleObject(pi.hProcess, INFINITE);
+    		CloseHandle(pi.hProcess);
+    		CloseHandle(pi.hThread);
+    	}
+    	return true;
+    } else {
+    	return false;
+    }
+}