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 +4 -0
 - msad/group.py +15 -36
 - msad/main.py +339 -0
 - msad/search.py +5 -2
 - msad/user.py +13 -9
 - msad-0.4.0.dist-info/METADATA +103 -0
 - msad-0.4.0.dist-info/RECORD +12 -0
 - msad-0.4.0.dist-info/entry_points.txt +2 -0
 - msad/command_line.py +0 -265
 - msad-0.3.5.dist-info/METADATA +0 -118
 - msad-0.3.5.dist-info/RECORD +0 -11
 - msad-0.3.5.dist-info/entry_points.txt +0 -2
 - {msad-0.3.5.dist-info → msad-0.4.0.dist-info}/WHEEL +0 -0
 - {msad-0.3.5.dist-info → msad-0.4.0.dist-info}/licenses/LICENSE +0 -0
 
    
        msad/__main__.py
    ADDED
    
    
    
        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 
     | 
    
         
            -
                 
     | 
| 
       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 
     | 
    
         
            -
                 
     | 
| 
       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 
     | 
    
         
            -
                 
     | 
| 
       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,  
     | 
| 
      
 48 
     | 
    
         
            +
                conn, search_base, limit, group, attributes=None
         
     | 
| 
       62 
49 
     | 
    
         
             
            ):
         
     | 
| 
       63 
     | 
    
         
            -
                 
     | 
| 
       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,  
     | 
| 
       74 
     | 
    
         
            -
                 
     | 
| 
       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 
     | 
    
         
            -
             
     | 
| 
       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 
     | 
    
         
            -
                 
     | 
| 
       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,  
     | 
| 
       57 
     | 
    
         
            -
                 
     | 
| 
      
 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,  
     | 
| 
       38 
     | 
    
         
            -
                 
     | 
| 
       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,  
     | 
| 
      
 136 
     | 
    
         
            +
            def user_groups(conn, search_base, limit, user, nested=True):
         
     | 
| 
       138 
137 
     | 
    
         
             
                """retrieve all groups (also nested) of a user"""
         
     | 
| 
       139 
138 
     | 
    
         | 
| 
       140 
     | 
    
         
            -
                 
     | 
| 
       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 
     | 
    
         
            -
                 
     | 
| 
      
 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= 
     | 
| 
      
 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,,
         
     | 
    
        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()
         
     | 
    
        msad-0.3.5.dist-info/METADATA
    DELETED
    
    | 
         @@ -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.
         
     | 
    
        msad-0.3.5.dist-info/RECORD
    DELETED
    
    | 
         @@ -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,,
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     |