Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature(rules): retrieve user extended data using win32 API #494

Merged
merged 6 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions qgis_deployment_toolbelt/profiles/rules_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
get_user_domain_groups,
get_user_local_groups,
)
from qgis_deployment_toolbelt.utils.win32utils import (
ExtendedNameFormat,
get_current_user_extended_data,
)

# #############################################################################
# ########## Globals ###############
Expand Down Expand Up @@ -79,6 +83,7 @@ def _context_environment(self) -> dict:
return {
"computer_network_name": platform.node(),
"operating_system_code": opersys,
"operating_system_release": platform.release(),
"processor_architecture": platform.machine(),
# custom Linux
"linux_distribution_name": linux_distribution_name,
Expand All @@ -95,10 +100,18 @@ def _context_user(self) -> dict:
Returns:
dict: dict user information.
"""
if opersys == "win32":
windows_extended = {
k.name: get_current_user_extended_data(k) for k in ExtendedNameFormat
}
else:
windows_extended = None

return {
"name": getuser(),
"groups_local": get_user_local_groups(),
"groups_domain": get_user_domain_groups(),
"windows_extended": windows_extended,
}

# -- EXPORT
Expand Down
47 changes: 35 additions & 12 deletions qgis_deployment_toolbelt/utils/user_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,10 @@
"""windows"""

# 3rd party
import pyad
import win32com
import win32net

# try to import pyad
try:
import pyad
except ImportError:
logging.info("'pyad' package is not available.")
pyad = None
from pywintypes import error as PyWinException

else:
import grp
Expand Down Expand Up @@ -72,7 +67,19 @@ def get_user_local_groups(user_name: str | None = None) -> list[str]:
return sorted({g.gr_name for g in grp.getgrall() if user_name in g.gr_mem})
elif opersys.lower() in ("win32", "windows"):
server_host_name = uname()[1]
return sorted(set(win32net.NetUserGetLocalGroups(server_host_name, user_name)))
try:
local_groups = sorted(
set(win32net.NetUserGetLocalGroups(server_host_name, user_name))
)
except PyWinException as err:
logger.info(
f"Retrieving user ('{user_name}') local groups on {server_host_name} "
"failed. This usually means that it's not a local session, and that the "
"computer is linked to a domain and subscribed to a directory. "
f"Trace: {err}"
)
local_groups = []
return local_groups
else:
raise NotImplementedError(f"Unsupported operating system: {opersys}")

Expand Down Expand Up @@ -110,10 +117,26 @@ def get_user_domain_groups(user_name: str | None = None) -> list[str]:
# using pure ldap)
return []
elif opersys.lower() in ("win32", "windows"):
if pyad is not None:
user_obj = pyad.aduser.ADUser.from_cn(getuser())
return sorted(set(user_obj.get_attribute("memberOf")))
return []
user_obj = pyad.aduser.ADUser.from_cn(user_name)
user_groups: list[pyad.ADGroup] | None = user_obj.get_memberOfs()
if not user_groups or not len(user_groups):
logger.warning(
f"It looks like '{user_name}' does not belong to any domain group."
)
return []
else:
logger.info(
f"The current user '{user_name}' belongs to {len(user_groups)} Active "
"Directory groups."
)

return sorted(
{
grp.get_attribute("name")[0]
for grp in user_groups
if isinstance(grp, pyad.ADGroup)
}
)
else:
raise NotImplementedError(f"Unsupported operating system: {opersys}")

Expand Down
98 changes: 98 additions & 0 deletions qgis_deployment_toolbelt/utils/win32utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@
# ########## Libraries #############
# ##################################


# Standard library
import ctypes
import logging
from enum import Enum
from os import sep # required since pathlib strips trailing whitespace
from pathlib import Path
from sys import platform as opersys
Expand Down Expand Up @@ -42,6 +45,58 @@
)
user_hkey = (winreg.HKEY_CURRENT_USER, r"Environment")


# #############################################################################
# ########## Classes ###############
# ##################################


class ExtendedNameFormat(Enum):
"""Possible values for user name in a Windows Active Directory context.
See references:

- https://learn.microsoft.com/windows/win32/api/secext/ne-secext-extended_name_format
- https://mhammond.github.io/pywin32/win32api__GetUserNameEx_meth.html
- https://github.com/mhammond/pywin32/blob/dde12b8ef274a157aede18f46cae5b44f112be17/win32/Lib/win32con.py#L4963-L4973

"""

# An unknown name type.
NameUnknown = 0
# The fully qualified distinguished name (for example, CN=Jeff Smith,OU=Users,DC=Engineering,DC=Microsoft,DC=Com).
NameFullyQualifiedDN = 1
# A legacy account name (for example, Engineering\JSmith).
# The domain-only version includes trailing backslashes (\).
NameSamCompatible = 2
# A "friendly" display name (for example, Jeff Smith).
# The display name is not necessarily the defining relative distinguished name (RDN).
NameDisplay = 3
# A GUID string that the IIDFromString function returns (for example,
# {4fa050f0-f561-11cf-bdd9-00aa003a77b6}).
NameUniqueId = 6
# The complete canonical name (for example,
# engineering.microsoft.com/software/someone).
# The domain-only version includes a trailing forward slash (/).
NameCanonical = 7
# The user principal name (for example, someone@example.com).
NameUserPrincipal = 8
# The same as NameCanonical except that the rightmost forward slash (/) is replaced
# with a new line character (\n), even in a domain-only case (for example,
# engineering.microsoft.com/software\nJSmith).
NameCanonicalEx = 9
# The generalized service principal name (for example,
# www/www.microsoft.com@microsoft.com).
NameServicePrincipal = 10
# The DNS domain name followed by a backward-slash and the SAM user name.
NameDnsDomain = 12
# The first name or given name of the user. Note: This type is only available for
# GetUserNameEx calls for an Active Directory user.
NameGivenName = 13
# The last name or surname of the user. Note: This type is only available for
# GetUserNameEx calls for an Active Directory user.
NameSurname = 14


# #############################################################################
# ########## Functions #############
# ##################################
Expand Down Expand Up @@ -111,6 +166,49 @@ def get_environment_variable(envvar_name: str, scope: str = "user") -> str | Non
return None


def get_current_user_extended_data(extended_name_format: ExtendedNameFormat) -> str:
"""Get current user full data extended with Active Directory informations.

This method uses the ctypes module since the upper-level method implemented in
`pywin32.win32api` (`win32api.GetUserNameEx(extended_name_format.value)`) raises an
error when the specified format is not reachable.

Inspired from: https://stackoverflow.com/a/70182936/2556577

Returns:
str: use data in the specified format

Example:

.. code-block:: python

from qgis_deployment_toolbelt.utils.win32utils import (
ExtendedNameFormat,
get_current_user_extended_data,
)

user_data = {
k.name: get_current_user_extended_data(k)
for k in ExtendedNameFormat
}

print(user_data)

"""
format_index = extended_name_format.value

# use system DLL to call API
GetUserNameEx = ctypes.windll.secur32.GetUserNameExW

size = ctypes.pointer(ctypes.c_ulong(0))
GetUserNameEx(format_index, None, size)

nameBuffer = ctypes.create_unicode_buffer(size.contents.value)
GetUserNameEx(format_index, nameBuffer, size)

return nameBuffer.value


def normalize_path(input_path: Path, add_trailing_slash_if_dir: bool = True) -> str:
r"""Returns a path as normalized and fully escaped for Windows old-school file style.

Expand Down