python-win-ad 0.6.1__tar.gz → 0.6.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/PKG-INFO +4 -3
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/pyad/__init__.py +4 -0
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/pyad/adbase.py +6 -3
- python_win_ad-0.6.3/pyad/adcontainer.py +238 -0
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/pyad/adobject.py +35 -2
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/pyad/pyadexceptions.py +8 -4
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/pyad/pyadutils.py +85 -14
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/python_win_ad.egg-info/PKG-INFO +4 -3
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/python_win_ad.egg-info/SOURCES.txt +3 -1
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/setup.py +2 -3
- python_win_ad-0.6.3/tests/tests_adbase.py +22 -0
- python_win_ad-0.6.3/tests/tests_adquery.py +53 -0
- python-win-ad-0.6.1/pyad/adcontainer.py +0 -123
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/LICENSE +0 -0
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/README.rst +0 -0
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/pyad/adcomputer.py +0 -0
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/pyad/addomain.py +0 -0
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/pyad/adgroup.py +0 -0
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/pyad/adquery.py +0 -0
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/pyad/adsearch.py +0 -0
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/pyad/aduser.py +0 -0
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/pyad/pyad.py +0 -0
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/pyad/pyadconstants.py +0 -0
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/python_win_ad.egg-info/dependency_links.txt +0 -0
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/python_win_ad.egg-info/requires.txt +0 -0
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/python_win_ad.egg-info/top_level.txt +0 -0
- {python-win-ad-0.6.1 → python_win_ad-0.6.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-win-ad
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: An Object-Oriented Active Directory management framework built on ADSI
|
|
5
5
|
Home-page: https://github.com/jcarswell/pyad/
|
|
6
6
|
Author: Zakir Durumeric
|
|
@@ -17,14 +17,15 @@ Classifier: Natural Language :: English
|
|
|
17
17
|
Classifier: Operating System :: Microsoft :: Windows
|
|
18
18
|
Classifier: Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP
|
|
19
19
|
Classifier: Programming Language :: Python :: 3
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
22
20
|
Classifier: Programming Language :: Python :: 3.8
|
|
23
21
|
Classifier: Programming Language :: Python :: 3.9
|
|
24
22
|
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
25
24
|
Obsoletes: pyad
|
|
26
25
|
Requires-Python: >=3.6
|
|
27
26
|
License-File: LICENSE
|
|
27
|
+
Requires-Dist: setuptools
|
|
28
|
+
Requires-Dist: pywin32
|
|
28
29
|
|
|
29
30
|
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
|
30
31
|
:align: center
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import sys
|
|
2
3
|
import win32com.client
|
|
3
4
|
|
|
4
5
|
from .pyadexceptions import SetupError
|
|
5
6
|
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
6
8
|
_adsi_provider = win32com.client.Dispatch("ADsNameSpaces")
|
|
7
9
|
|
|
8
10
|
try:
|
|
9
11
|
# Discover default domain and forest information
|
|
10
12
|
__default_domain_obj = _adsi_provider.GetObject("", "LDAP://rootDSE")
|
|
11
13
|
except:
|
|
12
|
-
# If there was an error, this
|
|
13
|
-
|
|
14
|
-
"
|
|
14
|
+
# If there was an error, this computer might not be on a domain.
|
|
15
|
+
logger.info(
|
|
16
|
+
"Unable to connect to default domain. "
|
|
17
|
+
"Computer is likely not attached to an AD domain."
|
|
15
18
|
)
|
|
16
19
|
__default_domain_obj = None
|
|
17
20
|
_default_detected_forest = None
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import pywintypes
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List
|
|
4
|
+
from .adobject import ADObject
|
|
5
|
+
from .aduser import ADUser
|
|
6
|
+
from .adcomputer import ADComputer
|
|
7
|
+
from .adgroup import ADGroup
|
|
8
|
+
from . import pyadconstants
|
|
9
|
+
from . import pyadutils
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ADContainer(ADObject):
|
|
13
|
+
def get_children_iter(
|
|
14
|
+
self, recursive: bool = False, filter: List[ADObject] = None
|
|
15
|
+
) -> ADObject:
|
|
16
|
+
"""
|
|
17
|
+
Iterate over the children objects in the container.
|
|
18
|
+
|
|
19
|
+
:param recursive: enumerate all containers with in the object,
|
|
20
|
+
defaults to False
|
|
21
|
+
:type recursive: bool, optional
|
|
22
|
+
:param filter: filter to only specific object classes ie ADUser,
|
|
23
|
+
defaults to None
|
|
24
|
+
:type filter: List[ADObject], optional
|
|
25
|
+
:yield: _description_
|
|
26
|
+
:rtype: _type_
|
|
27
|
+
"""
|
|
28
|
+
for com_object in self._ldap_adsi_obj:
|
|
29
|
+
q = ADObject.from_com_object(com_object)
|
|
30
|
+
q.adjust_pyad_type()
|
|
31
|
+
if q.type == "organizationalUnit" and recursive:
|
|
32
|
+
for c in q.get_children_iter(recursive=recursive):
|
|
33
|
+
if not filter or c.__class__ in filter:
|
|
34
|
+
yield c
|
|
35
|
+
if not filter or q.__class__ in filter:
|
|
36
|
+
yield q
|
|
37
|
+
|
|
38
|
+
def get_children(
|
|
39
|
+
self, recursive: bool = False, filter: List[ADObject] = None
|
|
40
|
+
) -> list:
|
|
41
|
+
"""
|
|
42
|
+
returns a list of child containers with in the current container. Optionally
|
|
43
|
+
the list can be filtered to specific object classes and search through child
|
|
44
|
+
containers recursively.
|
|
45
|
+
|
|
46
|
+
:param recursive: include children from sub-containers, defaults to False
|
|
47
|
+
:type recursive: bool, optional
|
|
48
|
+
:param filter: filter to only specific object classes ie ADUser, defaults to None
|
|
49
|
+
:type filter: List[ADObject], optional
|
|
50
|
+
:return: a list of all child objects
|
|
51
|
+
:rtype: list
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
return list(self.get_children_iter(recursive=recursive, filter=filter))
|
|
55
|
+
|
|
56
|
+
def __create_object(self, type_: str, name: str) -> ADObject:
|
|
57
|
+
prefix = "ou" if type_ == "organizationalUnit" else "cn"
|
|
58
|
+
prefixed_name = "=".join((prefix, name))
|
|
59
|
+
return self._ldap_adsi_obj.Create(type_, prefixed_name)
|
|
60
|
+
|
|
61
|
+
def create_user(
|
|
62
|
+
self,
|
|
63
|
+
name: str,
|
|
64
|
+
password: str = None,
|
|
65
|
+
upn_suffix: str = None,
|
|
66
|
+
enable: bool = True,
|
|
67
|
+
optional_attributes: dict = {},
|
|
68
|
+
) -> ADUser:
|
|
69
|
+
"""
|
|
70
|
+
Create a new user object in the container.
|
|
71
|
+
|
|
72
|
+
:param name: The name of the user
|
|
73
|
+
:type name: str
|
|
74
|
+
:param password: The password for the user it is strongly recommended to
|
|
75
|
+
populate this parameter as the account will be created with password
|
|
76
|
+
not required which will not get clear within AD leading to a security
|
|
77
|
+
vulnerability.
|
|
78
|
+
:type password: str, optional
|
|
79
|
+
:param upn_suffix: The upn suffix for the user, defaults to default upn
|
|
80
|
+
suffix for the domain.
|
|
81
|
+
:type upn_suffix: str, optional
|
|
82
|
+
:param enable: Whether the user should be enabled or disabled, defaults to True
|
|
83
|
+
:type enable: bool, optional
|
|
84
|
+
:param optional_attributes: List of additional attribute to set, defaults to {}
|
|
85
|
+
:type optional_attributes: dict, optional
|
|
86
|
+
:return: the created user object
|
|
87
|
+
:rtype: ADUser
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
pyadobj = None
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
if not upn_suffix:
|
|
94
|
+
upn_suffix = self.get_domain().get_default_upn()
|
|
95
|
+
upn = "@".join((name, upn_suffix))
|
|
96
|
+
obj = self.__create_object("user", name)
|
|
97
|
+
obj.Put("sAMAccountName", optional_attributes.get("sAMAccountName", name))
|
|
98
|
+
obj.Put("userPrincipalName", upn)
|
|
99
|
+
obj.SetInfo()
|
|
100
|
+
pyadobj = ADUser.from_com_object(obj)
|
|
101
|
+
|
|
102
|
+
if enable:
|
|
103
|
+
pyadobj.enable()
|
|
104
|
+
|
|
105
|
+
if password:
|
|
106
|
+
pyadobj.set_password(password)
|
|
107
|
+
pyadobj.set_user_account_control_setting("PASSWD_NOTREQD", False)
|
|
108
|
+
|
|
109
|
+
pyadobj.set_user_account_control_setting("NORMAL_ACCOUNT", True)
|
|
110
|
+
pyadobj.update_attributes(optional_attributes)
|
|
111
|
+
return pyadobj
|
|
112
|
+
except pywintypes.com_error as e:
|
|
113
|
+
if pyadobj:
|
|
114
|
+
# clean up the object if it was created
|
|
115
|
+
pyadobj.delete()
|
|
116
|
+
pyadutils.pass_up_com_exception(e)
|
|
117
|
+
|
|
118
|
+
def create_group(
|
|
119
|
+
self,
|
|
120
|
+
name: str,
|
|
121
|
+
security_enabled: bool = True,
|
|
122
|
+
scope: str = "GLOBAL",
|
|
123
|
+
optional_attributes: Dict[str, Any] = {},
|
|
124
|
+
) -> ADGroup:
|
|
125
|
+
"""
|
|
126
|
+
Create a new group object in the container
|
|
127
|
+
|
|
128
|
+
:param name: The Group Name
|
|
129
|
+
:type name: str
|
|
130
|
+
:param security_enabled: If this is a security enabled group or a distribution
|
|
131
|
+
group, defaults to True
|
|
132
|
+
:type security_enabled: bool, optional
|
|
133
|
+
:param scope: The scope of group. Must be one of [GLOBAL, LOCAL, UNIVERSAL].
|
|
134
|
+
Defaults to "GLOBAL"
|
|
135
|
+
:type scope: str, optional
|
|
136
|
+
:param optional_attributes: Additional attributes to set when creating the
|
|
137
|
+
object, defaults to {}
|
|
138
|
+
:type optional_attributes: Dict[str,Any], optional
|
|
139
|
+
:return: The created group object
|
|
140
|
+
:rtype: ADGroup
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
obj = None
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
obj = self.__create_object("group", name)
|
|
147
|
+
obj.Put("sAMAccountName", name)
|
|
148
|
+
val = pyadconstants.ADS_GROUP_TYPE[scope]
|
|
149
|
+
if security_enabled:
|
|
150
|
+
val = val | pyadconstants.ADS_GROUP_TYPE["SECURITY_ENABLED"]
|
|
151
|
+
obj.Put("groupType", val)
|
|
152
|
+
obj.SetInfo()
|
|
153
|
+
pyadobj = ADGroup.from_com_object(obj)
|
|
154
|
+
pyadobj.update_attributes(optional_attributes)
|
|
155
|
+
return pyadobj
|
|
156
|
+
except pywintypes.com_error as e:
|
|
157
|
+
if obj:
|
|
158
|
+
obj.delete()
|
|
159
|
+
pyadutils.pass_up_com_exception(e)
|
|
160
|
+
except KeyError:
|
|
161
|
+
if obj:
|
|
162
|
+
obj.delete()
|
|
163
|
+
raise ValueError(f"Invalid scope: {scope}")
|
|
164
|
+
|
|
165
|
+
def create_container(
|
|
166
|
+
self, name: str, optional_attributes: Dict[str, Any] = {}
|
|
167
|
+
) -> "ADContainer":
|
|
168
|
+
"""
|
|
169
|
+
Create a new organizational unit in the container
|
|
170
|
+
|
|
171
|
+
:param name: The name of the container
|
|
172
|
+
:type name: str
|
|
173
|
+
:param optional_attributes: Additional attributes to set when creating the
|
|
174
|
+
object, defaults to {}
|
|
175
|
+
:type optional_attributes: Dict[str,Any], optional
|
|
176
|
+
:return: the created container object
|
|
177
|
+
:rtype: ADContainer
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
obj = None
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
obj = self.__create_object("organizationalUnit", name)
|
|
184
|
+
obj.SetInfo()
|
|
185
|
+
pyadobj = ADContainer.from_com_object(obj)
|
|
186
|
+
pyadobj.update_attributes(optional_attributes)
|
|
187
|
+
return pyadobj
|
|
188
|
+
except pywintypes.com_error as e:
|
|
189
|
+
if obj:
|
|
190
|
+
obj.delete()
|
|
191
|
+
pyadutils.pass_up_com_exception(e)
|
|
192
|
+
|
|
193
|
+
def create_computer(
|
|
194
|
+
self, name: str, enable: bool = True, optional_attributes: Dict[str, Any] = {}
|
|
195
|
+
) -> ADComputer:
|
|
196
|
+
"""
|
|
197
|
+
Create a new computer object in the container
|
|
198
|
+
|
|
199
|
+
:param name: The computer name
|
|
200
|
+
:type name: str
|
|
201
|
+
:param enable: Whether the computer should be enabled or disabled,
|
|
202
|
+
defaults to True
|
|
203
|
+
:type enable: bool, optional
|
|
204
|
+
:param optional_attributes: Additional attributes to set when creating the
|
|
205
|
+
object, defaults to {}
|
|
206
|
+
:type optional_attributes: Dict[str,Any], optional
|
|
207
|
+
:return: the created computer object
|
|
208
|
+
:rtype: ADComputer
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
obj = None
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
obj = self.__create_object("computer", name)
|
|
215
|
+
obj.Put("sAMAccountName", name + "$")
|
|
216
|
+
if enable:
|
|
217
|
+
obj.Put("userAccountControl", 4128)
|
|
218
|
+
else:
|
|
219
|
+
obj.Put("userAccountControl", 4130)
|
|
220
|
+
obj.SetInfo()
|
|
221
|
+
pyadobj = ADComputer.from_com_object(obj)
|
|
222
|
+
if enable:
|
|
223
|
+
pyadobj.enable()
|
|
224
|
+
pyadobj.update_attributes(optional_attributes)
|
|
225
|
+
return pyadobj
|
|
226
|
+
except pywintypes.com_error as e:
|
|
227
|
+
if obj:
|
|
228
|
+
obj.delete()
|
|
229
|
+
pyadutils.pass_up_com_exception(e)
|
|
230
|
+
|
|
231
|
+
def remove_child(self, child: ADObject) -> None:
|
|
232
|
+
"""Removes the child object from the domain"""
|
|
233
|
+
|
|
234
|
+
self._ldap_adsi_obj.Delete(child.type, child.prefixed_cn)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
ADObject._py_ad_object_mappings["organizationalUnit"] = ADContainer
|
|
238
|
+
ADObject._py_ad_object_mappings["container"] = ADContainer
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import win32com
|
|
2
3
|
import pywintypes
|
|
3
4
|
import time
|
|
@@ -16,6 +17,7 @@ from .pyadconstants import (
|
|
|
16
17
|
ADS_USER_FLAG,
|
|
17
18
|
)
|
|
18
19
|
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
19
21
|
|
|
20
22
|
@total_ordering
|
|
21
23
|
class ADObject(ADBase):
|
|
@@ -231,6 +233,37 @@ class ADObject(ADBase):
|
|
|
231
233
|
else:
|
|
232
234
|
raise AttributeError(attribute)
|
|
233
235
|
|
|
236
|
+
def __getitem__(self, key: str):
|
|
237
|
+
"""
|
|
238
|
+
Get an attribute from the ADObject attributes.
|
|
239
|
+
|
|
240
|
+
:param key: The attribute name to retrieve
|
|
241
|
+
:type key: str
|
|
242
|
+
:raises AttributeError: if the key does not exist
|
|
243
|
+
:return: The value of the attribute
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
if hasattr(self._ldap_adsi_obj, key):
|
|
247
|
+
return self.get_attribute(key, False)
|
|
248
|
+
else:
|
|
249
|
+
raise AttributeError(key)
|
|
250
|
+
|
|
251
|
+
def __setitem__(self, key: str, value):
|
|
252
|
+
"""
|
|
253
|
+
Set an attribute on the ADObject.
|
|
254
|
+
|
|
255
|
+
:param key: The attribute name to set
|
|
256
|
+
:type key: str
|
|
257
|
+
:param value: The value to set the attribute to
|
|
258
|
+
:type value: Any
|
|
259
|
+
:raises AttributeError: if the key does not exist
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
if hasattr(self._ldap_adsi_obj, key):
|
|
263
|
+
self.update_attribute(key, value)
|
|
264
|
+
else:
|
|
265
|
+
raise AttributeError(key)
|
|
266
|
+
|
|
234
267
|
def _flush(self):
|
|
235
268
|
"""Commits any changes to the AD object."""
|
|
236
269
|
self._ldap_adsi_obj.SetInfo()
|
|
@@ -633,7 +666,7 @@ class ADObject(ADBase):
|
|
|
633
666
|
)
|
|
634
667
|
node.appendChild(text)
|
|
635
668
|
except:
|
|
636
|
-
|
|
669
|
+
logger.error("attribute: %s not xml-able" % attribute)
|
|
637
670
|
else:
|
|
638
671
|
node.setAttribute("type", "multiValued")
|
|
639
672
|
ok_elem = False
|
|
@@ -653,7 +686,7 @@ class ADObject(ADBase):
|
|
|
653
686
|
node.appendChild(valnode)
|
|
654
687
|
ok_elem = True
|
|
655
688
|
except:
|
|
656
|
-
|
|
689
|
+
logger.error("attribute: %s not xml-able" % attribute)
|
|
657
690
|
if ok_elem:
|
|
658
691
|
adobj_xml_doc.appendChild(node)
|
|
659
692
|
return doc.toxml(encoding="UTF-8")
|
|
@@ -46,14 +46,18 @@ class InvalidObjectException(noObjectFoundException, win32Exception):
|
|
|
46
46
|
class InvalidAttribute(BaseException, AttributeError):
|
|
47
47
|
def __str__(self):
|
|
48
48
|
return (
|
|
49
|
-
'The attribute "%s" is not permitted by the schema definition of the
|
|
49
|
+
'The attribute "%s" is not permitted by the schema definition of the '
|
|
50
|
+
'object "%s" (the requested attribute does not exist).'
|
|
50
51
|
% (self.attribute, self.obj)
|
|
51
52
|
)
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
class noExecutedQuery(BaseException):
|
|
55
56
|
def __str__(self):
|
|
56
|
-
return
|
|
57
|
+
return (
|
|
58
|
+
"No query has been executed. Therefore there are no results to return. "
|
|
59
|
+
"Execute a query before requesting results."
|
|
60
|
+
)
|
|
57
61
|
|
|
58
62
|
|
|
59
63
|
class invalidResults(BaseException):
|
|
@@ -62,8 +66,8 @@ class invalidResults(BaseException):
|
|
|
62
66
|
|
|
63
67
|
def __str__(self):
|
|
64
68
|
return (
|
|
65
|
-
"The specified query returned %i results. getSingleResults only functions
|
|
66
|
-
% self.number_results
|
|
69
|
+
"The specified query returned %i results. getSingleResults only functions "
|
|
70
|
+
"with a single result." % self.number_results
|
|
67
71
|
)
|
|
68
72
|
|
|
69
73
|
|
|
@@ -41,17 +41,34 @@ def validate_credentials(
|
|
|
41
41
|
return None
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def convert_error_code(error_code):
|
|
44
|
+
def convert_error_code(error_code: int) -> int:
|
|
45
45
|
"""
|
|
46
46
|
Convert error code from the format returned by pywin32 to the format that Microsoft
|
|
47
47
|
documents everything in.
|
|
48
|
+
|
|
49
|
+
:param error_code: error code
|
|
50
|
+
:type error_code: int
|
|
51
|
+
:return: The error code in the format Microsoft documents it
|
|
52
|
+
:rtype: int
|
|
48
53
|
"""
|
|
49
54
|
|
|
50
55
|
return error_code % 2**32
|
|
51
56
|
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
def interpret_com_exception(
|
|
59
|
+
excp: "pywintype.com_error", additional_info: dict = {}
|
|
60
|
+
) -> dict:
|
|
61
|
+
"""
|
|
62
|
+
Convert a pywin32 com_error exception into a dictionary of error information.
|
|
63
|
+
|
|
64
|
+
:param excp: pywin32 com_error exception
|
|
65
|
+
:type excp: pywintype.com_error
|
|
66
|
+
:param additional_info: any additional information with the error, defaults to {}
|
|
67
|
+
:type additional_info: dict, optional
|
|
68
|
+
:return: a dictionary of error information
|
|
69
|
+
:rtype: dict
|
|
70
|
+
"""
|
|
71
|
+
|
|
55
72
|
d = {}
|
|
56
73
|
d["error_num"] = convert_error_code(excp.args[2][5])
|
|
57
74
|
# for some reason hex() includes the L for long in the hex...
|
|
@@ -87,7 +104,18 @@ def interpret_com_exception(excp, additional_info={}):
|
|
|
87
104
|
return d
|
|
88
105
|
|
|
89
106
|
|
|
90
|
-
def pass_up_com_exception(excp, additional_info={}):
|
|
107
|
+
def pass_up_com_exception(excp: "pywintype.com_error", additional_info: dict = {}):
|
|
108
|
+
"""
|
|
109
|
+
reparse the com_error into a sane exception and raise it.
|
|
110
|
+
|
|
111
|
+
:param excp: the com_error exception
|
|
112
|
+
:type excp: pywintype.com_error
|
|
113
|
+
:param additional_info: Additional exception details, defaults to {}
|
|
114
|
+
:type additional_info: dict, optional
|
|
115
|
+
:raises excp: if we don't know how to handle the exception raise the original
|
|
116
|
+
exception
|
|
117
|
+
"""
|
|
118
|
+
|
|
91
119
|
if excp.__class__ in (genericADSIException, comException, win32Exception):
|
|
92
120
|
raise excp
|
|
93
121
|
else:
|
|
@@ -110,9 +138,14 @@ def pass_up_com_exception(excp, additional_info={}):
|
|
|
110
138
|
|
|
111
139
|
|
|
112
140
|
def convert_datetime(adsi_time_com_obj):
|
|
113
|
-
"""
|
|
114
|
-
|
|
115
|
-
|
|
141
|
+
"""
|
|
142
|
+
Converts 64-bit integer COM object representing time into
|
|
143
|
+
a python datetime object.
|
|
144
|
+
|
|
145
|
+
Credit goes to John Nielsen who documented this at
|
|
146
|
+
`<http://docs.activestate.com/activepython/2.6/pywin32/html/com/help/active_directory.html>`_.
|
|
147
|
+
"""
|
|
148
|
+
|
|
116
149
|
if not hasattr(adsi_time_com_obj, "highpart") or not hasattr(
|
|
117
150
|
adsi_time_com_obj, "lowpart"
|
|
118
151
|
):
|
|
@@ -133,19 +166,38 @@ def convert_datetime(adsi_time_com_obj):
|
|
|
133
166
|
return datetime.datetime.fromtimestamp(date_value)
|
|
134
167
|
|
|
135
168
|
|
|
136
|
-
def convert_bigint(obj):
|
|
137
|
-
|
|
169
|
+
def convert_bigint(obj) -> int:
|
|
170
|
+
"""
|
|
171
|
+
Converts a ADSI time object to an integer.
|
|
172
|
+
|
|
173
|
+
based on http://www.selfadsi.org/ads-attributes/user-usnChanged.htm
|
|
174
|
+
|
|
175
|
+
:param obj: the AD bigint object
|
|
176
|
+
:raises AttributeError: invalid object type
|
|
177
|
+
:return: the decimal value of the object
|
|
178
|
+
:rtype: int
|
|
179
|
+
"""
|
|
180
|
+
|
|
138
181
|
if hasattr(obj, "HighPart") and hasattr(obj, "LowPart"):
|
|
139
182
|
h, l = obj.HighPart, obj.LowPart
|
|
140
183
|
if l < 0:
|
|
141
184
|
h += 1
|
|
142
185
|
return (h << 32) + l
|
|
143
186
|
else:
|
|
144
|
-
raise
|
|
187
|
+
raise AttributeError(
|
|
188
|
+
f"Expected adsi time object got '{obj.__class__.__name__}'"
|
|
189
|
+
)
|
|
190
|
+
|
|
145
191
|
|
|
192
|
+
def convert_timespan(obj) -> datetime.timedelta:
|
|
193
|
+
"""
|
|
194
|
+
Converts COM object representing time span to a python time span object.
|
|
195
|
+
|
|
196
|
+
:param obj: ADSI time span object
|
|
197
|
+
:return: the python timedelta object
|
|
198
|
+
:rtype: datetime.timedelta
|
|
199
|
+
"""
|
|
146
200
|
|
|
147
|
-
def convert_timespan(obj):
|
|
148
|
-
"""Converts COM object representing time span to a python time span object."""
|
|
149
201
|
as_seconds = (
|
|
150
202
|
abs(convert_bigint(obj)) / 10000000
|
|
151
203
|
) # number of 100 nanoseconds in a second
|
|
@@ -160,7 +212,17 @@ def convert_sid(sid_object):
|
|
|
160
212
|
return pywintypes.SID(bytes(sid_object))
|
|
161
213
|
|
|
162
214
|
|
|
163
|
-
def generate_list(input):
|
|
215
|
+
def generate_list(input) -> list:
|
|
216
|
+
"""
|
|
217
|
+
converts a set or tuple to a list or returns the input in a list if it is not
|
|
218
|
+
a list.
|
|
219
|
+
|
|
220
|
+
:param input: a list like object or any
|
|
221
|
+
:type input: list, set, tuple, Any
|
|
222
|
+
:return: a list
|
|
223
|
+
:rtype: list
|
|
224
|
+
"""
|
|
225
|
+
|
|
164
226
|
if type(input) is list:
|
|
165
227
|
return input
|
|
166
228
|
elif type(input) in (set, tuple):
|
|
@@ -171,7 +233,16 @@ def generate_list(input):
|
|
|
171
233
|
]
|
|
172
234
|
|
|
173
235
|
|
|
174
|
-
def escape_path(path):
|
|
236
|
+
def escape_path(path: str) -> str:
|
|
237
|
+
"""
|
|
238
|
+
escapes a path for use in ADSI.
|
|
239
|
+
|
|
240
|
+
:param path: the raw path to escape
|
|
241
|
+
:type path: str
|
|
242
|
+
:return: the escaped path
|
|
243
|
+
:rtype: str
|
|
244
|
+
"""
|
|
245
|
+
|
|
175
246
|
escapes = (
|
|
176
247
|
("\+", "+"),
|
|
177
248
|
("\*", "*"),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-win-ad
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: An Object-Oriented Active Directory management framework built on ADSI
|
|
5
5
|
Home-page: https://github.com/jcarswell/pyad/
|
|
6
6
|
Author: Zakir Durumeric
|
|
@@ -17,14 +17,15 @@ Classifier: Natural Language :: English
|
|
|
17
17
|
Classifier: Operating System :: Microsoft :: Windows
|
|
18
18
|
Classifier: Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP
|
|
19
19
|
Classifier: Programming Language :: Python :: 3
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.6
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
22
20
|
Classifier: Programming Language :: Python :: 3.8
|
|
23
21
|
Classifier: Programming Language :: Python :: 3.9
|
|
24
22
|
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
25
24
|
Obsoletes: pyad
|
|
26
25
|
Requires-Python: >=3.6
|
|
27
26
|
License-File: LICENSE
|
|
27
|
+
Requires-Dist: setuptools
|
|
28
|
+
Requires-Dist: pywin32
|
|
28
29
|
|
|
29
30
|
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
|
30
31
|
:align: center
|
|
@@ -19,4 +19,6 @@ python_win_ad.egg-info/PKG-INFO
|
|
|
19
19
|
python_win_ad.egg-info/SOURCES.txt
|
|
20
20
|
python_win_ad.egg-info/dependency_links.txt
|
|
21
21
|
python_win_ad.egg-info/requires.txt
|
|
22
|
-
python_win_ad.egg-info/top_level.txt
|
|
22
|
+
python_win_ad.egg-info/top_level.txt
|
|
23
|
+
tests/tests_adbase.py
|
|
24
|
+
tests/tests_adquery.py
|
|
@@ -12,7 +12,7 @@ def read(fname):
|
|
|
12
12
|
|
|
13
13
|
setup(
|
|
14
14
|
name="python-win-ad",
|
|
15
|
-
version="0.6.
|
|
15
|
+
version="0.6.3",
|
|
16
16
|
author="Zakir Durumeric",
|
|
17
17
|
author_email="zakird@gmail.com",
|
|
18
18
|
maintainer="Josh Carswell",
|
|
@@ -38,11 +38,10 @@ setup(
|
|
|
38
38
|
"Operating System :: Microsoft :: Windows",
|
|
39
39
|
"Topic :: System :: Systems Administration :: Authentication/Directory :: LDAP",
|
|
40
40
|
"Programming Language :: Python :: 3",
|
|
41
|
-
"Programming Language :: Python :: 3.6",
|
|
42
|
-
"Programming Language :: Python :: 3.7",
|
|
43
41
|
"Programming Language :: Python :: 3.8",
|
|
44
42
|
"Programming Language :: Python :: 3.9",
|
|
45
43
|
"Programming Language :: Python :: 3.10",
|
|
44
|
+
"Programming Language :: Python :: 3.11",
|
|
46
45
|
],
|
|
47
46
|
install_requires=["setuptools", "pywin32"],
|
|
48
47
|
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from .pyadunittest import ADTestCase
|
|
2
|
+
from pyad.adbase import ADBase
|
|
3
|
+
import pyad
|
|
4
|
+
|
|
5
|
+
class TestADBase(ADTestCase):
|
|
6
|
+
def setUp(self):
|
|
7
|
+
# set all defaults back to their default
|
|
8
|
+
ADBase.default_ldap_server = None
|
|
9
|
+
ADBase.default_gc_server = None
|
|
10
|
+
ADBase.default_ldap_port = None
|
|
11
|
+
ADBase.default_gc_port = None
|
|
12
|
+
|
|
13
|
+
def test_detected_forest(self):
|
|
14
|
+
self.assertEqual(ADBase.default_domain, self.SANDBOX_DOMAIN)
|
|
15
|
+
|
|
16
|
+
def test_detected_domain(self):
|
|
17
|
+
self.assertEqual(ADBase.default_forest, self.SANDBOX_FOREST)
|
|
18
|
+
|
|
19
|
+
def test_set_defaults(self):
|
|
20
|
+
pyad.set_defaults(ldap_server = self.TEST_DC, ldap_port = 389)
|
|
21
|
+
self.assertEqual(ADBase.default_ldap_server, self.TEST_DC)
|
|
22
|
+
self.assertEqual(ADBase.default_ldap_port, 389)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from .pyadunittest import ADTestCase
|
|
2
|
+
import pyad
|
|
3
|
+
|
|
4
|
+
class TestADQuery(ADTestCase):
|
|
5
|
+
def setUp(self):
|
|
6
|
+
self.ad_query = pyad.ADQuery()
|
|
7
|
+
|
|
8
|
+
def test_dne_rowcount(self):
|
|
9
|
+
query = "cn = '%s'" % self.KNOWN_DNE_OBJECT
|
|
10
|
+
self.ad_query.execute_query(where_clause = query)
|
|
11
|
+
self.assertEqual(self.ad_query.row_count, 0)
|
|
12
|
+
|
|
13
|
+
def test_dne_single_result(self):
|
|
14
|
+
query = "cn = '%s'" % self.KNOWN_DNE_OBJECT
|
|
15
|
+
self.ad_query.execute_query(where_clause = query)
|
|
16
|
+
self.assertEquals(self.ad_query.get_single_result,{})
|
|
17
|
+
|
|
18
|
+
def test_dne_all_results(self):
|
|
19
|
+
query = "cn = '%s'" % self.KNOWN_DNE_OBJECT
|
|
20
|
+
self.ad_query.execute_query(where_clause = query)
|
|
21
|
+
self.assertEqual(self.ad_query.get_all_results(), [])
|
|
22
|
+
|
|
23
|
+
def test_single_rowcount(self):
|
|
24
|
+
query = "cn = '%s'" % self.KNOWN_EXISTS_USER
|
|
25
|
+
self.ad_query.execute_query(where_clause = query)
|
|
26
|
+
self.assertEqual(self.ad_query.get_row_count(), 1)
|
|
27
|
+
|
|
28
|
+
def test_single_single_result(self):
|
|
29
|
+
query = "cn = '%s'" % self.KNOWN_EXISTS_USER
|
|
30
|
+
self.ad_query.execute_query(attributes=("cn","distinguishedname"),where_clause = query)
|
|
31
|
+
self.assertEqual(self.ad_query.get_single_result()['cn'],self.KNOWN_EXISTS_USER)
|
|
32
|
+
|
|
33
|
+
def test_single_all_results(self):
|
|
34
|
+
query = "cn = '%s'" % self.KNOWN_EXISTS_USER
|
|
35
|
+
self.ad_query.execute_query(attributes=("cn","distinguishedname"),where_clause = query)
|
|
36
|
+
self.assertEqual(self.ad_query.get_all_results()[0]['cn'],self.KNOWN_EXISTS_USER)
|
|
37
|
+
|
|
38
|
+
def test_multiple_rowcount(self):
|
|
39
|
+
query = "cn = '%s' or cn = '%s'" % (self.KNOWN_EXISTS_USER, self.KNOWN_EXISTS_COMPUTER)
|
|
40
|
+
self.ad_query.execute_query(where_clause = query)
|
|
41
|
+
self.assertEqual(self.ad_query.row_count, 2)
|
|
42
|
+
|
|
43
|
+
def test_multiple_single_result(self):
|
|
44
|
+
query = "cn = '%s' or cn = '%s'" % (self.KNOWN_EXISTS_USER, self.KNOWN_EXISTS_COMPUTER)
|
|
45
|
+
self.ad_query.execute_query(where_clause = query)
|
|
46
|
+
self.assertIsInstance(self.ad_query.get_single_result,dict)
|
|
47
|
+
|
|
48
|
+
def test_multiple_all_results(self):
|
|
49
|
+
query = "cn = '%s' or cn = '%s'" % (self.KNOWN_EXISTS_USER, self.KNOWN_EXISTS_COMPUTER)
|
|
50
|
+
self.ad_query.execute_query(attributes=("cn","distinguishedname"),where_clause = query)
|
|
51
|
+
r = map(lambda x: x['cn'], self.ad_query.get_all_results())
|
|
52
|
+
k = [self.KNOWN_EXISTS_USER, self.KNOWN_EXISTS_COMPUTER]
|
|
53
|
+
self.assertEqual(r.sort(),k.sort())
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import pywintypes
|
|
2
|
-
|
|
3
|
-
from .adobject import ADObject
|
|
4
|
-
from .aduser import ADUser
|
|
5
|
-
from .adcomputer import ADComputer
|
|
6
|
-
from .adgroup import ADGroup
|
|
7
|
-
from . import pyadconstants
|
|
8
|
-
from . import pyadutils
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class ADContainer(ADObject):
|
|
12
|
-
def get_children_iter(self, recursive=False, filter=None):
|
|
13
|
-
for com_object in self._ldap_adsi_obj:
|
|
14
|
-
q = ADObject.from_com_object(com_object)
|
|
15
|
-
q.adjust_pyad_type()
|
|
16
|
-
if q.type == "organizationalUnit" and recursive:
|
|
17
|
-
for c in q.get_children_iter(recursive=recursive):
|
|
18
|
-
if not filter or c.__class__ in filter:
|
|
19
|
-
yield c
|
|
20
|
-
if not filter or q.__class__ in filter:
|
|
21
|
-
yield q
|
|
22
|
-
|
|
23
|
-
def get_children(self, recursive=False, filter=None) -> list:
|
|
24
|
-
"""
|
|
25
|
-
Iterate over the children objects in the container.
|
|
26
|
-
|
|
27
|
-
:param recursive: include children from sub-containers, defaults to False
|
|
28
|
-
:type recursive: bool, optional
|
|
29
|
-
:param filter: filter to only specific object classes ie ADUser, defaults to None
|
|
30
|
-
:type filter: object, optional
|
|
31
|
-
:return: a list of all child objects
|
|
32
|
-
:rtype: list
|
|
33
|
-
"""
|
|
34
|
-
return list(self.get_children_iter(recursive=recursive, filter=filter))
|
|
35
|
-
|
|
36
|
-
def __create_object(self, type_, name):
|
|
37
|
-
prefix = "ou" if type_ == "organizationalUnit" else "cn"
|
|
38
|
-
prefixed_name = "=".join((prefix, name))
|
|
39
|
-
return self._ldap_adsi_obj.Create(type_, prefixed_name)
|
|
40
|
-
|
|
41
|
-
def create_user(
|
|
42
|
-
self, name, password=None, upn_suffix=None, enable=True, optional_attributes={}
|
|
43
|
-
):
|
|
44
|
-
"""Create a new user object in the container"""
|
|
45
|
-
|
|
46
|
-
try:
|
|
47
|
-
if not upn_suffix:
|
|
48
|
-
upn_suffix = self.get_domain().get_default_upn()
|
|
49
|
-
upn = "@".join((name, upn_suffix))
|
|
50
|
-
obj = self.__create_object("user", name)
|
|
51
|
-
obj.Put("sAMAccountName", optional_attributes.get("sAMAccountName", name))
|
|
52
|
-
obj.Put("userPrincipalName", upn)
|
|
53
|
-
obj.SetInfo()
|
|
54
|
-
pyadobj = ADUser.from_com_object(obj)
|
|
55
|
-
if enable:
|
|
56
|
-
pyadobj.enable()
|
|
57
|
-
if password:
|
|
58
|
-
pyadobj.set_password(password)
|
|
59
|
-
pyadobj.set_user_account_control_setting("NORMAL_ACCOUNT", True)
|
|
60
|
-
pyadobj.set_user_account_control_setting("PASSWD_NOTREQD", False)
|
|
61
|
-
pyadobj.update_attributes(optional_attributes)
|
|
62
|
-
return pyadobj
|
|
63
|
-
except pywintypes.com_error as e:
|
|
64
|
-
pyadutils.pass_up_com_exception(e)
|
|
65
|
-
|
|
66
|
-
def create_group(
|
|
67
|
-
self, name, security_enabled=True, scope="GLOBAL", optional_attributes={}
|
|
68
|
-
):
|
|
69
|
-
"""Create a new group object in the container"""
|
|
70
|
-
|
|
71
|
-
try:
|
|
72
|
-
obj = self.__create_object("group", name)
|
|
73
|
-
obj.Put("sAMAccountName", name)
|
|
74
|
-
val = pyadconstants.ADS_GROUP_TYPE[scope]
|
|
75
|
-
if security_enabled:
|
|
76
|
-
val = val | pyadconstants.ADS_GROUP_TYPE["SECURITY_ENABLED"]
|
|
77
|
-
obj.Put("groupType", val)
|
|
78
|
-
obj.SetInfo()
|
|
79
|
-
pyadobj = ADGroup.from_com_object(obj)
|
|
80
|
-
pyadobj.update_attributes(optional_attributes)
|
|
81
|
-
return pyadobj
|
|
82
|
-
except pywintypes.com_error as e:
|
|
83
|
-
pyadutils.pass_up_com_exception(e)
|
|
84
|
-
|
|
85
|
-
def create_container(self, name, optional_attributes={}):
|
|
86
|
-
"""Create a new organizational unit in the container"""
|
|
87
|
-
|
|
88
|
-
try:
|
|
89
|
-
obj = self.__create_object("organizationalUnit", name)
|
|
90
|
-
obj.SetInfo()
|
|
91
|
-
pyadobj = ADContainer.from_com_object(obj)
|
|
92
|
-
pyadobj.update_attributes(optional_attributes)
|
|
93
|
-
return pyadobj
|
|
94
|
-
except pywintypes.com_error as e:
|
|
95
|
-
pyadutils.pass_up_com_exception(e)
|
|
96
|
-
|
|
97
|
-
def create_computer(self, name, enable=True, optional_attributes={}):
|
|
98
|
-
"""Create a new computer object in the container"""
|
|
99
|
-
|
|
100
|
-
try:
|
|
101
|
-
obj = self.__create_object("computer", name)
|
|
102
|
-
obj.Put("sAMAccountName", name + "$")
|
|
103
|
-
if enable:
|
|
104
|
-
obj.Put("userAccountControl", 4128)
|
|
105
|
-
else:
|
|
106
|
-
obj.Put("userAccountControl", 4130)
|
|
107
|
-
obj.SetInfo()
|
|
108
|
-
pyadobj = ADComputer.from_com_object(obj)
|
|
109
|
-
if enable:
|
|
110
|
-
pyadobj.enable()
|
|
111
|
-
pyadobj.update_attributes(optional_attributes)
|
|
112
|
-
return pyadobj
|
|
113
|
-
except pywintypes.com_error as e:
|
|
114
|
-
pyadutils.pass_up_com_exception(e)
|
|
115
|
-
|
|
116
|
-
def remove_child(self, child) -> None:
|
|
117
|
-
"""Removes the child object from the domain"""
|
|
118
|
-
|
|
119
|
-
self._ldap_adsi_obj.Delete(child.type, child.prefixed_cn)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
ADObject._py_ad_object_mappings["organizationalUnit"] = ADContainer
|
|
123
|
-
ADObject._py_ad_object_mappings["container"] = ADContainer
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|