msad 0.3.5__py3-none-any.whl → 0.4.0__py3-none-any.whl

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.
msad/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .main import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
msad/group.py CHANGED
@@ -17,51 +17,37 @@
17
17
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
18
18
 
19
19
  import logging
20
- from .search import *
21
20
 
21
+ from .search import get_dn, search
22
22
 
23
- def add_member(
24
- conn, search_base=None, group_name=None, group_dn=None, user_name=None, user_dn=None
25
- ):
26
- if group_name:
27
- group_dn = get_dn(conn, search_base, group_name)
23
+ def add_member(conn, search_base, group, user):
24
+ group_dn = get_dn(conn, search_base, group)
28
25
  if not group_dn:
29
- logging.error("group_name or group_dn must be passed and exist")
30
26
  return None
31
27
 
32
- if user_name:
33
- user_dn = get_dn(conn, search_base, user_name)
28
+ user_dn = get_dn(conn, search_base, user)
34
29
  if not user_dn:
35
- logging.error("user_name or user_dn must be passed and exist")
36
30
  return None
37
31
 
38
32
  return conn.extend.microsoft.add_members_to_groups([user_dn], [group_dn])
39
33
 
40
34
 
41
- def remove_member(
42
- conn, search_base=None, group_name=None, group_dn=None, user_name=None, user_dn=None
43
- ):
44
- if group_name:
45
- group_dn = get_dn(conn, search_base, group_name)
46
-
35
+ def remove_member(conn, search_base, group, user):
36
+ group_dn = get_dn(conn, search_base, group)
47
37
  if not group_dn:
48
- logging.error("group_name or group_dn must be passed and exist")
49
38
  return None
50
- if user_name:
51
- user_dn = get_dn(conn, search_base, user_name)
52
39
 
40
+ user_dn = get_dn(conn, search_base, user)
53
41
  if not user_dn:
54
- logging.error("user_name or user_dn must be passed and exist")
55
42
  return None
56
43
 
57
44
  return conn.extend.microsoft.remove_members_from_groups([user_dn], [group_dn])
58
45
 
59
46
 
60
47
  def group_flat_members(
61
- conn, search_base, limit, group_name=None, group_dn=None, attributes=None
48
+ conn, search_base, limit, group, attributes=None
62
49
  ):
63
- if group_name:
64
- group_dn = get_dn(conn, search_base, group_name)
50
+ group_dn = get_dn(conn, search_base, group)
65
51
 
66
52
  if not group_dn:
67
53
  return None
