Update project sturcture and rewrite for python3 #1

Merged
holgi merged 15 commits from py3 into master 3 years ago
  1. 4
      .flake8
  2. 68
      .gitignore
  3. 38
      .pre-commit-config.yaml
  4. 4
      CHANGES.md
  5. 117
      CONTRIBUTING.md
  6. 10
      LICENSE
  7. 90
      Makefile
  8. 68
      README.md
  9. 848
      authz
  10. 206
      elab_users/__init__.py
  11. 183
      elab_users/authz.py
  12. 20
      elab_users/constants.py
  13. 79
      elab_users/users.py
  14. 6
      htpasswd
  15. 14
      manage.py
  16. 78
      pyproject.toml
  17. 9
      run.py
  18. 39
      test-data/authz
  19. 0
      test-data/authz.original
  20. 2
      test-data/htpasswd
  21. 66
      tests/conftest.py
  22. 386
      tests/test_elab_users.py
  23. 258
      tests/test_elab_users_authz.py
  24. 104
      tests/test_elab_users_users.py
  25. 14
      tox.ini

4
.flake8

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
[flake8]
select = C,E,F,W,S
ignore = E203,W503
per-file-ignores = tests/*:S101

68
.gitignore vendored

@ -0,0 +1,68 @@ @@ -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

38
.pre-commit-config.yaml

@ -0,0 +1,38 @@ @@ -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

4
CHANGES.md

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
0.0.1 - first version
----------------------
- setting up the project

117
CONTRIBUTING.md

@ -0,0 +1,117 @@ @@ -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 <new version>
$ git push
$ git push --tags
$ flit publish

10
LICENSE

@ -0,0 +1,10 @@ @@ -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
* ----------------------------------------------------------------------------
*/

90
Makefile

@ -0,0 +1,90 @@ @@ -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

68
README.md

@ -11,9 +11,7 @@ First of all, we talk about the following files and folders: @@ -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 **. @@ -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:
optional arguments:
-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
to grant a restricted user access to another folder, you have to carefully
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 ...

848
authz

@ -1,848 +0,0 @@ @@ -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 =

206
elab_users/__init__.py

@ -0,0 +1,206 @@ @@ -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)

183
elab_users/authz.py

@ -0,0 +1,183 @@ @@ -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

20
elab_users/constants.py

@ -0,0 +1,20 @@ @@ -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 = ":/"

79
elab_users/users.py

@ -0,0 +1,79 @@ @@ -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)]
)

6
htpasswd

@ -1,6 +0,0 @@ @@ -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

14
manage.py

@ -12,7 +12,7 @@ import sys @@ -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")
@ -286,14 +286,16 @@ if __name__ == "__main__": @@ -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__": @@ -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:

78
pyproject.toml

@ -0,0 +1,78 @@ @@ -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",
]

9
run.py

@ -0,0 +1,9 @@ @@ -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"))

39
test-data/authz

@ -0,0 +1,39 @@ @@ -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

0
authz.original → test-data/authz.original

2
test-data/htpasswd

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
AlexanderDietz:$apr1$n0Oaok6e$wyHcUg6Upm9sE2AoYlVMO/
UrmilShh:$apr1$WxMGE8Wb$H0xWao6KZGqBJoXj7fJ420

66
tests/conftest.py

@ -0,0 +1,66 @@ @@ -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

386
tests/test_elab_users.py

@ -0,0 +1,386 @@ @@ -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

258
tests/test_elab_users_authz.py

@ -0,0 +1,258 @@ @@ -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}

104
tests/test_elab_users_users.py

@ -0,0 +1,104 @@ @@ -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

14
tox.ini

@ -0,0 +1,14 @@ @@ -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
Loading…
Cancel
Save