diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83e8f15 --- /dev/null +++ b/.gitignore @@ -0,0 +1,158 @@ +# Data +data/ + +# Tmp +tmp/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/autocomplete/__init__.py b/autocomplete/__init__.py new file mode 100644 index 0000000..b1cf1ab --- /dev/null +++ b/autocomplete/__init__.py @@ -0,0 +1,18 @@ +from .mail import AutocompleteMail +from .name import AutocompleteName +from .password import AutocompletePassword +from .posixuser import AutocompletePosixUser + +from .thu import AutocompleteTHU + + +plugins = [ + # first run + AutocompleteTHU, + + # normal + AutocompleteMail, + AutocompleteName, + AutocompletePassword, + AutocompletePosixUser +] diff --git a/autocomplete/mail.py b/autocomplete/mail.py new file mode 100644 index 0000000..8fc989e --- /dev/null +++ b/autocomplete/mail.py @@ -0,0 +1,13 @@ +class AutocompleteMail: + def __init__(self, conf): + self.config = { + "base": "@company.local" + } + + self.config.update(conf) + + def run(self, fields): + autocomplete = { + "mail": fields.get("uid") + self.config["base"], + } + return {**autocomplete, **fields} diff --git a/autocomplete/name.py b/autocomplete/name.py new file mode 100644 index 0000000..ef07c1e --- /dev/null +++ b/autocomplete/name.py @@ -0,0 +1,12 @@ +class AutocompleteName: + def __init__(self, conf): + self.config = {} + + self.config.update(conf) + + def run(self, fields): + autocomplete = { + "sn": fields.get("cn")[0], + "givenName": fields.get("cn")[1:] + } + return {**autocomplete, **fields} diff --git a/autocomplete/password.py b/autocomplete/password.py new file mode 100644 index 0000000..742504b --- /dev/null +++ b/autocomplete/password.py @@ -0,0 +1,19 @@ +import imp +import secrets +import string + + +class AutocompletePassword: + def __init__(self, conf): + self.config = { + "charset": string.ascii_lowercase + string.digits, + "length": 12 + } + + self.config.update(conf) + + def run(self, fields): + autocomplete = { + "userPassword": "".join(secrets.choice(self.config["charset"]) for _ in range(self.config["length"])) + } + return {**autocomplete, **fields} diff --git a/autocomplete/posixuser.py b/autocomplete/posixuser.py new file mode 100644 index 0000000..2bca155 --- /dev/null +++ b/autocomplete/posixuser.py @@ -0,0 +1,19 @@ +import random + + +class AutocompletePosixUser: + def __init__(self, conf): + self.config = { + "uidRange": [10001, 2147483647], + "gidNumber": 10000 + } + + self.config.update(conf) + + def run(self, fields): + autocomplete = { + "homeDirectory": "/home/" + fields.get("uid", "default"), + "uidNumber": random.randrange(*self.config["uidRange"]), + "gidNumber": self.config["gidNumber"] + } + return {**autocomplete, **fields} diff --git a/autocomplete/thu.py b/autocomplete/thu.py new file mode 100644 index 0000000..f38283e --- /dev/null +++ b/autocomplete/thu.py @@ -0,0 +1,83 @@ +import os +import logging +import csv +import time + +import pymssql +import pandas as pd + + +class AutocompleteTHU: + def __init__(self, conf): + self.config = { + "db_server": "", + "db_user": "", + "db_password": "", + "db_name": "", + + "csv_file": os.path.join(os.path.dirname(os.path.realpath(__file__)), "tmp/thu.csv"), + "csv_expire": 86400 # seconds + } + + self.config.update(conf) + + # db is loaded + self.db = None + + def sync_db(self): + logging.info(__name__, "Sync database...") + + # connect + conn = pymssql.connect( + server=self.config["db_server"], + user=self.config["db_user"], + password=self.config["db_password"], + database=self.config["db_name"]) + + # query + cursor = conn.cursor() + cursor.execute("SELECT * FROM ReaderInfo") + + # export csv + with open(self.config["csv_file"], "w") as f: + writer = csv.writer(f, quoting=csv.QUOTE_ALL) + writer.writerow(col[0] for col in cursor.description) + for row in cursor: + writer.writerow(row) + f.close() + + # close + cursor.close() + conn.close() + + def load_db(self): + if self.db is not None: + return + + # sync if csv expired + if (not os.path.isfile(self.config["csv_file"])) \ + or (time.time() - os.path.getmtime(self.config["csv_file"]) >= self.config["csv_expire"]): + + self.sync_db() + + # load db + self.db = pd.read_csv(self.config["csv_file"]) + + def run(self, fields): + autocomplete = {} + # get index + uniid = fields.get("employeeNumber") + if uniid: + # fill fields + self.load_db() + rows = self.db[self.db["uniid"] == uniid] + + if len(rows): + item = rows.iloc[0].to_dict() + autocomplete = { + "uid": item["yhm"], + "cn": item["username"], + "employeeType": item["deptno_name"] + } + + return {**autocomplete, **fields} diff --git a/batch_add.py b/batch_add.py new file mode 100644 index 0000000..25a7741 --- /dev/null +++ b/batch_add.py @@ -0,0 +1,74 @@ +import json +import os +import glob +import logging + +import ldap3 +import pandas + + +class LDAPBatchAdd: + def __init__(self, conf): + # initialize config + self.config = { + "server": "", + "userdn": "", + "userpassword": "", + "template_dn": "", + } + self.config.update(conf) + + # connect server + self.server = ldap3.Server(self.config["server"]) + self.conn = ldap3.Connection( + self.server, + user=self.config["userdn"], + password=self.config["userpassword"], + auto_bind=True) + + # get template + assert self.conn.search( + search_base=self.config["template_dn"], + search_filter="(objectClass=*)", + search_scope=ldap3.BASE, + attributes=ldap3.ALL_ATTRIBUTES), "Template user not found" + self.template = self.conn.entries[0] + + def add_user(self, fields): + # calculate dn (replace first fields) + template_dn = self.template.entry_dn.split(",") + k, v = template_dn[0].split("=") + new_dn = ",".join([k + "=" + fields.get(k, v)] + template_dn[1:]) + + # add + print(fields) + return self.conn.add(new_dn, self.template.objectClass.value, fields) + + +def main(): + # load config + script_dir = os.path.dirname(os.path.realpath(__file__)) + with open(os.path.join(script_dir, "data/config.json"), "r") as f: + conf = json.load(f) + f.close() + + batch_add = LDAPBatchAdd(conf) + + # load autocomplete + import autocomplete + plugins = [plugin(conf.get(plugin.__name__, {})) for plugin in autocomplete.plugins] + + # import all files + for csv_filename in glob.glob(os.path.join(script_dir, "data/import/*.csv")): + df = pandas.read_csv(csv_filename) + for idx, row in df.iterrows(): + fields = row.to_dict() + # autocomplete + for plugin in plugins: + fields = plugin.run(fields) + # add + batch_add.add_user(fields) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5407fdc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +ldap3 +pandas \ No newline at end of file diff --git a/ticket/__init__.py b/ticket/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ticket/__init__.py