@@ -70,30 +56,23 @@ def group_flat_members(
70
56
  return search(conn, search_base, search_filter, attributes=attributes)
71
57
 
72
58
 
73
- def group_members(conn, search_base, group_name=None, group_dn=None):
74
- if group_name:
75
- group_dn = get_dn(conn, search_base, group_name)
59
+ def group_members(conn, search_base, group):
60
+ group_dn = get_dn(conn, search_base, group)
76
61
  if not group_dn:
77
- logging.error("group_name or group_dn must be passed and exist")
78
62
  return None
79
63
 
80
64
  search_filter = f"(distinguishedName={group_dn})"
81
65
  return search(conn, group_dn, search_filter, limit=1, attributes=["member"])
82
66
 
83
67
 
84
- def group_member(
85
- conn, search_base, group_name=None, group_dn=None, user_name=None, user_dn=None
86
- ):
87
- if group_name:
88
- group_dn = get_dn(conn, search_base, group_name)
68
+ def group_member(conn, search_base, group, user):
69
+
70
+ group_dn = get_dn(conn, search_base, group)
89
71
  if not group_dn:
90
- logging.error("group_name or group_dn must be passed and exist")
91
72
  return None
92
73
 
93
- if user_name:
94
- user_dn = get_dn(conn, search_base, user_name)
74
+ user_dn = get_dn(conn, search_base, user)
95
75
  if not user_dn:
96
- logging.error("user_name or user_dn must be passed and exist")
97
76
  return None
98
77
 
99
78
  search_filter = f"(&(memberOf:1.2.840.113556.1.4.1941:={group_dn})(objectCategory=person)(objectClass=user)(distinguishedName={user_dn}))"
msad/main.py ADDED
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # msad - Active Directory tool
4
+ # Copyright (C) 2025 - matteo.redaelli@gmail.com
5
+
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program. If not, see <https://www.gnu.org/licenses/>
18
+
19
+ import logging
20
+ import datetime
21
+ import os
22
+ import json
23
+ import ssl
24
+ import sys
25
+ import tomllib
26
+
27
+ import typer
28
+
29
+ import msad
30
+ import ldap3
31
+
32
+ from pathlib import Path
33
+
34
+
35
+ BANNER = """
36
+
37
+ """
38
+
39
+ logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
40
+ logging.info(BANNER)
41
+
42
+ def _get_domain_config(config: dict|None, domain: str|None):
43
+ if "defaults" not in config:
44
+ logging.error(f"Missing entry 'defaults' in config file. Bye!")
45
+ sys.exit(100)
46
+ if "domain" not in config["defaults"]:
47
+ logging.error("Missing entry 'domain' in section 'defaults' in config file. Bye!")
48
+ sys.exit(101)
49
+
50
+ default_domain = config["defaults"]["domain"]
51
+
52
+ if "domains" not in config:
53
+ logging.error("Missing section 'domains' in config file. Bye!")
54
+ sys.exit(102)
55
+
56
+ if default_domain not in config["domains"] :
57
+ logging.error(f"Missing section '{default_domain}' in section 'domains' in config file. Bye!")
58
+ sys.exit(103)
59
+
60
+ if domain and domain not in config["domains"] :
61
+ logging.error(f"Missing section '{domain}' in section 'domains' in config file. Bye!")
62
+ sys.exit(104)
63
+
64
+ # TODO: validateing domain sections
65
+ if not domain:
66
+ domain = default_domain
67
+
68
+ domain_config = config["domains"][domain]
69
+ for field in ["host", "port", "search_base", "use_ssl"]:
70
+ if field not in domain_config:
71
+ logging.error(f"Missing required field '{field}' in section 'domains.{domain}' in config file. Bye!")
72
+ sys.exit(105)
73
+ return domain_config
74
+
75
+ def _get_config(domain: str|None, config_file: str|None):
76
+ if not config_file:
77
+ home = Path.home()
78
+ config_file = home / ".msad.toml"
79
+
80
+ if not os.path.isfile(config_file):
81
+ logging.error(f"Missing file {config_file}. Bye!")
82
+ sys.exit(1)
83
+
84
+ if not os.access(config_file, os.R_OK):
85
+ logging.error(f"File {config_file} is not readable. Bye!")
86
+ sys.exit(2)
87
+
88
+ """Read config file"""
89
+ with open(config_file, "rb") as f:
90
+ data = tomllib.load(f)
91
+ return _get_domain_config(data, domain)
92
+
93
+ def _json_converter(o):
94
+ if isinstance(o, datetime.datetime):
95
+ return o.__str__()
96
+ elif isinstance(o, list):
97
+ return ";".join(o)
98
+ # else return o
99
+
100
+
101
+ def _get_connection_krb(host: str, port: int, use_ssl: bool):
102
+ tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2)
103
+ server = ldap3.Server(host, port=port, use_ssl=use_ssl, tls=tls)
104
+
105
+ conn = ldap3.Connection(
106
+ server,
107
+ authentication=ldap3.SASL,
108
+ sasl_mechanism=ldap3.KERBEROS,
109
+ auto_bind=False,
110
+ )
111
+ # conn.bind()
112
+ return conn
113
+
114
+
115
+ def _get_connection_user_pwd(host: str, port: int, use_ssl: bool, user: str, password: str):
116
+ server = ldap3.Server(host, port=port, use_ssl=use_ssl)
117
+
118
+ conn = ldap3.Connection(server, user=user, password=password, auto_bind=False)
119
+ # conn.bind()
120
+ return conn
121
+
122
+
123
+ def _get_connection(config: dict):
124
+ if "user" in config and "password" in config:
125
+ conn = _get_connection_user_pwd(config["host"],
126
+ config["port"],
127
+ config["use_ssl"],
128
+ config["user"],
129
+ config["password"])
130
+ else:
131
+ conn = _get_connection_krb(config["host"],
132
+ config["port"],
133
+ config["use_ssl"])
134
+ conn.bind()
135
+ return conn
136
+
137
+ def _pprint(ldapresult, out_format="json", sep="\t"):
138
+ if not ldapresult or out_format == "default":
139
+ return ldapresult
140
+ elif out_format == "json1":
141
+ return json.dumps(dict(ldapresult))
142
+ else:
143
+ result = ""
144
+ for obj in ldapresult:
145
+ if out_format == "json":
146
+ result = (
147
+ result + json.dumps(dict(obj), default=_json_converter) + "\n"
148
+ )
149
+ elif out_format == "csv":
150
+ sorted_obj = dict(sorted(obj.items()))
151
+ new_values = list(
152
+ map(
153
+ lambda v: "|".join(v) if isinstance(v, list) else v,
154
+ sorted_obj.values(),
155
+ )
156
+ )
157
+ result = result + sep.join(new_values) + "\n"
158
+ return result
159
+
160
+ # def users(self, user):
161
+ # """Find users inside AD. The
162
+ # filter can be the cn or userPrincipalName or samaccoutnname or mail to be searched. Can contain *
163
+ # """
164
+ # result = msad.users(
165
+ # self._conn, self._search_base, user, attributes=self._attributes
166
+ # )
167
+ # return self._pprint(result)
168
+
169
+ # def is_disabled(self, user):
170
+ # """Check if a user is disabled"""
171
+ # return msad.user.is_disabled(self._conn, self._search_base, user)
172
+
173
+ # def is_locked(self, user):
174
+ # """Check if the user is locked"""
175
+ # return msad.user.is_locked(self._conn, self._search_base, user)
176
+
177
+ # def password_changed_in_days(self, user):
178
+ # return msad.user.password_changed_in_days(self._conn, self._search_base, user)
179
+
180
+ # def has_expired_password(self, user, max_age):
181
+ # """Check is user has the expired password"""
182
+ # return msad.has_expired_password(self._conn, self._search_base, user, max_age)
183
+
184
+ # def has_never_expires_password(self, user):
185
+ # """Check if a user has never expires password"""
186
+ # return msad.has_never_expires_password(self._conn, self._search_base, user)
187
+
188
+ # def check_user(self, user, max_age, groups=[]):
189
+ # """Get some info about a user: is it locked? disabled? password expired?"""
190
+ # return msad.check_user(self._conn, self._search_base, user, max_age, groups)
191
+
192
+
193
+
194
+ # def user_groups(self, user_name=None, user_dn=None):
195
+ # """Extract the list of groups of a user (using DN or sAMAccountName)"""
196
+ # return msad.user.user_groups(
197
+ # self._conn, self._search_base, self._limit, user_name, user_dn
198
+ # )
199
+
200
+
201
+ # def group_member(
202
+ # self, group_name=None, group_dn=None, user_name=None, user_dn=None
203
+ # ):
204
+ # """Check if the user is a member of a group (using DN or sAMAccountName)"""
205
+ # return msad.group_member(
206
+ # conn=self._conn,
207
+ # search_base=self._search_base,
208
+ # group_name=group_name,
209
+ # group_dn=group_dn,
210
+ # user_name=user_name,
211
+ # user_dn=user_dn,
212
+ # )
213
+
214
+
215
+
216
+ app = typer.Typer()
217
+
218
+ @app.command()
219
+ def change_password(user: str,
220
+ domain: str|None = None,
221
+ config_file: str|None = None):
222
+ config = _get_config(config_file, domain)
223
+ conn = _get_connection(config)
224
+ return msad.user.change_password(
225
+ conn, config["search_base"], user)
226
+
227
+ @app.command()
228
+ def group_add_member(group: str,
229
+ user: str,
230
+ domain: str|None = None,
231
+ config_file: str|None = None):
232
+ """Adds the user to a group (using DN or sAMAccountName)"""
233
+
234
+ config = _get_config(config_file, domain)
235
+ conn = _get_connection(config)
236
+ result = msad.add_member(
237
+ conn=conn,
238
+ search_base=config["search_base"],
239
+ group=group,
240
+ user=user,
241
+ )
242
+ return result
243
+
244
+ @app.command()
245
+ def group_remove_member(group: str,
246
+ user: str,
247
+ domain: str|None = None,
248
+ config_file: str|None = None):
249
+ """Remove the user to a group (using DN or sAMAccountName)"""
250
+
251
+ config = _get_config(config_file, domain)
252
+ conn = _get_connection(config)
253
+ result = msad.remove_member(
254
+ conn=conn,
255
+ search_base=config["search_base"],
256
+ group=group,
257
+ user=user,
258
+ )
259
+ return result
260
+
261
+ @app.command()
262
+ def group_members(group: str,
263
+ nested: bool=False,
264
+ limit: int = 2000,
265
+ domain: str|None = None,
266
+ config_file: str|None = None,
267
+ out_format: str = "json",
268
+ attributes: list[str] = []):
269
+
270
+ config = _get_config(config_file, domain)
271
+ conn = _get_connection(config)
272
+ if nested:
273
+ result = msad.group_flat_members(
274
+ conn,
275
+ config["search_base"],
276
+ limit,
277
+ group,
278
+ attributes=attributes,
279
+ )
280
+ else:
281
+ """Extract the direct members of a group"""
282
+ result = msad.group_members(
283
+ conn,
284
+ config["search_base"],
285
+ group)
286
+ print(_pprint(result))
287
+
288
+ @app.command()
289
+ def search(filter: str,
290
+ limit: int = 2000,
291
+ domain: str|None = None,
292
+ config_file: str|None = None,
293
+ out_format: str = "json",
294
+ attributes: list[str] = []):
295
+ config = _get_config(config_file, domain)
296
+ conn = _get_connection(config)
297
+ result = msad.search(conn, config["search_base"], filter, limit=limit, attributes=attributes)
298
+ print(_pprint(result, out_format))
299
+
300
+ @app.command()
301
+ def get_sample_config():
302
+ output = """
303
+ [defaults]
304
+
305
+ domain = "mydomain"
306
+
307
+ [domains]
308
+
309
+ [domains.mydomain]
310
+
311
+ host = "example.com"
312
+ search_base = "dc=example,dc=com"
313
+
314
+ port = 636
315
+ use_ssl = true
316
+ #port = 389
317
+ #use_ssl = false
318
+
319
+ # user =
320
+ # password =
321
+ """
322
+ print(output)
323
+
324
+ @app.command()
325
+ def user_groups(user: str,
326
+ nested: bool=False,
327
+ limit: int = 2000,
328
+ domain: str|None = None,
329
+ config_file: str|None = None,
330
+ out_format: str = "json"):
331
+
332
+ config = _get_config(config_file, domain)
333
+ conn = _get_connection(config)
334
+
335
+ result = msad.user.user_groups(conn, config["search_base"], limit, user, nested=nested)
336
+ print(_pprint(result, out_format))
337
+
338
+ if __name__ == "__main__":
339
+ app()
msad/search.py CHANGED
@@ -53,11 +53,14 @@ def users(conn, search_base, string, limit, attributes=None):
53
53
  return search(conn, search_base, search_filter, limit=limit, attributes=attributes)
