diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..eed4137 --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +select = C,E,F,W,S +ignore = E203,W503 +per-file-ignores = tests/*:S101 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aeb398a --- /dev/null +++ b/.gitignore @@ -0,0 +1,68 @@ +# ---> Python (custom) +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +.venv/ +.env/ +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# 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/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Mac Stuff +.DS_Store + +# Editors +.vscode + diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5540cce --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.4.0 + hooks: + - id: check-added-large-files + - id: check-byte-order-marker + - id: check-json + - id: check-merge-conflict + - id: check-toml + - id: debug-statements + - id: detect-private-key +- repo: local + hooks: + - id: isort-project + name: isort_project + entry: isort -rc elab_users + language: system + pass_filenames: false + - id: isort-test + name: isort_test + entry: isort -rc tests + language: system + pass_filenames: false + - id: black + name: black + entry: black elab_users tests + language: system + pass_filenames: false + - id: flake8 + name: flake8 + entry: flake8 elab_users tests + language: system + pass_filenames: false + - id: pytest + name: pytest + entry: pytest tests + pass_filenames: false + language: system diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..2fd3f54 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,4 @@ +0.0.1 - first version +---------------------- + + - setting up the project diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..30b93d5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,117 @@ +Contributing +============ + +Contributions are welcome, and they are greatly appreciated! Every little bit +helps, and credit will always be given. + +You can contribute in many ways: + +Types of Contributions +---------------------- + +### Report Bugs + +Report bugs at https://git.cpi.imtek.uni-freiburg.de/CPI/elab-users.git/issues. + +If you are reporting a bug, please include: + +* Your operating system name and version. +* Any details about your local setup that might be helpful in troubleshooting. +* Detailed steps to reproduce the bug. + +### Fix Bugs + +Look through the GitHub issues for bugs. Anything tagged with "bug" and "help +wanted" is open to whoever wants to implement it. + +### Implement Features + +Look through the GitHub issues for features. Anything tagged with "enhancement" +and "help wanted" is open to whoever wants to implement it. + +### Write Documentation + +Elab Users could always use more documentation, whether as part of the +official Elab Users docs, in docstrings, or even on the web in blog posts, +articles, and such. + +### Submit Feedback + +The best way to send feedback is to file an issue at https://git.cpi.imtek.uni-freiburg.de/CPI/elab-users.git/issues. + +If you are proposing a feature: + +* Explain in detail how it would work. +* Keep the scope as narrow as possible, to make it easier to implement. +* Remember that this is a volunteer-driven project, and that contributions + are welcome :) + +Get Started! +------------ + +Ready to contribute? Here's how to set up `elab_users` for local development. + +1. Fork the `elab_users` repo on GitHub. +2. Clone your fork locally:: + + `$ git clone git@github.com:your_name_here/elab_users.git` + +3. Install your local copy into a virtualenv. + + `$ cd elab_users/` + `$ make devenv` + +4. Create a branch for local development:: + + `$ git checkout -b name-of-your-bugfix-or-feature` + + Now you can make your changes locally. + +5. When you're done making changes, check that your changes passes the linters and the + tests, including testing other Python versions with tox:: + + ``` + $ make lint + $ make coverage + $ make tox + ``` + +6. Commit your changes and push your branch to GitHub:: + + ``` + $ git add . + $ git commit -m "Your detailed description of your changes." + $ git push origin name-of-your-bugfix-or-feature + ``` + +7. Submit a pull request through the GitHub website. + +Pull Request Guidelines +----------------------- + +Before you submit a pull request, check that it meets these guidelines: + +1. The pull request should include tests. +2. If the pull request adds functionality, the docs should be updated. Put + your new functionality into a function with a docstring, and add the + feature to the list in README.md and CHANGES.md + +Tips +---- + +To run a quick set of tests without coverage report + + $ make test + +Deploying +--------- + +A reminder for the maintainers on how to deploy. +Bump the version in `elab_users/__init__.py` and +make sure all your changes are committed (including an entry in CHANGES.md). + + $ git tag + $ git push + $ git push --tags + $ flit publish + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a3dfb3e --- /dev/null +++ b/LICENSE @@ -0,0 +1,10 @@ +/* +* ---------------------------------------------------------------------------- +* "THE BEER-WARE LICENSE" (Revision 42): +* frey@imtek.de wrote this file. As long as you retain this notice you +* can do whatever you want with this stuff. If we meet some day, and you think +* this stuff is worth it, you can buy me a beer in return. Holger Frey +* ---------------------------------------------------------------------------- +*/ + + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c21cadb --- /dev/null +++ b/Makefile @@ -0,0 +1,90 @@ +.PHONY: clean clean-test clean-pyc clean-build docs help +.DEFAULT_GOAL := help + +define BROWSER_PYSCRIPT +import os, webbrowser, sys + +try: + from urllib import pathname2url +except: + from urllib.request import pathname2url + +webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) +endef +export BROWSER_PYSCRIPT + +define PRINT_HELP_PYSCRIPT +import re, sys + +for line in sys.stdin: + match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) + if match: + target, help = match.groups() + print("%-20s %s" % (target, help)) +endef +export PRINT_HELP_PYSCRIPT + +BROWSER := python -c "$$BROWSER_PYSCRIPT" + +help: + @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + +clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts + +clean-build: ## remove build artifacts + rm -fr build/ + rm -fr dist/ + rm -fr .eggs/ + find . -name '*.egg-info' -exec rm -fr {} + + find . -name '*.egg' -exec rm -f {} + + +clean-pyc: ## remove Python file artifacts + find . -name '*.pyc' -exec rm -f {} + + find . -name '*.pyo' -exec rm -f {} + + find . -name '*~' -exec rm -f {} + + find . -name '__pycache__' -exec rm -fr {} + + +clean-test: ## remove test and coverage artifacts + rm -fr .pytest_cache/ + rm -fr .tox/ + rm -f .coverage + rm -fr htmlcov/ + +lint: ## reformat with black and check style with flake8 + isort elab_users + isort tests + black elab_users tests + flake8 elab_users tests + +test: lint ## run tests quickly with the default Python + pytest tests -x --disable-warnings -m "not fun" + +testall: lint ## run tests quickly with the default Python + pytest tests + +coverage: lint ## full test suite, check code coverage and open coverage report + pytest tests --cov=elab_users -m "fun" + coverage html + $(BROWSER) htmlcov/index.html + +tox: ## run fully isolated tests with tox + tox + +install: ## install updated project.toml with flint + flit install --pth-file + +devenv: ## setup development environment + python3 -m venv --prompt elab_users .venv + .venv/bin/pip3 install --upgrade pip + .venv/bin/pip3 install flit + .venv/bin/flit install --pth-file + +repo: devenv ## complete project setup with development environment and git repo + git init . + git branch -m main + git add . + git commit -m "import of project template" + git remote add origin https://git.cpi.imtek.uni-freiburg.de/CPI/elab-users.git + git push -u origin main --no-verify + + .venv/bin/pre-commit install --install-hooks diff --git a/README.md b/README.md index c4e0497..4d12578 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,7 @@ First of all, we talk about the following files and folders: .htpasswd README authz - cpi - elab-users.py - old-scripts-backup + JaneDoe ** WARNING: ** In this list, there is one hidden files: `.htpasswd`. This file is hidden on @@ -25,70 +23,62 @@ purpose, so ** don't mess with it **. `authz`: defines the access controll list, so who has access to what -`cpi`: folder that holds the svn repository itself +`JaneDoe`: folder that holds one svn repository for a user -`elab-users.py`: usermanagement script -`old-scripts-backup`: contains the old scripts to add a user for backup reasons - - -Usermanagement with `elab-users.py` +Usermanagement with `elab-users` ----------------------------------- -** HINT: ** To run this scipt first change to the directory with -`cd /var/www/svn` and prepend every command with `./` -(e.g. `./elab-users.py --help`). +** HINT: ** To run this scipt if the service was deployed to dokku, use this +command structure `dokku run svn elab-users [command] [name]` + -The script `elab-users.py` provides some options to add and delete users, +The script `elab-users` provides some options to add and delete users, show access information from users and their elab journals. If the scprit is called with the `--help` option, the folowing help message is displayed: - Usage: elab-users.py [option] name + usage: elab-users [-h] [command] [name] - shows and manipulates svn access rights + positional arguments: + command one of the commands: [user, group, add, restricted, retire, password] + name user or group to perform the command on - Options: - -h, --help show this help message and exit - -g, --groupinfo display users in a group - -a, --add add a regular user - -r, --restricted add a restricted user - -m, --move move a user to alumni - -p, --password reset a user password + optional arguments: + -h, --help show this help message and exit - to grant a restricted user access to another folder, you have to carefully - edit the authz file +To grant a restricted user access to another folder, you have to carefully +edit the authz file -the following combinations are possible: +The following combinations are possible: -* `elab-users.py`: will show a list of all groups and their users -* `elab-users.py UserName`: shows the access rights of the user and their labjournal -* `elab-users.py -g GroupName`: shows a list of all group members -* `elab-users.py -a UserName`: adds a regular user, creates svn folders and sets a random password -* `elab-users.py -a UserName`: adds a restricted user, creates svn folders and sets a random password -* `elab-users.py -m UserName`: moves an existing user to the alumni group, removes his password -* `elab-users.py -p UserName`: resets the password for an existing user to a new random one +* `elab-users`: will show a list of all groups and their users +* `elab-users UserName`: shows the access rights of the user and their lab journal +* `elab-users group GroupName`: shows a list of all group members +* `elab-users add UserName`: adds a regular user, creates svn folders and sets a random password +* `elab-users restricted UserName`: adds a restricted user, creates svn folders and sets a random password +* `elab-users retire UserName`: moves an existing user to the alumni group, removes his password +* `elab-users password UserName`: resets the password for an existing user to a new random one Grant read writes to restricted users ------------------------------------- -As noted in the help message of `elab-users.py`, if a restriced user should have -read access to another labjournal, the `authz` file has to be edited manually. -Here are two examples that grant the user 'UrmilShah' read access to -two different lab journals: +As noted before, if a restriced user should have read access to another +lab journal, the `authz` file has to be edited manually. Here are two examples +that grant the user 'JaneDoe' read access to two different lab journals: ... snip ... - [cpi:/AndreasEvers] + [JohnSmith:/] @restricted = - UrmilShah = r + JaneDoe = r ... snip ... - [cpi:/HolgerFrey] + [MaxMustermann:/] @restricted = - UrmilShah = r - HolgerFrey = rw + JaneDoe = r + MaxMustermann = rw ... snip ... diff --git a/authz b/authz deleted file mode 100644 index 616cdb7..0000000 --- a/authz +++ /dev/null @@ -1,848 +0,0 @@ -[groups] -administrators = JuergenRuehe, OswaldPrucker -alumni = AlexeyKopyshev, AndreasBoenisch, AndreasEver, AnkeWoerz, AnneLoesche, ArthurMartens, ArulGeetha, CamillaOestevold, CanerKaganaslan, ChristianSchuh, ChristineBunte, CkPandiyarajan, CleoStannard, FanWu, GerhardBaaken, GinoRodriguez, GuillermoBenites, HeikeHaller, IrenaEipert, JacobBelardi, JenniferPfau, JoachimLauterwasser, JohannesBaader, KatrinMoosmann, KeLi, KerstinSchuh, KimberlySimancas, MarcoArmbruster, MariaVoehringer, MariaVohringer, MartinRendl, MartinVellinger, MartinaAuerswald, MatthiasLischka, MessRechner, MichaelaFrase, MiriamScheckenbach, MonicaPerez, NinoLomadze, Nongluck, OliverDornfeld, PeterZahn, PhilippDiefenthaler, PhilippWollermann, RebeccaBlell, RodrigoNavarro, SaraFuchs, SebastianBoehmer, SebastianSebald, SimonBodendorfer, SimonSchuster, SirasaYodmongkol, ThidaratWangkam, TobiasHeitzler, TobiasKoenig, TristanBourrel, UlrikeRiehle, ViVek, VinicioCarias, WolfgangEhm, YnSekhar, ZouStaarter -users = AlexanderDietz, AliciaMalekLuz, AndreasMader, AnnaSchuler, AnneBuderer, ChristophScheibelein, CrispinAmiriNaini, DanielaMoessner, DavidBoschert, DavidSchwaerzle, EstherRiga, FrankScherag, FranziskaDorner, GregorOsterwinter, HeidiPerez, HolgerFrey, JanNiklasSchoenberg, JonGreen, KarenLienkamp, KatyaSergeeva, LauraHerrera, MalwinaPajestka, MaraFlorea, MarcZinggeler, MarcelHoffmann, MarcelRothfelder, MartinKoerner, MartinSchoenstein, MatthiasMenzel, MelanieEichhorn, MichaelHenze, MonikaKurowska, MostafaMahmoud, NataliaSchatz, NicoleBirsner, NilsKorf, PengZou, PetraHettich, PhilipKotrade, RaduCristianMutihac, RomanErath, SamarKazan, SaschaEngel, SebastianBonaus, ShararehSahneh, SureshReddyBanda, ThananthornKanokwijitsilp, ThomasBrandstetter, TianyangZheng, VanessaWeiss, VitaliyKondrashov, WibkeHartleb, XiaoqiangHou, ZhuolingDeng -restricted = BeniPrasser, JuliaSaar, SimonZunker, UrmilShah, YongZhou - -[cpi:/] -@administrators= r -@users = r -@restricted = -@alumni = - -[AlexanderDietz:/] -@administrators= rw -@users = r -@restricted = -@alumni = -AlexanderDietz= r - -[AlexeyKopyshev:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[AliciaMalekLuz:/] -@administrators= rw -@users = r -@restricted = -@alumni = -AliciaMalekLuz= r - -[AndreasBoenisch:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[AndreasEver:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[AndreasEvers:/] -@administrators= rw -@users = r -@restricted = -@alumni = -UrmilShah = r - -[AndreasMader:/] -@administrators= rw -@users = r -@restricted = -@alumni = -AndreasMader= r - -[AnkeWoerz:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[AnnaSchuler:/] -@administrators= rw -@users = r -@restricted = -@alumni = -AnnaSchuler= r - -[AnneBuderer:/] -@administrators= rw -@users = r -@restricted = -@alumni = -AnneBuderer= r - -[AnneLoesche:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[AnselmHoppmann:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[ArthurMartens:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[ArulGeetha:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[BeniPrasser:/] -@administrators= rw -@users = r -@restricted = -@alumni = -BeniPrasser= r - -[CamillaOestevold:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[CanerKaganaslan:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[ChristianSchuh:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[ChristineBunte:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[ChristophScheibelein:/] -@administrators= rw -@users = r -@restricted = -@alumni = -ChristophScheibelein= r - -[CkPandiyarajan:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[CleoStannard:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[CrispinAmiriNaini:/] -@administrators= rw -@users = -@restricted = -@alumni = -CrispinAmiriNaini= r - -[DanielaMoessner:/] -@administrators= rw -@users = r -@restricted = -@alumni = -DanielaMoessner= r - -[DavidBoschert:/] -@administrators= rw -@users = r -@restricted = -@alumni = -DavidBoschert= r - -[DavidSchwaerzle:/] -@administrators= rw -@users = r -@restricted = -@alumni = -DavidSchwaerzle= r - -[DennisTrenkle:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[DingdingHe:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[EstherRiga:/] -@administrators= rw -@users = r -@restricted = -@alumni = -EstherRiga= r - -[FanWu:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[FrankScherag:/] -@administrators= rw -@users = r -@restricted = -@alumni = -FrankScherag= r - -[FranziskaDorner:/] -@administrators= rw -@users = r -@restricted = -@alumni = -FranziskaDorner= r - -[GerhardBaaken:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[GinoRodriguez:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[GregorOsterwinter:/] -@administrators= rw -@users = r -@restricted = -@alumni = -GregorOsterwinter= r - -[GuillermoBenites:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[HeidiPerez:/] -@administrators= rw -@users = r -@restricted = -@alumni = -HeidiPerez= r - -[HeikeHaller:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[HolgerFrey:/] -@administrators= rw -@users = r -@restricted = -@alumni = -UrmilShah = r -HolgerFrey= rw - -[IrenaEipert:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[JacobBelardi:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[JanNiklasSchoenberg:/] -@administrators= rw -@users = r -@restricted = -@alumni = -JanNiklasSchoenberg= r - -[JenniferPfau:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[JoachimLauterwasser:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[JohannesBaader:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[JonGreen:/] -@administrators= rw -@users = r -@restricted = -@alumni = -JonGreen= r - -[JonasGroten:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[JuergenRuehe:/] -@administrators= rw -@users = -@restricted = -@alumni = -JuergenRuehe= r - -[JuliaSaar:/] -@administrators= rw -@users = r -@restricted = -@alumni = -JuliaSaar= r - -[KarenLienkamp:/] -@administrators= rw -@users = r -@restricted = -@alumni = -KarenLienkamp= r - -[KatrinMoosmann:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[KatyaSergeeva:/] -@administrators= rw -@users = r -@restricted = -@alumni = -KatyaSergeeva= r - -[KeLi:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[KerstinSchuh:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[KimberlySimancas:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[LauraHerrera:/] -@administrators= rw -@users = r -@restricted = -@alumni = -LauraHerrera= r - -[MalwinaPajestka:/] -@administrators= rw -@users = r -@restricted = -@alumni = -MalwinaPajestka= r - -[MaraFlorea:/] -@administrators= rw -@users = r -@restricted = -@alumni = -MaraFlorea= r - -[MarcZinggeler:/] -@administrators= rw -@users = r -@restricted = -@alumni = -MarcZinggeler= r - -[MarcelHoffmann:/] -@administrators= rw -@users = r -@restricted = -@alumni = -MarcelHoffmann= r - -[MarcelRothfelder:/] -@administrators= rw -@users = r -@restricted = -@alumni = -MarcelRothfelder= r - -[MarcoArmbruster:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[MariaVoehringer:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[MartinKoerner:/] -@administrators= rw -@users = r -@restricted = -@alumni = -MartinKoerner= r - -[MartinMarazita:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[MartinRendl:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[MartinSchoenstein:/] -@administrators= rw -@users = r -@restricted = -@alumni = -MartinSchoenstein= r - -[MartinVellinger:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[MartinaAuerswald:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[MatthiasLischka:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[MatthiasMenzel:/] -@administrators= rw -@users = r -@restricted = -@alumni = -MatthiasMenzel= r - -[MaxMustermann:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[MelanieEichhorn:/] -@administrators= rw -@users = r -@restricted = -@alumni = -MelanieEichhorn= r - -[MessRechner:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[MichaelHenze:/] -@administrators= rw -@users = r -@restricted = -@alumni = -MichaelHenze= r - -[MichaelaFrase:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[MiriamScheckenbach:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[MonicaPerez:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[MonikaKurowska:/] -@administrators= rw -@users = r -@restricted = -@alumni = -MonikaKurowska= r - -[MostafaMahmoud:/] -@administrators= rw -@users = r -@restricted = -@alumni = -MostafaMahmoud= r - -[NataliaSchatz:/] -@administrators= rw -@users = r -@restricted = -@alumni = -NataliaSchatz= r - -[NicolasSchorr:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[NicoleBirsner:/] -@administrators= rw -@users = r -@restricted = -@alumni = -NicoleBirsner= r - -[NilsKorf:/] -@administrators= rw -@users = r -@restricted = -@alumni = -NilsKorf= r - -[NinoLomadze:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[Nongluck:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[OliverDornfeld:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[OswaldPrucker:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[PengZou:/] -@administrators= rw -@users = r -@restricted = -@alumni = -PengZou= r - -[PeterZahn:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[PetraHettich:/] -@administrators= rw -@users = r -@restricted = -@alumni = -PetraHettich= r - -[PhilipKotrade:/] -@administrators= rw -@users = r -@restricted = -@alumni = -PhilipKotrade= r - -[PhilippDiefenthaler:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[RaduCristianMutihac:/] -@administrators= rw -@users = r -@restricted = -@alumni = -RaduCristianMutihac= r - -[RebeccaBlell:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[RodrigoNavarro:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[RomanErath:/] -@administrators= rw -@users = r -@restricted = -@alumni = -RomanErath= r - -[SamarKazan:/] -@administrators= rw -@users = r -@restricted = -@alumni = -SamarKazan= r - -[SaraFuchs:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[SaschaEngel:/] -@administrators= rw -@users = r -@restricted = -@alumni = -SaschaEngel= r - -[SebastianBoehmer:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[SebastianBonaus:/] -@administrators= rw -@users = r -@restricted = -@alumni = -SebastianBonaus= r - -[ShararehSahneh:/] -@administrators= rw -@users = r -@restricted = -@alumni = -ShararehSahneh= r - -[SimonBodendorfer:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[SimonEbner:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[SimonSchuster:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[SimonZunker:/] -@administrators= rw -@users = r -@restricted = -@alumni = -SimonZunker= r - -[SirasaYodmongkol:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[SureshReddyBanda:/] -@administrators= rw -@users = r -@restricted = -@alumni = -SureshReddyBanda= r - -[ThananthornKanokwijitsilp:/] -@administrators= rw -@users = r -@restricted = -@alumni = -ThananthornKanokwijitsilp= r - -[ThidaratWangkam:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[ThomasBrandstetter:/] -@administrators= rw -@users = r -@restricted = -@alumni = -ThomasBrandstetter= r - -[TianyangZheng:/] -@administrators= rw -@users = r -@restricted = -@alumni = -TianyangZheng= r - -[TobiasHeitzler:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[TobiasKoenig:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[TristanBourrel:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[UlrikeRiehle:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[UrmilShah:/] -@administrators= rw -@users = r -@restricted = -@alumni = -UrmilShah= r - -[VanessaWeiss:/] -@administrators= rw -@users = r -@restricted = -@alumni = -VanessaWeiss= r - -[ViVek:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[VinicioCarias:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[VitaliyKondrashov:/] -@administrators= rw -@users = r -@restricted = -@alumni = -VitaliyKondrashov= r -SimonZunker = r - -[WibkeHartleb:/] -@administrators= rw -@users = r -@restricted = -@alumni = -WibkeHartleb= r - -[WolfgangEhm:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[XiaoqiangHou:/] -@administrators= rw -@users = r -@restricted = -@alumni = -XiaoqiangHou= r - -[YnSekhar:/] -@administrators= rw -@users = r -@restricted = -@alumni = - -[YongZhou:/] -@administrators= rw -@users = r -@restricted = -@alumni = -YongZhou= r - -[ZhuolingDeng:/] -@administrators= rw -@users = r -@restricted = -@alumni = -ZhuolingDeng= r - -[ZouStaarter:/] -@administrators= rw -@users = r -@restricted = -@alumni = diff --git a/elab_users/__init__.py b/elab_users/__init__.py new file mode 100644 index 0000000..be1b041 --- /dev/null +++ b/elab_users/__init__.py @@ -0,0 +1,206 @@ +""" Elab Users + +Manage elab (svn) users +""" + +__version__ = "0.0.1" + +import os +import sys +import argparse +import subprocess # noqa: S404 +from pathlib import Path + +from .authz import AuthzConfigParser +from .constants import ( + USERS, + ADMINS, + ALUMNI, + READ_ACL, + WRITE_ACL, + RESTRICTED, + SVN_SUFFIX, + AUTHZ_FILE_NAME, + HTPWD_FILE_NAME, +) + +SVN_REPOS_PATH = Path(os.getenv("SVN_REPOS_PATH", default=".")).resolve() + +COMMANDS = ["user", "group", "add", "restricted", "retire", "password"] + + +def get_config(svn_dir=SVN_REPOS_PATH, authz=AUTHZ_FILE_NAME): + authz_path = Path(svn_dir) / authz + if not authz_path.is_file(): + sys.exit(f"Could not find authz file at {authz_path}") + return AuthzConfigParser.from_file(authz_path) + + +def list_users(svn_dir=SVN_REPOS_PATH, authz=AUTHZ_FILE_NAME): + """list all users""" + config = get_config(svn_dir, authz) + groups = config.group_users() + for name, usernames in groups.items(): + print(f"Users in group '{name}':") + for name in sorted(usernames): + print(f" {name}") + + +def show_group_info(groupname, svn_dir=SVN_REPOS_PATH, authz=AUTHZ_FILE_NAME): + """show group information""" + config = get_config(svn_dir, authz) + groups = config.group_users() + if groupname not in groups: + sys.exit(f"Group '{groupname}' not found in authz file") + print(f"Users in group '{groupname}':") + for name in sorted(groups[groupname]): + print(f" {name}") + + +def add_new_user( + username, + group, + svn_dir=SVN_REPOS_PATH, + authz=AUTHZ_FILE_NAME, + htpwd=HTPWD_FILE_NAME, + handler=subprocess, +): + """add a user, restricted or regular""" + config = get_config(svn_dir, authz) + if username in config.elab_users: + sys.exit(f"Username '{username}' already in use") + if username.lower() in {i.name.lower() for i in svn_dir.iterdir()}: + sys.exit(f"Username '{username}' not allowed") + user = config.add_journal_acl_for(username, group) + user.create_new_repository(svn_dir, handler) + password = user.set_new_password(svn_dir / htpwd, handler=handler) + print("New password for :") + print(f"username: {username}") + print(f"password: {password}") + print(f"url: https://svn.cpi.imtek.uni-freiburg.de/{username}") + config.write_to_file() + + +def retire_user( + username, + svn_dir=SVN_REPOS_PATH, + authz=AUTHZ_FILE_NAME, + htpwd=HTPWD_FILE_NAME, + handler=subprocess, +): + config = get_config(svn_dir, authz) + if username not in config.elab_users: + sys.exit(f"User {username} not found.") + user = config.elab_users[username] + if user.group == ALUMNI: + sys.exit(f"User '{username}' is already in group '{ALUMNI}'") + if user.group == ADMINS: + sys.exit( + ( + f"User '{username}' is in group '{ADMINS}', " + f"will not moved to '{ALUMNI}'" + ) + ) + config.move_user_to_alumni(username) + config.write_to_file() + user.delete_password(svn_dir / htpwd, handler=handler) + print(f"Moved user {username} to alumni") + + +def change_password( + username, + svn_dir=SVN_REPOS_PATH, + authz=AUTHZ_FILE_NAME, + htpwd=HTPWD_FILE_NAME, + handler=subprocess, +): + config = get_config(svn_dir, authz) + if username not in config.elab_users: + sys.exit(f"User {username} not found.") + user = config.elab_users[username] + password = user.set_new_password(svn_dir / htpwd, handler=handler) + print("New password for :") + print(f"username: {username}") + print(f"password: {password}") + + +def show_user_info(username, svn_dir=SVN_REPOS_PATH, authz=AUTHZ_FILE_NAME): + config = get_config(svn_dir, authz) + if username not in config.elab_users: + sys.exit(f"User {username} not found.") + + user = config.elab_users[username] + print(f"User {user.name} is in group '{user.group}':") + + # print the write acls for a user + if user.group == ADMINS: + print(" Write access is granted to all journals.") + elif user.write_acl: + write_acl = [item + SVN_SUFFIX for item in user.write_acl] + print(" Write access is granted to:", ", ".join(write_acl)) + else: + print(" Write access is NOT granted to any journals") + + # print the read acls for a user + if user.group == ADMINS: + print(" Read access is granted to all journals.") + elif user.group == USERS: + print(" Read access is granted to (nearly) all journals.") + elif user.read_acl: + read_acl = [item + SVN_SUFFIX for item in user.read_acl] + print(" Read access is granted to:", ", ".join(read_acl)) + else: + print(" Read access is NOT granted to any journals") + + journal = config.get_journal_info(username) + print(f"Labjournal {username}{SVN_SUFFIX}") + + # print the write acls for a journal + if journal[WRITE_ACL]: + print(" Write access granted to:", ", ".join(journal[WRITE_ACL])) + else: + print(" No write access granted to anybody") + + # print the read acls for a journal + if journal[READ_ACL]: + print(" Read access granted to:", ", ".join(journal[READ_ACL])) + else: + print(" No read access granted to anybody") + + +def main( + svn_dir=SVN_REPOS_PATH, + authz=AUTHZ_FILE_NAME, + htpwd=HTPWD_FILE_NAME, + handler=subprocess, + cli_args=None, +): + parser = argparse.ArgumentParser(prog="elab-users") + parser.add_argument( + "command", + nargs="?", + help="one of the commands: [" + ", ".join(COMMANDS) + "]", + ) + parser.add_argument( + "name", nargs="?", help="user or group to perform the command on" + ) + args = parser.parse_args(cli_args) + print(args.command) + print(args.name) + + if not args.command: + list_users(svn_dir, authz) + elif args.command.lower() not in COMMANDS: + show_user_info(args.command, svn_dir, authz) + elif args.command.lower() == "user": + show_user_info(args.name, svn_dir, authz) + elif args.command.lower() == "group": + show_group_info(args.name, svn_dir, authz) + elif args.command.lower() == "add": + add_new_user(args.name, USERS, svn_dir, authz, htpwd, handler) + elif args.command.lower() == "restricted": + add_new_user(args.name, RESTRICTED, svn_dir, authz, htpwd, handler) + elif args.command.lower() == "retire": + retire_user(args.name, svn_dir, authz, htpwd, handler) + elif args.command.lower() == "password": + change_password(args.name, svn_dir, authz, htpwd, handler) diff --git a/elab_users/authz.py b/elab_users/authz.py new file mode 100644 index 0000000..7dfeb04 --- /dev/null +++ b/elab_users/authz.py @@ -0,0 +1,183 @@ +import re +import configparser + +from .users import ElabUser +from .constants import ( + USERS, + ADMINS, + ALUMNI, + READ_ACL, + WRITE_ACL, + RESTRICTED, + SVN_SUFFIX, + GROUP_DEFAULTS, +) + +RE_LIST_SEPARATORS = re.compile("[\t ,;]+") + + +def format_ini_option(key, value): + """formats a key value pair for writing an ini file""" + return " = ".join((key, str(value).replace("\n", "\n\t"))) + + +class AuthzConfigParser(configparser.ConfigParser): + """custom functions for parsing the "authz" file as used at cpi + + there is a dict of users defined, the journals themselves can be accessed + via the sections functionality of the ConfigParser base class + """ + + def __init__(self): + """initialization of the class""" + self.elab_users = {} + self.original_path = None + super().__init__() + + def optionxform(self, value): + """reset the method to use cases sensitive names""" + return str(value) + + def read(self, path): + """set up the acl defaults after reading the file""" + super().read(path) + self.original_path = path + self._extract_user_info_from_config() + + @classmethod + def from_file(cls, path): + instance = cls() + instance.read(path) + return instance + + def write_to_file(self, path=None): + path = path or self.original_path + if not path: + raise IOError("No path specified") + with open(path, "w") as filehandle: + self.write(filehandle) + + def write(self, fp): + """Write an .ini-format representation of the configuration state. + + this is adapted from the original library file. changes: + - no default section + - group-section at top + - rest of section sorted by name + """ + sorted_keys = sorted(self._sections.keys()) + sorting = ["groups"] + sorting.extend([k for k in sorted_keys if k != "groups"]) + for section in sorting: + fp.write("[%s]\n" % section) + acls = { + k: v + for k, v in self._sections[section].items() + if k != "__name__" + } + if section != "groups": + for group in (ADMINS, USERS, RESTRICTED, ALUMNI): + group_id = "@" + group + acl_value = acls.pop(group_id, GROUP_DEFAULTS[group]) + key = format_ini_option(group_id, acl_value) + fp.write("%s\n" % (key)) + for (key, value) in acls.items(): + if (value is not None) or (self._optcre == self.OPTCRE): + key = format_ini_option(key, value) + fp.write("%s\n" % (key)) + fp.write("\n") + + def _extract_user_info_from_config(self): + """extracts the user information from the config file + + the information of the journals can be accessed via get_journal_info + """ + # first parse the group definitions + self._extract_group_definitions() + # walk through the sections to get individual acl information + self._extract_individual_acls() + + def _extract_group_definitions(self): + """extracts the group information from the config file""" + # first parse the group definitions + for group, userlist in self.items("groups"): + if group not in GROUP_DEFAULTS: + raise KeyError(f"Undefined group {group} in authz file") + for username in RE_LIST_SEPARATORS.split(userlist): + if username in self.elab_users: + raise Exception( + ( + f"Found duplicate entry for user " + f"{username} in authz file" + ) + ) + self.elab_users[username] = ElabUser(username, group) + + def _extract_individual_acls(self): + """extracts the acl information from the elab section""" + elabs = (item for item in self.sections() if item.endswith(SVN_SUFFIX)) + for elab in elabs: + for (user_or_group, acl) in self.items(elab): + if user_or_group in self.elab_users: + # a nicer name for the lab journal + belongs_to = elab[: -len(SVN_SUFFIX)] + # a acl entry for a user + if acl.lower() == WRITE_ACL: + self.elab_users[user_or_group].write_acl.add( + belongs_to + ) + elif acl.lower() == READ_ACL: + self.elab_users[user_or_group].read_acl.add(belongs_to) + + def group_users(self): + """uses the list of users to group them by their group name""" + groups = {key: [] for key in GROUP_DEFAULTS.keys()} + for user in self.elab_users.values(): + if user.group not in groups: + raise KeyError( + f"found unknown group {user.group} for user {user.name}" + ) + groups[user.group].append(user.name) + return groups + + def add_journal_acl_for(self, username, group): + """sets the acls for a new user an the corresponding journal""" + self.elab_users[username] = ElabUser(username, group) + journal_path = username + SVN_SUFFIX + self.add_section(journal_path) + self.set(journal_path, username, WRITE_ACL) + for group, acl in GROUP_DEFAULTS.items(): + self.set(journal_path, f"@{group}", acl) + self._update_user_group_config() + return self.elab_users[username] + + def move_user_to_alumni(self, name): + """moves a user to the alumni group and removes the acl privileges""" + user = self.elab_users[name] + user.group = ALUMNI + for access_to in user.write_acl: + self.remove_option(access_to + SVN_SUFFIX, user.name) + for access_to in user.read_acl: + self.remove_option(access_to + SVN_SUFFIX, user.name) + user.write_acl = set() + user.read_acl = set() + self._update_user_group_config() + return user + + def _update_user_group_config(self): + """updates the config settings of the groups section""" + groups = self.group_users() + for group, userlist in groups.items(): + self.set("groups", group, ", ".join(sorted(userlist))) + + def get_journal_info(self, elab): + """returns read and write access info of an lab journal""" + if not elab.endswith(SVN_SUFFIX): + elab = elab + SVN_SUFFIX + if not self.has_section(elab): + return None + info = {WRITE_ACL: [], READ_ACL: []} + for (user_or_group, acl) in self.items(elab): + if acl in (WRITE_ACL, READ_ACL): + info[acl].append(user_or_group) + return info diff --git a/elab_users/constants.py b/elab_users/constants.py new file mode 100644 index 0000000..fad70dc --- /dev/null +++ b/elab_users/constants.py @@ -0,0 +1,20 @@ +AUTHZ_FILE_NAME = "authz" +HTPWD_FILE_NAME = ".htpasswd" + +ADMINS = "administrators" +USERS = "users" +RESTRICTED = "restricted" +ALUMNI = "alumni" + +NO_ACL = "" +READ_ACL = "r" +WRITE_ACL = "rw" + +GROUP_DEFAULTS = { + ADMINS: WRITE_ACL, + USERS: READ_ACL, + RESTRICTED: NO_ACL, + ALUMNI: NO_ACL, +} + +SVN_SUFFIX = ":/" diff --git a/elab_users/users.py b/elab_users/users.py new file mode 100644 index 0000000..34ca518 --- /dev/null +++ b/elab_users/users.py @@ -0,0 +1,79 @@ +import os +import random +import string +import tempfile +import subprocess # noqa: S404 +from typing import Set +from pathlib import Path +from datetime import datetime +from dataclasses import field, dataclass + + +@dataclass +class ElabUser: + name: str + group: str + write_acl: Set = field(default_factory=set) + read_acl: Set = field(default_factory=set) + + def __str__(self): + """return a string representation""" + return self.name + + def set_new_password(self, htpasswd_path, length=10, handler=subprocess): + """sets a new random password for a user""" + characters = string.ascii_letters + string.digits + password = "".join( + random.choice(characters) for i in range(length) # noqa: S311 + ) + handler.check_call( + ["htpasswd", "-b", htpasswd_path, self.name, password] + ) + return password + + def delete_password(self, htpasswd_path, handler=subprocess): + """deletes a password for a user""" + # if the user was not added to the password db, the removal will show + # an error message that is confusing to the user - at least it + # confused me - so redirect this to /dev/null + with open(os.devnull, "wb") as devnull: + handler.check_call( + ["htpasswd", "-D", htpasswd_path, self.name], stderr=devnull + ) + + def create_new_repository(self, data_dir, handler=subprocess): + """creates a repository for a user and checks in some stuff""" + # create the new repository + data_dir = Path(data_dir) + new_repo = data_dir / self.name + handler.check_call( + ["svnadmin", "create", str(new_repo)], stderr=handler.STDOUT + ) + + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = Path(tmpdir) + # check out a temporary working copy + handler.check_call( + ["svn", "checkout", f"file://{new_repo}", str(tmpdir)] + ) + # create subfolders + today = datetime.now() + year_path = tmpdir / f"{today.year:0>4}" + year_path.mkdir() + for month in range(today.month, 13): + month_path = year_path / f"{month:0>2}" + month_path.mkdir() + handler.check_call(["touch", str(month_path / ".empty")]) + # copy some examples + for temp in ("experiment", "synthesis", "toc"): + filename = f"template-{temp}.doc" + in_file = data_dir / filename + out_file = tmpdir / filename + handler.check_call(["cp", str(in_file), str(out_file)]) + # add and commit the changes + handler.check_call( + ["svn", "add", "--force", str(tmpdir) + "/"] # noqa: S604 + ) + handler.check_call( + ["svn", "commit", "-m", f"New User: {self.name}", str(tmpdir)] + ) diff --git a/htpasswd b/htpasswd deleted file mode 100644 index af66e5c..0000000 --- a/htpasswd +++ /dev/null @@ -1,6 +0,0 @@ -foo:$apr1$SzJRyvJU$U3luHwCA6xHfKowizE.Gl. -FOO:$apr1$LSPDdLqg$tiGbDGgNEXcRA/oyadYSw1 -AndreasEvers:$apr1$n0Oaok6e$wyHcUg6Upm9sE2AoYlVMO/ -FOOBar:$apr1$pZCbClF5$smEDwhMJIVmPsNmMEkRPd1 -FooBar:$apr1$24r9zF2e$9q30fNOqSlvn6itdhZMpc1 -UrmilShh:$apr1$WxMGE8Wb$H0xWao6KZGqBJoXj7fJ420 diff --git a/manage.py b/manage.py index 0118de2..eadddaa 100644 --- a/manage.py +++ b/manage.py @@ -12,7 +12,7 @@ import sys from datetime import datetime # defining some constants -MOUNT_PATH = os.path.join("/mnt", "sshfs-for-svn") +MOUNT_PATH = os.path.join("/mnt", "nfs-data-store-1", "drive") REPO_PATH = os.path.join(MOUNT_PATH, "svn-repository") AUTHZ_PATH = os.path.join(REPO_PATH, "authz") HTPWD_PATH = os.path.join(REPO_PATH, ".htpasswd") @@ -142,8 +142,8 @@ class AuthzConfigParser(ConfigParser.ConfigParser, object): for section in sorting: fp.write("[%s]\n" % section) acls = dict( (k, v) for k, v in self._sections[section].items() if k != "__name__") - if section != "groups": - for group in (ADMINS, USERS, RESTRICTED, ALUMNI): + if section != "groups": + for group in (ADMINS, USERS, RESTRICTED, ALUMNI): group_id = "@" + group acl_value = acls.pop(group_id, GROUP_DEFAULTS[group]) key = " = ".join((group_id, str(acl_value).replace('\n', '\n\t'))) @@ -286,14 +286,16 @@ if __name__ == "__main__": if options.what in ("a", "r"): # add a user, restricted or regular if name in config.elab_users: - sys.exit("Username '%s' already in use" % username) + sys.exit("Username '%s' already in use" % name) group = RESTRICTED if options.what == "r" else USERS config.add_journal_acl_for(name, group) create_new_repository(name) #subprocess.check_call(SVN_DIR_CREATOR + " " + name, shell=True) password = set_new_password(name) - print "New password for user '%s': '%s'" % (name, password) - print "http://svn.cpi.imtek.uni-freiburg.de/" + name + print "New password for :" + print "username: " + name + print "password: " + password + print "url: https://svn.cpi.imtek.uni-freiburg.de/" + name config.write_to_file() sys.exit() @@ -316,7 +318,9 @@ if __name__ == "__main__": if options.what == "p": # reset a password password = set_new_password(name) - print "New password for user '%s': '%s'" % (name, password) + print "New password for :" + print "username: " + name + print "password: " + password sys.exit() # no option, just a name: diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1499f67 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,78 @@ + + +[build-system] +requires = ["flit"] +build-backend = "flit.buildapi" + +[tool.flit.metadata] +module = "elab_users" +dist-name = "elab_users" +author = "Holger Frey" +author-email = "frey@imtek.de" +home-page = "https://git.cpi.imtek.uni-freiburg.de/CPI/elab-users.git" +description-file = "README.md" +license = "Beerware" + +# see https://pypi.org/classifiers/ +classifiers = [ + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3 :: Only", + "License :: Freely Distributable", +] + +requires = [ + +] +requires-python = ">=3.7" + +[tool.flit.metadata.requires-extra] +test = [ + "pytest >=4.0.0", + "pytest-cov", + "pytest-mock", + "pytest-randomly", + "tox", +] +dev = [ + "black", + "flake8", + "flake8-comprehensions", + "flake8-bandit", + "isort >= 5.0.0", + "keyring", + "pre-commit", +] + +[tool.flit.scripts] +elab-users = "elab_users:main" + +[tool.black] +line-length = 79 +py37 = true +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.tox + | \.venv + | build + | dist +)/ +''' + +[tool.isort] +line_length=79 +multi_line_output=3 +length_sort="True" +include_trailing_comma="True" + +[tool.pytest.ini_options] +markers = [ + "fun: marks tests as functional (deselect with '-m \"not fun\"')", +] +addopts = [ + "--strict-markers", +] diff --git a/run.py b/run.py new file mode 100644 index 0000000..eb431be --- /dev/null +++ b/run.py @@ -0,0 +1,9 @@ +import elab_users.authz + +f = "test-data/authz" +p = elab_users.authz.AuthzConfigParser() +p.read(f) +print(p.group_users()) + +print(p.get_journal_info("CamillaOestevold")) +print(p.get_journal_info("AlexanderDietz")) diff --git a/test-data/authz b/test-data/authz new file mode 100644 index 0000000..920cdda --- /dev/null +++ b/test-data/authz @@ -0,0 +1,39 @@ +[groups] +administrators = JuergenRuehe, OswaldPrucker +alumni = AlexeyKopyshev, AndreasBoenisch, AndreasEvers, AnkeWoerz, AnneLoesche, ArthurMartens, ArulGeetha, CamillaOestevold, CanerKaganaslan, ChristianSchuh, ChristineBunte, CkPandiyarajan, CleoStannard, FanWu, GerhardBaaken, GinoRodriguez, GuillermoBenites, HeikeHaller, IrenaEipert, JacobBelardi, JenniferPfau, JoachimLauterwasser, JohannesBaader, KatrinMoosmann, KeLi, KerstinSchuh, KimberlySimancas, MarcoArmbruster, MariaVoehringer, MariaVohringer, MartinRendl, MartinVellinger, MartinaAuerswald, MatthiasLischka, MessRechner, MichaelaFrase, MiriamScheckenbach, MonicaPerez, NinoLomadze, Nongluck, OliverDornfeld, PeterZahn, PhilippDiefenthaler, PhilippWollermann, RebeccaBlell, RodrigoNavarro, SaraFuchs, SebastianBoehmer, SebastianSebald, SimonBodendorfer, SimonSchuster, SirasaYodmongkol, ThidaratWangkam, TobiasHeitzler, TobiasKoenig, TristanBourrel, UlrikeRiehle, ViVek, VinicioCarias, WolfgangEhm, YnSekhar, ZouStaarter +users = AlexanderDietz, AliciaMalekLuz, AndreasMader, AnnaSchuler, AnneBuderer, ChristophScheibelein, CrispinAmiriNaini, DanielaMoessner, DavidBoschert, DavidSchwaerzle, EstherRiga, FrankScherag, FranziskaDorner, GregorOsterwinter, HeidiPerez, HolgerFrey, JanNiklasSchoenberg, JonGreen, KarenLienkamp, KatyaSergeeva, LauraHerrera, MalwinaPajestka, MaraFlorea, MarcZinggeler, MarcelHoffmann, MarcelRothfelder, MartinKoerner, MartinSchoenstein, MatthiasMenzel, MelanieEichhorn, MichaelHenze, MonikaKurowska, MostafaMahmoud, NataliaSchatz, NicoleBirsner, NilsKorf, PengZou, PetraHettich, PhilipKotrade, RaduCristianMutihac, RomanErath, SamarKazan, SaschaEngel, SebastianBonaus, ShararehSahneh, SureshReddyBanda, ThananthornKanokwijitsilp, ThomasBrandstetter, TianyangZheng, VanessaWeiss, VitaliyKondrashov, WibkeHartleb, XiaoqiangHou, ZhuolingDeng +restricted = BeniPrasser, JuliaSaar, SimonZunker, UrmilShah, YongZhou + +[AlexanderDietz:/] +@administrators = rw +@users = r +@restricted = +@alumni = +AlexanderDietz = rw + +[AlexeyKopyshev:/] +@administrators = rw +@users = r +@restricted = +@alumni = + +[AndreasEvers:/] +@administrators = rw +@users = r +@restricted = +@alumni = +UrmilShah = r + +[OswaldPrucker:/] +@administrators = rw +@users = r +@restricted = +@alumni = +OswaldPrucker = rw + +[UrmilShah:/] +@administrators = rw +@users = r +@restricted = +@alumni = +UrmilShah = rw \ No newline at end of file diff --git a/authz.original b/test-data/authz.original similarity index 100% rename from authz.original rename to test-data/authz.original diff --git a/test-data/htpasswd b/test-data/htpasswd new file mode 100644 index 0000000..5ea70aa --- /dev/null +++ b/test-data/htpasswd @@ -0,0 +1,2 @@ +AlexanderDietz:$apr1$n0Oaok6e$wyHcUg6Upm9sE2AoYlVMO/ +UrmilShh:$apr1$WxMGE8Wb$H0xWao6KZGqBJoXj7fJ420 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..db5780f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,66 @@ +import shutil +import tempfile +from typing import Dict, List +from pathlib import Path +from dataclasses import dataclass + +import pytest + + +@dataclass +class StubCall: + func: str + args: List + kargs: Dict + + +class StubShell: + + STDOUT = "STDOUT" + + def __init__(self): + self.stack = [] + + def _add(self, func, args, kargs): + sc = StubCall(func, args, kargs) + self.stack.append(sc) + + def check_call(self, args, **kargs): + self._add("check_call", args, kargs) + + +@pytest.fixture +def stub_handler(): + return StubShell() + + +def temporary_data_file(src_data_dir, file_name): + source = src_data_dir / file_name + with tempfile.TemporaryDirectory() as tmpdirname: + destination = Path(tmpdirname) / file_name + shutil.copy(source, destination) + yield destination + + +@pytest.fixture +def example_data_dir(): + this_file = Path(__file__).absolute() + src_dir = this_file.parent.parent + return src_dir / "test-data" + + +@pytest.fixture +def example_authz(example_data_dir): + yield from temporary_data_file(example_data_dir, "authz") + + +@pytest.fixture +def example_htpasswd(example_data_dir): + yield from temporary_data_file(example_data_dir, "htpasswd") + + +@pytest.fixture +def example_empty_file(): + with tempfile.TemporaryDirectory() as tmpdirname: + destination = Path(tmpdirname) / "empty" + yield destination diff --git a/tests/test_elab_users.py b/tests/test_elab_users.py new file mode 100644 index 0000000..570010d --- /dev/null +++ b/tests/test_elab_users.py @@ -0,0 +1,386 @@ +""" Stub file for testing the project + +There are three predefined ways to run tests: + +make test: + runs only unit tests, that are not marked with "fun" (for functional test) + in a random order. If a test failed before, only the failed tests will be + run. This is intended to be the default testing method while developing. + +make testall: + runs unit tests and functional tests in random order. Will give a complete + overview of the test suite. + +make coverage: + runs only tests marked with "fun" (for functional tests) and generates a + coverage report for the test run. The idea is to check the test coverage + only on functinal tests to see if a) everything is as much covered as + possible and b) to find dead code that is not called in end-to-end tests. + +all three test strategies will run "make lint" before to catch easily made +mistakes. +""" + +import pytest + +INFO_RESULT_AD = """ +User AlexanderDietz is in group 'users': + Write access is granted to: AlexanderDietz:/ + Read access is granted to (nearly) all journals. +Labjournal AlexanderDietz:/ + Write access granted to: @administrators, AlexanderDietz + Read access granted to: @users +""" + +INFO_RESULT_OP = """ +User OswaldPrucker is in group 'administrators': + Write access is granted to all journals. + Read access is granted to all journals. +Labjournal OswaldPrucker:/ + Write access granted to: @administrators, OswaldPrucker + Read access granted to: @users +""" + + +INFO_RESULT_AE = """ +User AndreasEvers is in group 'alumni': + Write access is NOT granted to any journals + Read access is NOT granted to any journals +Labjournal AndreasEvers:/ + Write access granted to: @administrators + Read access granted to: @users, UrmilShah +""" + + +INFO_RESULT_US = """ +User UrmilShah is in group 'restricted': + Write access is granted to: UrmilShah:/ + Read access is granted to: AndreasEvers:/ +Labjournal UrmilShah:/ + Write access granted to: @administrators, UrmilShah + Read access granted to: @users +""" + + +@pytest.mark.fun +def test_get_config(example_authz): + from elab_users import get_config + from elab_users.authz import AuthzConfigParser + + tmp_dir = example_authz.parent + parser = get_config(tmp_dir, "authz") + + assert isinstance(parser, AuthzConfigParser) + assert parser.elab_users != {} + + +@pytest.mark.fun +def test_get_config_missing_file(example_authz): + from elab_users import get_config + + tmp_dir = example_authz.parent + with pytest.raises(SystemExit): + get_config(tmp_dir, "not existent") + + +@pytest.mark.fun +def test_list_users(example_authz, capsys): + from elab_users import list_users + + list_users(example_authz.parent, example_authz.name) + captured = capsys.readouterr() + + admins = "Users in group 'administrators':\n JuergenRuehe\n" + assert admins in captured.out + users = "Users in group 'users':\n AlexanderDietz\n" + assert users in captured.out + restricted = "Users in group 'restricted':\n BeniPrasser\n" + assert restricted in captured.out + alumni = "Users in group 'alumni':\n AlexeyKopyshev\n" + assert alumni in captured.out + + +@pytest.mark.fun +def test_show_group_info(example_authz, capsys): + from elab_users import show_group_info + + show_group_info("alumni", example_authz.parent, example_authz.name) + captured = capsys.readouterr() + + alumni = "Users in group 'alumni':\n AlexeyKopyshev\n" + assert captured.out.startswith(alumni) + + +@pytest.mark.fun +def test_show_group_info_unknown_group(example_authz): + from elab_users import show_group_info + + with pytest.raises(SystemExit): + show_group_info("unknown", example_authz.parent, example_authz.name) + + +@pytest.mark.fun +def test_add_new_user(example_authz, example_htpasswd, stub_handler, capsys): + from elab_users import get_config, add_new_user + + add_new_user( + "JaneDoe", + "users", + example_authz.parent, + example_authz.name, + example_htpasswd.name, + handler=stub_handler, + ) + captured = capsys.readouterr() + + assert captured.out.startswith("New password for :") + assert "username: JaneDoe" in captured.out + url = "https://svn.cpi.imtek.uni-freiburg.de/JaneDoe" + assert f"url: {url}" in captured.out + + assert stub_handler.stack[0].args[:2] == ["svnadmin", "create"] + assert stub_handler.stack[0].args[2].endswith("/JaneDoe") + + config = get_config(example_authz.parent, example_authz.name) + user = config.elab_users["JaneDoe"] + assert user.group == "users" + assert user.read_acl == set() + assert user.write_acl == {"JaneDoe"} + + items = config.items("JaneDoe:/") + assert sorted(items) == [ + ("@administrators", "rw"), + ("@alumni", ""), + ("@restricted", ""), + ("@users", "r"), + ("JaneDoe", "rw"), + ] + + +@pytest.mark.fun +def test_add_new_user_error_on_existing_user( + example_authz, example_htpasswd, stub_handler +): + from elab_users import add_new_user + + with pytest.raises(SystemExit): + add_new_user( + "AlexanderDietz", + "users", + example_authz.parent, + example_authz.name, + example_htpasswd.name, + handler=stub_handler, + ) + + +@pytest.mark.fun +def test_add_new_user_error_on_forbidden_name( + example_authz, example_htpasswd, stub_handler +): + from elab_users import add_new_user + + with pytest.raises(SystemExit): + add_new_user( + "authz", + "users", + example_authz.parent, + example_authz.name, + example_htpasswd.name, + handler=stub_handler, + ) + + +@pytest.mark.fun +def test_retire_user(example_authz, example_htpasswd, stub_handler, capsys): + + from elab_users import get_config, retire_user + + retire_user( + "AlexanderDietz", + example_authz.parent, + example_authz.name, + example_htpasswd.name, + handler=stub_handler, + ) + captured = capsys.readouterr() + assert captured.out.startswith("Moved user AlexanderDietz to alumni") + + config = get_config(example_authz.parent, example_authz.name) + user = config.elab_users["AlexanderDietz"] + assert user.group == "alumni" + assert user.read_acl == set() + assert user.write_acl == set() + + assert stub_handler.stack[-1].args[:2] == ["htpasswd", "-D"] + + +@pytest.mark.fun +def test_retire_user_error_unknown_user( + example_authz, example_htpasswd, stub_handler +): + + from elab_users import retire_user + + with pytest.raises(SystemExit): + retire_user( + "Unknown", + example_authz.parent, + example_authz.name, + example_htpasswd.name, + handler=stub_handler, + ) + + +@pytest.mark.fun +def test_retire_user_error_already_alumni( + example_authz, example_htpasswd, stub_handler +): + + from elab_users import retire_user + + with pytest.raises(SystemExit): + retire_user( + "CamillaOestevold", + example_authz.parent, + example_authz.name, + example_htpasswd.name, + handler=stub_handler, + ) + + +@pytest.mark.fun +def test_retire_user_error_admin( + example_authz, example_htpasswd, stub_handler +): + + from elab_users import retire_user + + with pytest.raises(SystemExit): + retire_user( + "OswaldPrucker", + example_authz.parent, + example_authz.name, + example_htpasswd.name, + handler=stub_handler, + ) + + +@pytest.mark.fun +def test_change_password( + example_authz, example_htpasswd, stub_handler, capsys +): + + from elab_users import change_password + + change_password( + "AlexanderDietz", + example_authz.parent, + example_authz.name, + example_htpasswd.name, + handler=stub_handler, + ) + captured = capsys.readouterr() + assert captured.out.startswith("New password for :") + + assert stub_handler.stack[-1].args[:2] == ["htpasswd", "-b"] + + +@pytest.mark.fun +def test_change_password_error_unknown_user( + example_authz, example_htpasswd, stub_handler +): + + from elab_users import change_password + + with pytest.raises(SystemExit): + change_password( + "Unknown", + example_authz.parent, + example_authz.name, + example_htpasswd.name, + handler=stub_handler, + ) + + +@pytest.mark.fun +@pytest.mark.parametrize( + "user, result", + [ + ("OswaldPrucker", INFO_RESULT_OP), + ("AlexanderDietz", INFO_RESULT_AD), + ("AndreasEvers", INFO_RESULT_AE), + ("UrmilShah", INFO_RESULT_US), + ], +) +def test_show_user_info(user, result, example_authz, capsys): + from elab_users import show_user_info + + show_user_info( + user, + example_authz.parent, + example_authz.name, + ) + captured = capsys.readouterr() + + assert captured.out.strip() == result.strip() + + +@pytest.mark.fun +def test_show_user_info_error_unknown_user(example_authz): + from elab_users import show_user_info + + with pytest.raises(SystemExit): + show_user_info( + "Unknown", + example_authz.parent, + example_authz.name, + ) + + +@pytest.mark.fun +@pytest.mark.parametrize( + "commands, result", + [ + ([], "Users in group 'restricted':"), + (["OswaldPrucker"], "granted to all journals"), + (["user", "OswaldPrucker"], "granted to all journals"), + (["group", "alumni"], "Users in group 'alumni':"), + (["add", "JaneDoe"], "url:"), + (["restricted", "JaneDoe"], "url:"), + (["retire", "AlexanderDietz"], "to alumni"), + (["password", "AlexanderDietz"], "username: AlexanderDietz"), + ], +) +def test_main( + commands, result, example_authz, example_htpasswd, stub_handler, capsys +): + from elab_users import main + + main( + example_authz.parent, + example_authz.name, + example_htpasswd.name, + handler=stub_handler, + cli_args=commands, + ) + captured = capsys.readouterr() + + assert result in captured.out + + +@pytest.mark.fun +def test_main_help(example_authz, example_htpasswd, stub_handler, capsys): + from elab_users import main + + with pytest.raises(SystemExit): + main( + example_authz.parent, + example_authz.name, + example_htpasswd.name, + handler=stub_handler, + cli_args=["--help"], + ) + captured = capsys.readouterr() + + assert "usage: elab-users [-h] [command] [name]" in captured.out diff --git a/tests/test_elab_users_authz.py b/tests/test_elab_users_authz.py new file mode 100644 index 0000000..5bfbadb --- /dev/null +++ b/tests/test_elab_users_authz.py @@ -0,0 +1,258 @@ +import pytest + + +def read_lines(path): + with path.open("r") as fh: + content = fh.read().strip() + lines = content.splitlines() + return [line.strip() for line in lines] + + +@pytest.mark.parametrize( + "value, expected", + [ + ("no line break", "KEY = no line break"), + ("with\nline\nbreak", "KEY = with\n\tline\n\tbreak"), + ], +) +def test_authz_format_ini_option(value, expected): + from elab_users.authz import format_ini_option + + result = format_ini_option("KEY", value) + + assert result == expected + + +def test_authz_parser_init(): + import configparser + + from elab_users.authz import AuthzConfigParser + + parser = AuthzConfigParser() + + assert isinstance(parser, configparser.ConfigParser) + assert parser.elab_users == {} + assert parser.original_path is None + + +def test_authz_parser_optionxfrom(): + from elab_users.authz import AuthzConfigParser + + parser = AuthzConfigParser() + + assert parser.optionxform(123) == "123" + + +def test_authz_parser_read(example_authz): + from elab_users.authz import AuthzConfigParser + + parser = AuthzConfigParser() + parser.read(example_authz) + + assert parser.original_path == example_authz + assert parser.elab_users != {} + + +def test_authz_parser_from_file(example_authz): + from elab_users.authz import AuthzConfigParser + + parser = AuthzConfigParser.from_file(example_authz) + + assert isinstance(parser, AuthzConfigParser) + assert parser.original_path == example_authz + assert parser.elab_users != {} + + +def test_authz_parser_write_to_file_raises_error(): + from elab_users.authz import AuthzConfigParser + + parser = AuthzConfigParser() + + with pytest.raises(IOError): + parser.write_to_file(path=None) + + +def test_authz_parser_write_to_file_uses_original_path( + example_authz, example_empty_file +): + from elab_users.authz import AuthzConfigParser + + parser = AuthzConfigParser.from_file(example_authz) + parser.original_path = example_empty_file + parser.write_to_file(path=None) + + assert example_empty_file.is_file() + + +def test_authz_parser_write_to_file_custom_path( + example_authz, example_empty_file +): + from elab_users.authz import AuthzConfigParser + + parser = AuthzConfigParser.from_file(example_authz) + parser.write_to_file(path=example_empty_file) + + assert example_empty_file.is_file() + + +def test_authz_parser_write(example_authz, example_empty_file): + from elab_users.authz import AuthzConfigParser + + parser = AuthzConfigParser.from_file(example_authz) + with open(example_empty_file, "w") as fh: + parser.write(fh) + + original = read_lines(example_authz) + created = read_lines(example_empty_file) + assert original == created + + +def test_authz_parser_extract_user_info_from_config(example_authz): + from elab_users.authz import AuthzConfigParser + + parser = AuthzConfigParser() + + super(type(parser), parser).read(example_authz) + assert parser.elab_users == {} + + parser._extract_user_info_from_config() + assert parser.elab_users != {} + + +@pytest.mark.parametrize( + "name, group", + [ + ("OswaldPrucker", "administrators"), + ("AlexanderDietz", "users"), + ("UrmilShah", "restricted"), + ("CamillaOestevold", "alumni"), + ], +) +def test_authz_parser_extract_group_definitions(name, group, example_authz): + from elab_users.authz import AuthzConfigParser + + parser = AuthzConfigParser() + + super(type(parser), parser).read(example_authz) + parser._extract_group_definitions() + + user = parser.elab_users[name] + assert user.group == group + + +@pytest.mark.parametrize( + "name, read, write", + [ + ("OswaldPrucker", [], ["OswaldPrucker"]), + ("AlexanderDietz", [], ["AlexanderDietz"]), + ("UrmilShah", ["AndreasEvers"], ["UrmilShah"]), + ], +) +def test_authz_parser_extract_individual_acls( + name, read, write, example_authz +): + from elab_users.authz import AuthzConfigParser + + parser = AuthzConfigParser() + + super(type(parser), parser).read(example_authz) + parser._extract_group_definitions() + parser._extract_individual_acls() + + user = parser.elab_users[name] + assert user.read_acl == set(read) + assert user.write_acl == set(write) + + +def test_authz_parser_group_users(example_authz): + from elab_users.authz import AuthzConfigParser + + parser = AuthzConfigParser.from_file(example_authz) + + groups = parser.group_users() + + assert len(groups) == 4 + assert len(groups["administrators"]) == 2 + assert len(groups["users"]) == 54 + assert len(groups["restricted"]) == 5 + assert len(groups["alumni"]) == 62 + + +def test_authz_parser_add_journal_acl_for(example_authz): + from elab_users.authz import AuthzConfigParser + + parser = AuthzConfigParser.from_file(example_authz) + + user = parser.add_journal_acl_for("JaneDoe", "users") + + assert user.name == "JaneDoe" + assert user.group == "users" + assert parser.elab_users["JaneDoe"] == user + assert "JaneDoe:/" in parser.sections() + items = parser.items("JaneDoe:/") + assert sorted(items) == [ + ("@administrators", "rw"), + ("@alumni", ""), + ("@restricted", ""), + ("@users", "r"), + ("JaneDoe", "rw"), + ] + + +def test_authz_parser_move_user_to_alumni(example_authz): + from elab_users.authz import AuthzConfigParser + + parser = AuthzConfigParser.from_file(example_authz) + + user = parser.move_user_to_alumni("UrmilShah") + + assert user.name == "UrmilShah" + assert user.group == "alumni" + assert user.write_acl == set() + assert user.read_acl == set() + + for group, userlist in parser.items("groups"): + if group == "alumni": + assert "UrmilShah" in userlist + else: + assert "UrmilShah" not in userlist + + +def test_authz_parser_update_user_group_config(example_authz): + from elab_users.authz import AuthzConfigParser + + parser = AuthzConfigParser.from_file(example_authz) + parser.elab_users["UrmilShah"].group = "alumni" + + parser._update_user_group_config() + + for group, userlist in parser.items("groups"): + if group == "alumni": + assert "UrmilShah" in userlist + else: + assert "UrmilShah" not in userlist + + +@pytest.mark.parametrize( + "elab, read, write", + [ + ("AlexeyKopyshev:/", ["@users"], ["@administrators"]), + ( + "AndreasEvers:/", + ["@users", "UrmilShah"], + ["@administrators"], + ), + ( + "UrmilShah:/", + ["@users"], + ["@administrators", "UrmilShah"], + ), + ], +) +def test_authz_parser_get_journal_info(elab, read, write, example_authz): + from elab_users.authz import AuthzConfigParser + + parser = AuthzConfigParser.from_file(example_authz) + + info = parser.get_journal_info(elab) + assert info == {"r": read, "rw": write} diff --git a/tests/test_elab_users_users.py b/tests/test_elab_users_users.py new file mode 100644 index 0000000..2567424 --- /dev/null +++ b/tests/test_elab_users_users.py @@ -0,0 +1,104 @@ +# import pytest +from datetime import datetime + + +def test_elabuser_string_representation(): + from elab_users.users import ElabUser + + eu = ElabUser("John Doe", "Some Group") + + assert str(eu) == "John Doe" + + +def test_elabuser_set_new_password(stub_handler): + from elab_users.users import ElabUser + + eu = ElabUser("John Doe", "Some Group") + + password = eu.set_new_password("some path", 12, handler=stub_handler) + + assert len(password) == 12 + assert len(stub_handler.stack) == 1 + called = stub_handler.stack[0] + assert called.func == "check_call" + assert called.args == [ + "htpasswd", + "-b", + "some path", + "John Doe", + password, + ] + assert called.kargs == {} + + +def test_elabuser_delete_password(stub_handler): + from elab_users.users import ElabUser + + eu = ElabUser("John Doe", "Some Group") + + eu.delete_password("some path", handler=stub_handler) + + assert len(stub_handler.stack) == 1 + called = stub_handler.stack[0] + assert called.func == "check_call" + assert called.args == [ + "htpasswd", + "-D", + "some path", + "John Doe", + ] + assert list(called.kargs.keys()) == ["stderr"] + + +def test_elabuser_create_new_repo(stub_handler): + from elab_users.users import ElabUser + + eu = ElabUser("John Doe", "Some Group") + + eu.create_new_repository("some path", handler=stub_handler) + + today = datetime.now() + current_month = today.month + current_year = today.year + + assert len(stub_handler.stack) == 8 + (12 - current_month) + + called = stub_handler.stack[0] + assert called.func == "check_call" + assert called.args == ["svnadmin", "create", "some path/John Doe"] + assert called.kargs == {"stderr": stub_handler.STDOUT} + + called = stub_handler.stack[1] + assert called.func == "check_call" + assert called.args[:3] == ["svn", "checkout", "file://some path/John Doe"] + assert called.args[3].startswith("/tmp/") # noqa: S108 + assert called.kargs == {} + + called = stub_handler.stack[2] + assert called.func == "check_call" + assert called.args[0] == "touch" + assert called.args[1].startswith("/tmp/") # noqa: S108 + assert called.args[1].endswith( + f"/{current_year:0>4}/{current_month:0>2}/.empty" + ) + assert called.kargs == {} + + called = stub_handler.stack[-3] + assert called.func == "check_call" + assert called.args[0] == "cp" + assert called.args[1] == "some path/template-toc.doc" + assert called.args[2].startswith("/tmp/") # noqa: S108 + assert called.args[2].endswith("/template-toc.doc") + assert called.kargs == {} + + called = stub_handler.stack[-2] + assert called.func == "check_call" + assert called.args[:3] == ["svn", "add", "--force"] + assert called.args[3].startswith("/tmp/") # noqa: S108 + assert called.args[3].endswith("/") + assert called.kargs == {} + + called = stub_handler.stack[-1] + assert called.func == "check_call" + assert called.args[:4] == ["svn", "commit", "-m", "New User: John Doe"] + assert called.args[4].startswith("/tmp/") # noqa: S108 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..1aae05d --- /dev/null +++ b/tox.ini @@ -0,0 +1,14 @@ +[tox] +envlist = py37 +isolated_build = True + +[testenv] +deps = + pytest + pytest-cov + pytest-mock + setuptools>=41.2.0 + pip>=20.0.2 + +changedir = {toxinidir}/tests +commands = pytest --cov=elab_users