Skip to content

Commit

Permalink
feature(rules): retrieve user extended data using win32 API (#494)
Browse files Browse the repository at this point in the history
This PR follows the 0.34 release to extend information when QDT is run
in a Windows domain. Sample output:

```json
{
    "date": {
        "current_day": 24,
        "current_month": 4,
        "current_weekday": 2,
        "current_year": 2024
    },
    "environment": {
        "computer_network_name": "VM200145",
        "linux_distribution_name": null,
        "linux_distribution_version": null,
        "operating_system_code": "win32",
        "operating_system_release": "10",
        "processor_architecture": "AMD64",
        "windows_edition": "Professional"
    },
    "user": {
        "groups_domain": [
            "GGD_FONC_HENRY-TMA",
            "GGD_ORGA_INTERVENANTS",
            "GGD_ORG_DG-HENRY_DES",
            "GGD_ORG_DG_TOUS",
            "GGD_SCM_PMAD-SI",
            "GGD_WIFI_AGENTS_TOUS",
            "GG_INSTALL_SAGE_EAU",
            "METROPOLIS_TOUS",
            "SITE_DOMICILE-BUREAU_SOCIETE_EXTERNE",
            "Tous_DDR",
            "Tous_HENRY",
            "Tous_HENRY_DINSI"
        ],
        "groups_local": [],
        "name": "ICHIGO",
        "windows_extended": {
            "NameCanonical": "bah.lady.glue/PERSONNES/METROPOLIS/HENRY/INTERVENANTS/ICHIGO",
            "NameCanonicalEx": "bah.lady.glue/PERSONNES/METROPOLIS/HENRY/INTERVENANTS\nICHIGO",
            "NameDisplay": "Julien M.",
            "NameDnsDomain": "bah.lady.glue\\ICHIGO",
            "NameFullyQualifiedDN": "CN=ICHIGO,OU=INTERVENANTS,OU=HENRY,OU=METROPOLIS,OU=PERSONNES,DC=ben,DC=oscar,DC=glue",
            "NameGivenName": "",
            "NameSamCompatible": "BEN\\ICHIGO",
            "NameServicePrincipal": "",
            "NameSurname": "",
            "NameUniqueId": "{123-2820-44s5b7-4654-5sbqeq5xcqx9qsbf}",
            "NameUnknown": "",
            "NameUserPrincipal": "ext.oslandia.julienm@tigre.com"
        }
    }
}
```
  • Loading branch information
Guts authored Apr 24, 2024
2 parents b7b2fac + 3592543 commit a46ec90
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 12 deletions.
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

0 comments on commit a46ec90

Please sign in to comment.