54
54
 
55
55
 
56
- def get_dn(conn, search_base, sAMAccountName):
57
- search_filter = f"(sAMAccountName={sAMAccountName})"
56
+ def get_dn(conn, search_base, entry):
57
+ if entry.lower().startswith("cn="):
58
+ return entry
59
+ search_filter = f"(sAMAccountName={entry})"
58
60
  result = search(conn, search_base, search_filter, attributes=["distinguishedName"])
59
61
  logging.debug(result)
60
62
  if len(result) < 1:
63
+ logging.error(f"entry {entry} not found")
61
64
  return None
62
65
 
63
66
  return result[0]["distinguishedName"]
msad/user.py CHANGED
@@ -20,7 +20,7 @@ import logging
20
20
  import getpass
21
21
  import ldap3
22
22
  import datetime
23
- from .search import *
23
+ from .search import get_dn, search
24
24
  from .group import *
25
25
 
26
26
 
@@ -34,9 +34,8 @@ def _enter_password(text):
34
34
  return p
35
35
 
36
36
 
37
- def change_password(conn, search_base, user_name=None, user_dn=None):
38
- if user_name:
39
- user_dn = get_dn(conn, search_base, user_name)
37
+ def change_password(conn, search_base, user):
38
+ user_dn = get_dn(conn, search_base, user)
40
39
 
41
40
  if not user_dn:
42
41
  return None
@@ -134,16 +133,21 @@ def check_user(conn, search_base, user, max_age, groups=[]):
134
133
  )
135
134
 
136
135
 
137
- def user_groups(conn, search_base, limit, user_name=None, user_dn=None):
136
+ def user_groups(conn, search_base, limit, user, nested=True):
138
137
  """retrieve all groups (also nested) of a user"""
139
138
 
140
- if user_name:
141
- user_dn = get_dn(conn, search_base, user_name)
139
+ user_dn = get_dn(conn, search_base, user)
142
140
 
143
141
  if not user_dn:
144
142
  return None
145
143
 
146
- search_filter = f"(member:1.2.840.113556.1.4.1941:={user_dn})"
144
+ if nested:
145
+ search_filter = f"(member:1.2.840.113556.1.4.1941:={user_dn})"
146
+ attributes = ["sAMaccountName"]
147
+ else:
148
+ search_filter = "(objectClass=*)"
149
+ search_base = user_dn
150
+ attributes = ["memberOf"]
147
151
  return search(
148
- conn, search_base, search_filter, limit=limit, attributes=["sAMaccountName"]
152
+ conn, search_base, search_filter, limit=limit, attributes=attributes
149
153
  )
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.4
2
+ Name: msad
3
+ Version: 0.4.0
4
+ Summary: msad is a commandline for interacting with Active Directory
5
+ Project-URL: Homepage, https://github.com/matteoredaelli/msad
6
+ Project-URL: Issues, https://github.com/matteoredaelli/msad/issues
7
+ Author-email: Matteo Redaelli <matteo.redaelli@gmail.com>
8
+ License-Expression: GPL-3.0-or-later
9
+ License-File: LICENSE
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.9
13
+ Requires-Dist: cryptography
14
+ Requires-Dist: gssapi
15
+ Requires-Dist: ldap3
16
+ Requires-Dist: typer
17
+ Description-Content-Type: text/markdown
18
+
19
+ # msAD
20
+
21
+ *msad* is a library and command line tool for working with an Active Directory / LDAP server from Unix, Linux and MacOs systems.
22
+
23
+ It supports authentication with user/pwd and kerberos
24
+
25
+ It supports paginations: it can retreive more than 2000 objects (a limit of AD)
26
+
27
+ It can be used for:
28
+ - search objects (users, groups, computers,..)
29
+ - search (recursively) group memberships and all user's groups
30
+ - add/remove members to/from AD groups using DN or sAMaccoutName
31
+ - change AD passwords
32
+ - check if a user is disabled or locked, group membership
33
+
34
+ ## Prerequisites
35
+
36
+ python >= 3.9
37
+
38
+ For kerboros auth
39
+
40
+ - krb5 lib and tools (like kinit, ...)
41
+ - a keytab file or
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pipx install msad
47
+ ```
48
+
49
+ ## COnfiguration
50
+
51
+ Create a configuration file in $HOME/.msad.toml as suggested by
52
+
53
+ ```bash
54
+ msad get-sample-config
55
+ ```
56
+
57
+
58
+ ## Usage
59
+
60
+
61
+ ```bash
62
+ msad --help
63
+
64
+ python -m msad --help
65
+
66
+ ```
67
+
68
+
69
+ For kerberos authentication, first you need to login to AD / get a ticket kerberos with
70
+
71
+ ```bash
72
+ kinit youraduser
73
+ ```
74
+
75
+
76
+
77
+ ```text
78
+ msad search "(samaccountname=matteo)" --out-format=json
79
+
80
+ msad search "(cn=redaelli*)" --attribute mail --attribute samaccountname --out-format=json
81
+
82
+ msad group-members qlik_analyzer_users --nested
83
+
84
+ msad group-add-member qlik_analyzer_users matteo
85
+
86
+ msad group-remove-member qlik_analyzer_users matteo
87
+
88
+ msad user-groups matteo --nested
89
+
90
+ ```
91
+
92
+ ## Sample
93
+
94
+
95
+
96
+ ## License
97
+
98
+ Copyright © 2021 - 2025 Matteo Redaelli
99
+
100
+ This program is free software: you can redistribute it and/or modify
101
+ it under the terms of the GNU General Public License as published by
102
+ the Free Software Foundation, either version 3 of the License, or
103
+ (at your option) any later version.
@@ -0,0 +1,12 @@
1
+ msad/__init__.py,sha256=DJ6egXlvzOK_k0XlN7BpmMRuwu4MM7eQoxLE0Ou-sLI,63
2
+ msad/__main__.py,sha256=RCZmmoCNOWC7rAfIDm_LaymsybXIzE6McYbUEEkf9P8,60
3
+ msad/ad.py,sha256=C3dknAgRY6Jnotk0RgPSye7uzNxUd-7B3oGloGR674E,5679
4
+ msad/group.py,sha256=3HNpXfYK4yXxDNsXn72JOZSMEwjj0kMiDvVBca1oVuI,2510
5
+ msad/main.py,sha256=5wBSPOZPkOnYfe-gow1YFt65FpawJooZ3eOKp_RFP0A,10573
6
+ msad/search.py,sha256=j8PedOZVf7eFBG34ZfHoVEfEPbYE0nOtznKTLwu48Eg,3476
7
+ msad/user.py,sha256=k9INOsuSekCB4w0EYYrcgPQChvOfawIzLYv7h37-DXM,4557
8
+ msad-0.4.0.dist-info/METADATA,sha256=KL37POSU35W_FjShUlAU8Qf48EVUpGwPkCbWMUmQ5T0,2288
9
+ msad-0.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
10
+ msad-0.4.0.dist-info/entry_points.txt,sha256=UKSjeppC0YX2dfybmBfxLXCqF_HMZFfPX1MRw88rtpI,39
11
+ msad-0.4.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
12
+ msad-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ msad = msad.main:app
msad/command_line.py DELETED
@@ -1,265 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- # msad - Active Directory tool
4
- # Copyright (C) 2020 - matteo.redaelli@gmail.com
5
-
6
- # This program is free software: you can redistribute it and/or modify
7
- # it under the terms of the GNU General Public License as published by
8
- # the Free Software Foundation, either version 3 of the License, or
9
- # (at your option) any later version.
10
-
11
- # This program is distributed in the hope that it will be useful,
12
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
- # GNU General Public License for more details.
15
-
16
- # You should have received a copy of the GNU General Public License
17
- # along with this program. If not, see <https://www.gnu.org/licenses/>
18
-
19
- import logging
20
- import os
21
- import datetime
22
- import sys
23
- import msad
24
- import fire
25
- import ldap3
26
- import ssl
27
- import json
28
- from typing import List, Tuple, Dict
29
- import pprint
30
-
31
-
32
- def _json_converter(o):
33
- if isinstance(o, datetime.datetime):
34
- return o.__str__()
35
- elif isinstance(o, list):
36
- return ";".join(o)
37
- # else return o
38
-
39
-
40
- def _get_connection_krb(host, port, use_ssl):
41
- tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2)
42
- server = ldap3.Server(host, port=port, use_ssl=use_ssl, tls=tls)
43
-
44
- conn = ldap3.Connection(
45
- server,
46
- authentication=ldap3.SASL,
47
- sasl_mechanism=ldap3.KERBEROS,
48
- auto_bind=False,
49
- )
50
- # conn.bind()
51
- return conn
52
-
53
-
54
- def _get_connection_user_pwd(host, port, use_ssl, user, password):
55
- server = ldap3.Server(host, port, use_ssl)
56
-
57
- conn = ldap3.Connection(server, user=user, password=password, auto_bind=False)
58
- # conn.bind()
59
- return conn
60
-
61
-
62
- def _get_connection(host, port, use_ssl, sso, user, password):
63
- if user and password:
64
- conn = _get_connection_user_pwd(host, port, use_ssl, user, password)
65
- else:
66
- conn = _get_connection_krb(host, port, use_ssl)
67
-
68
- conn.bind()
69
- return conn
70
-
71
-
72
- class AD:
73
- """*msad* is command line tool for Active Directory. With it you can
74
- search objects,
75
- add/remove members to/from groups,
76
- change password
77
- check if a user is locked, disabled
78
- check if a user's password is expired
79
- ..."""
80
-
81
- def __init__(
82
- self,
83
- host,
84
- port,
85
- use_ssl=True,
86
- sso=True,
87
- user=None,
88
- password=None,
89
- search_base=None,
90
- limit=0,
91
- attributes=None,
92
- out_format="default",
93
- sep=";",
94
- ):
95
- try:
96
- self._conn = _get_connection(host, port, use_ssl, sso, user, password)
97
- except:
98
- logging.error(
99
- f"Cannot login to Active Directory (host: {host}, port: {port}). Bye"
100
- )
101
- sys.exit(1)
102
- self._attributes = attributes
103
- self._sep = sep
104
- self._search_base = search_base
105
- self._limit = limit
106
- self._out_format = out_format
107
-
108
- def change_password(self, user_name=None, user_dn=None):
109
- return msad.user.change_password(
110
- self._conn, self._search_base, user_name, user_dn
111
- )
112
-
113
- def search(self, search_filter):
114
- self._conn.search(
115
- self._search_base,
116
- search_filter,
117
- size_limit=self._limit,
118
- attributes=self._attributes,
119
- )
120
- result = list(filter(lambda e: "attributes" in e, self._conn.response))
121
- result = list(map(lambda e: e["attributes"], result))
122
- return self._pprint(result)
123
-
124
- def _pprint(self, ldapresult):
125
- if not ldapresult or self._out_format == "default":
126
- return ldapresult
127
- elif self._out_format == "json1":
128
- return json.dumps(dict(ldapresult))
129
- else:
130
- result = ""
131
- for obj in ldapresult:
132
- if self._out_format == "json":
133
- result = (
134
- result + json.dumps(dict(obj), default=_json_converter) + "\n"
135
- )
136
- elif self._out_format == "csv":
137
- sorted_obj = dict(sorted(obj.items()))
138
- new_values = list(
139
- map(
140
- lambda v: "|".join(v) if isinstance(v, list) else v,
141
- sorted_obj.values(),
142
- )
143
- )
144
- result = result + self._sep.join(new_values) + "\n"
145
- return result
146
-
147
- def users(self, user):
148
- """Find users inside AD. The
149
- filter can be the cn or userPrincipalName or samaccoutnname or mail to be searched. Can contain *
150
- """
151
- result = msad.users(
152
- self._conn, self._search_base, user, attributes=self._attributes
153
- )
154
- return self._pprint(result)
155
-
156
- def is_disabled(self, user):
157
- """Check if a user is disabled"""
158
- return msad.user.is_disabled(self._conn, self._search_base, user)
159
-
160
- def is_locked(self, user):
161
- """Check if the user is locked"""
162
- return msad.user.is_locked(self._conn, self._search_base, user)
163
-
164
- def password_changed_in_days(self, user):
165
- return msad.user.password_changed_in_days(self._conn, self._search_base, user)
166
-
167
- def has_expired_password(self, user, max_age):
168
- """Check is user has the expired password"""
169
- return msad.has_expired_password(self._conn, self._search_base, user, max_age)
170
-
171
- def has_never_expires_password(self, user):
172
- """Check if a user has never expires password"""
173
- return msad.has_never_expires_password(self._conn, self._search_base, user)
174
-
175
- def check_user(self, user, max_age, groups=[]):
176
- """Get some info about a user: is it locked? disabled? password expired?"""
177
- return msad.check_user(self._conn, self._search_base, user, max_age, groups)
178
-
179
- def group_flat_members(self, group_name=None, group_dn=None):
180
- """Extract all the (nested) members of a group"""
181
- result = msad.group_flat_members(
182
- self._conn,
183
- self._search_base,
184
- self._limit,
185
- group_name,
186
- group_dn,
187
- attributes=self._attributes,
188
- )
189
- return self._pprint(result)
190
-
191
- def group_members(self, group_name=None, group_dn=None):
192
- """Extract the direct members of a group"""
193
- if group_name is None and group_dn is None:
194
- logging.error("group_name or group_dn must be entered")
195
- return None
196
- result = msad.group_members(self._conn, self._search_base, group_name, group_dn)
197
- return self._pprint(result)
198
-
199
- def add_member(self, group_name=None, group_dn=None, user_name=None, user_dn=None):
200
- """Adds the user to a group (using DN or sAMAccountName)"""
201
- return msad.add_member(
202
- conn=self._conn,
203
- search_base=self._search_base,
204
- group_name=group_name,
205
- group_dn=group_dn,
206
- user_name=user_name,
207
- user_dn=user_dn,
208
- )
209
-
210
- def user_groups(self, user_name=None, user_dn=None):
211
- """Extract the list of groups of a user (using DN or sAMAccountName)"""
212
- return msad.user.user_groups(
213
- self._conn, self._search_base, self._limit, user_name, user_dn
214
- )
215
-
216
- def remove_member(
217
- self, group_name=None, group_dn=None, user_name=None, user_dn=None
218
- ):
219
- """Remove the user from a group (using DN or sAMAccountName)"""
220
- return msad.remove_member(
221
- conn=self._conn,
222
- search_base=self._search_base,
223
- group_name=group_name,
224
- group_dn=group_dn,
225
- user_name=user_name,
226
- user_dn=user_dn,
227
- )
228
-
229
- def group_member(
230
- self, group_name=None, group_dn=None, user_name=None, user_dn=None
231
- ):
232
- """Check if the user is a member of a group (using DN or sAMAccountName)"""
233
- return msad.group_member(
234
- conn=self._conn,
235
- search_base=self._search_base,
236
- group_name=group_name,
237
- group_dn=group_dn,
238
- user_name=user_name,
239
- user_dn=user_dn,
240
- )
241
-
242
-
243
- BANNER = """
244
- __ __ ____ _ ____
245
- | \/ |/ ___| / \ | _ \
246
- | |\/| |\___ \ / _ \ | | | |
247
- | | | | ___) |/ ___ \ | |_| |
248
- |_| |_||____//_/ \_\|____/
249
-
250
- https://github.com/matteoredaelli/msad
251
-
252
- https://pypi.org/project/msad/
253
-
254
- """
255
-
256
-
257
- def main():
258
- """main"""
259
- logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
260
- logging.info(BANNER)
261
- fire.Fire(AD)
262
-
263
-
264
- if __name__ == "__main__":
265
- main()
@@ -1,118 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: msad
3
- Version: 0.3.5
4
- Summary: msad is a commandline for interacting with Active Directory
5
- Project-URL: Homepage, https://github.com/matteoredaelli/msad
6
- Project-URL: Issues, https://github.com/matteoredaelli/msad/issues
7
- Author-email: Matteo Redaelli <matteo.redaelli@gmail.com>
8
- License-Expression: GPL-3.0-or-later
9
- License-File: LICENSE
10
- Classifier: Operating System :: OS Independent
11
- Classifier: Programming Language :: Python :: 3
12
- Requires-Python: >=3.9
13
- Requires-Dist: cryptography
14
- Requires-Dist: fire
15
- Requires-Dist: gssapi
16
- Requires-Dist: ldap3
17
- Description-Content-Type: text/markdown
18
-
19
- # msAD
20
-
21
-
22
- msad is a library and command line tool for working with an Active Directory / LDAP server. It can be used for:
23
- - search objects (users, groups, computers,..)
24
- - search group members
25
- - add/remove members to/from AD groups using DN or sAMaccoutName
26
- - change AD passwords
27
- - check if a user is disabled or locked, group membership
28
-
29
-
30
- ## Usage
31
-
32
- ```bash
33
- msad --help
34
- ```
35
-
36
- ```text
37
- COMMAND is one of the following:
38
-
39
- add_member
40
- Adds the user to a group (using DN or sAMAccountName)
41
-
42
- change_password
43
-
44
- check_user
45
- Get some info about a user: is it locked? disabled? password expired?
46
-
47
- group_flat_members
48
- Extract all the (nested) members of a group
49
-
50
- group_member
51
- Check if the user is a member of a group (using DN or sAMAccountName)
52
-
53
- group_members
54
- Extract the direct members of a group
55
-
56
- has_expired_password
57
- Check is user has the expired password
58
-
59
- has_never_expires_password
60
- Check if a user has never expires password
61
-
62
- is_disabled
63
- Check if a user is disabled
64
-
65
- is_locked
66
- Check if the user is locked
67
-
68
- remove_member
69
- Remove the user from a group (using DN or sAMAccountName)
70
-
71
- search
72
-
73
- user_groups
74
- Extract the list of groups of a user (using DN or sAMAccountName)
75
-
76
- users
77
- Find users inside AD. The filter can be the cn or userPrincipalName or samaccoutnname or mail to be searched. Can contain *
78
-
79
- ```
80
-
81
- ## Sample
82
-
83
- I find useful to add an alias in my ~/.bash_aliases
84
-
85
- ```bash
86
- alias msad='/usr/local/bin/msad --host=dmc1it.group.redaelli.org --port=636 --search_base dc=group,dc=redaelli,dc=org'
87
- ```
88
-
89
- Retreive info about a user
90
-
91
- ```bash
92
- msad check_user matteo 90 \[qliksense_analyzer,qliksense_professional\] 2>/dev/null
93
- ```
94
-
95
- ```json
96
- {"is_disabled": false}
97
- {"is_locked": false}
98
- {"has_never_expires_password": false}
99
- {"has_expired_password": false}
100
- {"membership_qliksense_analyzer": false}
101
- {"membership_qliksense_professional": true}
102
- ```
103
-
104
- Getting nested group members (it is a pages search, it can retreive more than 1000 users)
105
-
106
- ```bash
107
- msad --out_format csv --attributes samaccountname,mail,sn,givenName group_flat_members "dc=group,dc=redaelli,dc=org" --group_name "qliksense_admin"
108
- ```
109
-
110
-
111
- ## License
112
-
113
- Copyright © 2021 2022 Matteo Redaelli
114
-
115
- This program is free software: you can redistribute it and/or modify
116
- it under the terms of the GNU General Public License as published by
117
- the Free Software Foundation, either version 3 of the License, or
118
- (at your option) any later version.
@@ -1,11 +0,0 @@
1
- msad/__init__.py,sha256=DJ6egXlvzOK_k0XlN7BpmMRuwu4MM7eQoxLE0Ou-sLI,63
2
- msad/ad.py,sha256=C3dknAgRY6Jnotk0RgPSye7uzNxUd-7B3oGloGR674E,5679
3
- msad/command_line.py,sha256=6JB_rcYsd7UKVgUWmIdMJxMJ5oKmE_KshX7WYe5b5P0,8500
4
- msad/group.py,sha256=TKIDEzQEBugux2imt81YX1byRtz31W_qZRXADfoHSbU,3448
5
- msad/search.py,sha256=sQfvaaWX6yMKNk5nCPH6yQb2_54LjaEJuXc8FGOhrIo,3383
6
- msad/user.py,sha256=oWpoAHcgTMsFf3RXr0wSECONZkJfsIXC3EZTx16BqiY,4466
7
- msad-0.3.5.dist-info/METADATA,sha256=1NboA4x0TsN2drxFZjyQpBRT-Y7lyQoJrM37dtN0-tg,3135
8
- msad-0.3.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
9
- msad-0.3.5.dist-info/entry_points.txt,sha256=s9wIVNQKKBX5Y6KaYj0STVIZ66mBsseHaamXjshIw7Y,48
10
- msad-0.3.5.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
11
- msad-0.3.5.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- msad = msad.command_line:main
File without changes