From 64d0ac105401de671abb5687f87ccc21dc400720 Mon Sep 17 00:00:00 2001 From: Holger Frey Date: Wed, 25 Aug 2021 12:48:30 +0200 Subject: [PATCH 01/15] added gitignore --- .gitignore | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .gitignore 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 + -- 2.25.1 From 74f9a490fc2e8fd174fc01c8bd83f30936af156d Mon Sep 17 00:00:00 2001 From: Holger Frey Date: Wed, 25 Aug 2021 12:58:22 +0200 Subject: [PATCH 02/15] added the changes of the last working version --- manage.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) 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: -- 2.25.1 From 3b4f956da737c906c27d0ae34d2bcdc4e1c21cc3 Mon Sep 17 00:00:00 2001 From: Holger Frey Date: Wed, 25 Aug 2021 12:59:07 +0200 Subject: [PATCH 03/15] setup of modern project structure with cookiecutter --- .flake8 | 4 ++ .pre-commit-config.yaml | 38 +++++++++++++ CHANGES.md | 4 ++ CONTRIBUTING.md | 117 +++++++++++++++++++++++++++++++++++++++ LICENSE | 10 ++++ Makefile | 90 ++++++++++++++++++++++++++++++ elab_users/__init__.py | 6 ++ pyproject.toml | 75 +++++++++++++++++++++++++ tests/test_elab_users.py | 41 ++++++++++++++ tox.ini | 14 +++++ 10 files changed, 399 insertions(+) create mode 100644 .flake8 create mode 100644 .pre-commit-config.yaml create mode 100644 CHANGES.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 elab_users/__init__.py create mode 100644 pyproject.toml create mode 100644 tests/test_elab_users.py create mode 100644 tox.ini 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/.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/elab_users/__init__.py b/elab_users/__init__.py new file mode 100644 index 0000000..e0219af --- /dev/null +++ b/elab_users/__init__.py @@ -0,0 +1,6 @@ +""" Elab Users + +Manage elab (svn) users +""" + +__version__ = "0.0.1" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c6c0c34 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,75 @@ + + +[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.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/tests/test_elab_users.py b/tests/test_elab_users.py new file mode 100644 index 0000000..ab75af1 --- /dev/null +++ b/tests/test_elab_users.py @@ -0,0 +1,41 @@ +""" 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 + + +def test_example_unittest(): + """ example unittest + + will be run by 'make test' and 'make testall' but not 'make coverage' + """ + assert True + + +@pytest.mark.fun +def test_example_functional_test(): + """ example unittest + + will be by 'make coverage' and 'make testall' but not 'make test' + """ + assert True 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 -- 2.25.1 From 741dcb103208486dde51c127d824ba7af4ea217e Mon Sep 17 00:00:00 2001 From: Holger Frey Date: Wed, 25 Aug 2021 13:00:47 +0200 Subject: [PATCH 04/15] moved test data to separate folder --- authz => test-data/authz | 0 authz.original => test-data/authz.original | 0 htpasswd => test-data/htpasswd | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename authz => test-data/authz (100%) rename authz.original => test-data/authz.original (100%) rename htpasswd => test-data/htpasswd (100%) diff --git a/authz b/test-data/authz similarity index 100% rename from authz rename to test-data/authz 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/htpasswd b/test-data/htpasswd similarity index 100% rename from htpasswd rename to test-data/htpasswd -- 2.25.1 From 6021ceec0e84388ab0aa24de32cac5811ed0e93f Mon Sep 17 00:00:00 2001 From: Holger Frey Date: Wed, 25 Aug 2021 14:42:13 +0200 Subject: [PATCH 05/15] working on modernizing --- elab_users/authz.py | 171 +++++++++++++++++++++++++++++++++++++++ elab_users/constants.py | 24 ++++++ elab_users/users.py | 77 ++++++++++++++++++ manage_scrap.py | 140 ++++++++++++++++++++++++++++++++ run.py | 9 +++ test-data/authz | 2 +- tests/test_elab_users.py | 4 +- 7 files changed, 424 insertions(+), 3 deletions(-) create mode 100644 elab_users/authz.py create mode 100644 elab_users/constants.py create mode 100644 elab_users/users.py create mode 100644 manage_scrap.py create mode 100644 run.py diff --git a/elab_users/authz.py b/elab_users/authz.py new file mode 100644 index 0000000..0db0a3f --- /dev/null +++ b/elab_users/authz.py @@ -0,0 +1,171 @@ +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 = {} + 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._extract_user_info_from_config() + + def write_to_file(self, path): + 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): + print(elab, "-", user_or_group, "-", acl) + 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.append( + belongs_to + ) + elif acl.lower() == READ_ACL: + self.elab_users[user_or_group].read_acl.append( + 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, "@" + group, acl) + self._update_user_group_config() + + 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) + self._update_user_group_config() + + 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..2f6a200 --- /dev/null +++ b/elab_users/constants.py @@ -0,0 +1,24 @@ +from pathlib import Path + +MOUNT_PATH = Path("/mnt") / "nfs-data-store-1" / "drive" +REPO_PATH = MOUNT_PATH / "svn-repository" +AUTHZ_PATH = REPO_PATH / "authz" +HTPWD_PATH = REPO_PATH / ".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..561c1af --- /dev/null +++ b/elab_users/users.py @@ -0,0 +1,77 @@ +import os +import random +import string +import tempfile +import subprocess # noqa: S404 +from pathlib import Path +from datetime import datetime +from dataclasses import dataclass + + +@dataclass +class ElabUser: + name: str + group: str + write_acl = [] + read_acl = [] + + 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 + new_repo = data_dir / self.name + handler.check_call( + ["svnadmin", "create", 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}", 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", 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", in_file, out_file]) + # add and commit the changes + handler.check_call( + "svn", "add", tmpdir / "*", shell=True # noqa: S604 + ) + handler.check_call( + ["svn", "commit", "-m", f"New User: {self.name}", tmpdir] + ) diff --git a/manage_scrap.py b/manage_scrap.py new file mode 100644 index 0000000..c6991e6 --- /dev/null +++ b/manage_scrap.py @@ -0,0 +1,140 @@ +#!/usr/bin/python + +# imports of modules +import optparse +import subprocess +import sys + + + + + + + +if __name__ == "__main__": + # create configparser instance + config = AuthzConfigParser() + # read config file + config.read(AUTHZ_PATH) + + # command line interface: + # no option: display info + # -g display users in a group + # -a add regular user + # -r add restricted user + # -m move to alumni + # -p reset user password + parser = optparse.OptionParser( + usage="usage: %prog [option] name", + description="shows and manipulates svn access rights", + epilog="to grant a restricted user access to another folder, you have to carefully edit the authz file") + parser.add_option("-g", "--groupinfo", action="store_const", dest="what", + const="g", help="display users in a group") + parser.add_option("-a", "--add", action="store_const", dest="what", + const="a", help="add a regular user") + parser.add_option("-r", "--restricted", action="store_const", dest="what", + const="r", help="add a restricted user") + parser.add_option("-m", "--move", action="store_const", dest="what", + const="m", help="move a user to alumni") + parser.add_option("-p", "--password", action="store_const", dest="what", + const="p", help="reset a user password") + options, args = parser.parse_args() + + if len(args)==0: + # no arguments? then display all the users! + groups = config.group_users() + for name, usernames in groups.items(): + print "Users in group '%s':" % name + for name in sorted(usernames): + print " " + name + sys.exit() + + if len(args)>1: + # more than one usename? not here, john boy + sys.exit("please provide only one name") + name = args[0] + + if options.what == "g": + # show group information + groups = config.group_users() + if name not in groups: + sys.exit("Group not found") + print "Users in group '%s':" % name + for usernamename in sorted(groups[name]): + print " " + usernamename + sys.exit() + + 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" % 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 :" + print "username: " + name + print "password: " + password + print "url: https://svn.cpi.imtek.uni-freiburg.de/" + name + config.write_to_file() + sys.exit() + + # from here downwards we need already existent usernames + if name not in config.elab_users: + sys.exit("User '%s' not found, use this without a name to get a list of users." % name) + + if options.what == "m": + # move user to alumni + user = config.elab_users[name] + if user.group == ALUMNI: + sys.exit("User '%s' is already in group '%s'" % (name, ALUMNI)) + if user.group == ADMINS: + sys.exit("User '%s' is in group '%s', will not moved to '%s'" % (name, ADMINS, ALUMNI)) + config.move_user_to_alumni(name) + config.write_to_file() + delete_password(name) + sys.exit() + + if options.what == "p": + # reset a password + password = set_new_password(name) + print "New password for :" + print "username: " + name + print "password: " + password + sys.exit() + + # no option, just a name: + user = config.elab_users[name] + print "User %s is in group '%s':" % (name, 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 = [ username + SVN_SUFFIX for username in user.write_acl ] + print " Write access is granted to '%s'. " % "', '".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 = [ username + SVN_SUFFIX for username in user.read_acl ] + print " Read access is granted to '%s'. " % "', '".join(read_acl) + else: + print " Read access is NOT granted to any journals" + + info = config.get_journal_info(name) + # print the write acls for a journal + print "Labjournal %s%s" % (name, SVN_SUFFIX) + if info[WRITE_ACL]: + print " Write access granted to: " + ", ".join(info[WRITE_ACL]) + else: + print " No write access granted to anybody" + # print the read acls for a journal + if info[READ_ACL]: + print " Read access granted to: " + ", ".join(info[READ_ACL]) + else: + print " No read access granted to anybody" 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 index 616cdb7..b25f3b9 100644 --- a/test-data/authz +++ b/test-data/authz @@ -15,7 +15,7 @@ restricted = BeniPrasser, JuliaSaar, SimonZunker, UrmilShah, YongZhou @users = r @restricted = @alumni = -AlexanderDietz= r +AlexanderDietz= rw [AlexeyKopyshev:/] @administrators= rw diff --git a/tests/test_elab_users.py b/tests/test_elab_users.py index ab75af1..69e728f 100644 --- a/tests/test_elab_users.py +++ b/tests/test_elab_users.py @@ -25,7 +25,7 @@ import pytest def test_example_unittest(): - """ example unittest + """example unittest will be run by 'make test' and 'make testall' but not 'make coverage' """ @@ -34,7 +34,7 @@ def test_example_unittest(): @pytest.mark.fun def test_example_functional_test(): - """ example unittest + """example unittest will be by 'make coverage' and 'make testall' but not 'make test' """ -- 2.25.1 From a5823726223c29a397c6e5abd6e34d499d390979 Mon Sep 17 00:00:00 2001 From: Holger Frey Date: Wed, 25 Aug 2021 16:03:19 +0200 Subject: [PATCH 06/15] completly untested rework --- elab_users/__init__.py | 205 ++++++++++++++++++++++++++++++++++++++++ elab_users/authz.py | 15 ++- elab_users/constants.py | 8 +- manage_scrap.py | 140 --------------------------- pyproject.toml | 3 + 5 files changed, 224 insertions(+), 147 deletions(-) delete mode 100644 manage_scrap.py diff --git a/elab_users/__init__.py b/elab_users/__init__.py index e0219af..e4e08de 100644 --- a/elab_users/__init__.py +++ b/elab_users/__init__.py @@ -4,3 +4,208 @@ 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=".")) + +COMMANDS = ["user", "groups", "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(username, 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("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.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("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) + + +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("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("User {username} not found.") + + user = config.elab_users[username] + print("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, +): + 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() + print(args.command) + print(args.username) + + if not args.command: + list_users(svn_dir, authz) + + if args.command.lower() not in COMMANDS: + show_user_info(args.command, svn_dir, authz) + + if args.command.lower() == "user": + show_user_info(args.name, svn_dir, authz) + + if args.command.lower() == "groups": + show_group_info(args.name, svn_dir, authz) + + if args.command.lower() == "add": + add_new_user(args.name, USERS, svn_dir, authz, htpwd, handler) + + if args.command.lower() == "restricted": + add_new_user(args.name, RESTRICTED, svn_dir, authz, htpwd, handler) + + if args.command.lower() == "retire": + retire_user(args.name, svn_dir, authz, htpwd, handler) + + if 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 index 0db0a3f..1be9963 100644 --- a/elab_users/authz.py +++ b/elab_users/authz.py @@ -31,6 +31,7 @@ class AuthzConfigParser(configparser.ConfigParser): def __init__(self): """initialization of the class""" self.elab_users = {} + self.original_path = None super().__init__() def optionxform(self, value): @@ -40,9 +41,19 @@ class AuthzConfigParser(configparser.ConfigParser): 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() - def write_to_file(self, path): + @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) @@ -141,6 +152,7 @@ class AuthzConfigParser(configparser.ConfigParser): for group, acl in GROUP_DEFAULTS.items(): self.set(journal_path, "@" + 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""" @@ -151,6 +163,7 @@ class AuthzConfigParser(configparser.ConfigParser): for access_to in user.read_acl: self.remove_option(access_to + SVN_SUFFIX, user.name) self._update_user_group_config() + return user def _update_user_group_config(self): """updates the config settings of the groups section""" diff --git a/elab_users/constants.py b/elab_users/constants.py index 2f6a200..fad70dc 100644 --- a/elab_users/constants.py +++ b/elab_users/constants.py @@ -1,9 +1,5 @@ -from pathlib import Path - -MOUNT_PATH = Path("/mnt") / "nfs-data-store-1" / "drive" -REPO_PATH = MOUNT_PATH / "svn-repository" -AUTHZ_PATH = REPO_PATH / "authz" -HTPWD_PATH = REPO_PATH / ".htpasswd" +AUTHZ_FILE_NAME = "authz" +HTPWD_FILE_NAME = ".htpasswd" ADMINS = "administrators" USERS = "users" diff --git a/manage_scrap.py b/manage_scrap.py deleted file mode 100644 index c6991e6..0000000 --- a/manage_scrap.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/python - -# imports of modules -import optparse -import subprocess -import sys - - - - - - - -if __name__ == "__main__": - # create configparser instance - config = AuthzConfigParser() - # read config file - config.read(AUTHZ_PATH) - - # command line interface: - # no option: display info - # -g display users in a group - # -a add regular user - # -r add restricted user - # -m move to alumni - # -p reset user password - parser = optparse.OptionParser( - usage="usage: %prog [option] name", - description="shows and manipulates svn access rights", - epilog="to grant a restricted user access to another folder, you have to carefully edit the authz file") - parser.add_option("-g", "--groupinfo", action="store_const", dest="what", - const="g", help="display users in a group") - parser.add_option("-a", "--add", action="store_const", dest="what", - const="a", help="add a regular user") - parser.add_option("-r", "--restricted", action="store_const", dest="what", - const="r", help="add a restricted user") - parser.add_option("-m", "--move", action="store_const", dest="what", - const="m", help="move a user to alumni") - parser.add_option("-p", "--password", action="store_const", dest="what", - const="p", help="reset a user password") - options, args = parser.parse_args() - - if len(args)==0: - # no arguments? then display all the users! - groups = config.group_users() - for name, usernames in groups.items(): - print "Users in group '%s':" % name - for name in sorted(usernames): - print " " + name - sys.exit() - - if len(args)>1: - # more than one usename? not here, john boy - sys.exit("please provide only one name") - name = args[0] - - if options.what == "g": - # show group information - groups = config.group_users() - if name not in groups: - sys.exit("Group not found") - print "Users in group '%s':" % name - for usernamename in sorted(groups[name]): - print " " + usernamename - sys.exit() - - 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" % 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 :" - print "username: " + name - print "password: " + password - print "url: https://svn.cpi.imtek.uni-freiburg.de/" + name - config.write_to_file() - sys.exit() - - # from here downwards we need already existent usernames - if name not in config.elab_users: - sys.exit("User '%s' not found, use this without a name to get a list of users." % name) - - if options.what == "m": - # move user to alumni - user = config.elab_users[name] - if user.group == ALUMNI: - sys.exit("User '%s' is already in group '%s'" % (name, ALUMNI)) - if user.group == ADMINS: - sys.exit("User '%s' is in group '%s', will not moved to '%s'" % (name, ADMINS, ALUMNI)) - config.move_user_to_alumni(name) - config.write_to_file() - delete_password(name) - sys.exit() - - if options.what == "p": - # reset a password - password = set_new_password(name) - print "New password for :" - print "username: " + name - print "password: " + password - sys.exit() - - # no option, just a name: - user = config.elab_users[name] - print "User %s is in group '%s':" % (name, 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 = [ username + SVN_SUFFIX for username in user.write_acl ] - print " Write access is granted to '%s'. " % "', '".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 = [ username + SVN_SUFFIX for username in user.read_acl ] - print " Read access is granted to '%s'. " % "', '".join(read_acl) - else: - print " Read access is NOT granted to any journals" - - info = config.get_journal_info(name) - # print the write acls for a journal - print "Labjournal %s%s" % (name, SVN_SUFFIX) - if info[WRITE_ACL]: - print " Write access granted to: " + ", ".join(info[WRITE_ACL]) - else: - print " No write access granted to anybody" - # print the read acls for a journal - if info[READ_ACL]: - print " Read access granted to: " + ", ".join(info[READ_ACL]) - else: - print " No read access granted to anybody" diff --git a/pyproject.toml b/pyproject.toml index c6c0c34..1499f67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,9 @@ dev = [ "pre-commit", ] +[tool.flit.scripts] +elab-users = "elab_users:main" + [tool.black] line-length = 79 py37 = true -- 2.25.1 From 9550625e0343339e9808fcac387b5b3a67eab987 Mon Sep 17 00:00:00 2001 From: Holger Frey Date: Mon, 30 Aug 2021 15:20:46 +0200 Subject: [PATCH 07/15] added first tests --- elab_users/authz.py | 11 +- elab_users/users.py | 20 +- test-data/authz | 844 +-------------------------------- test-data/htpasswd | 6 +- tests/conftest.py | 66 +++ tests/test_elab_users_authz.py | 258 ++++++++++ tests/test_elab_users_users.py | 104 ++++ 7 files changed, 459 insertions(+), 850 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/test_elab_users_authz.py create mode 100644 tests/test_elab_users_users.py diff --git a/elab_users/authz.py b/elab_users/authz.py index 1be9963..7dfeb04 100644 --- a/elab_users/authz.py +++ b/elab_users/authz.py @@ -118,19 +118,16 @@ class AuthzConfigParser(configparser.ConfigParser): 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): - print(elab, "-", user_or_group, "-", acl) 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.append( + self.elab_users[user_or_group].write_acl.add( belongs_to ) elif acl.lower() == READ_ACL: - self.elab_users[user_or_group].read_acl.append( - belongs_to - ) + 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""" @@ -150,7 +147,7 @@ class AuthzConfigParser(configparser.ConfigParser): self.add_section(journal_path) self.set(journal_path, username, WRITE_ACL) for group, acl in GROUP_DEFAULTS.items(): - self.set(journal_path, "@" + group, acl) + self.set(journal_path, f"@{group}", acl) self._update_user_group_config() return self.elab_users[username] @@ -162,6 +159,8 @@ class AuthzConfigParser(configparser.ConfigParser): 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 diff --git a/elab_users/users.py b/elab_users/users.py index 561c1af..1a83c77 100644 --- a/elab_users/users.py +++ b/elab_users/users.py @@ -3,17 +3,18 @@ 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 dataclass +from dataclasses import field, dataclass @dataclass class ElabUser: name: str group: str - write_acl = [] - read_acl = [] + write_acl: Set = field(default_factory=set) + read_acl: Set = field(default_factory=set) def __str__(self): """return a string representation""" @@ -43,16 +44,17 @@ class ElabUser: 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", new_repo], stderr=handler.STDOUT + ["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}", tmpdir] + ["svn", "checkout", f"file://{new_repo}", str(tmpdir)] ) # create subfolders today = datetime.now() @@ -61,17 +63,17 @@ class ElabUser: for month in range(today.month, 13): month_path = year_path / f"{month:0>2}" month_path.mkdir() - handler.check_call(["touch", month_path / ".empty"]) + 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", in_file, out_file]) + handler.check_call(["cp", str(in_file), str(out_file)]) # add and commit the changes handler.check_call( - "svn", "add", tmpdir / "*", shell=True # noqa: S604 + ["svn", "add", str(tmpdir / "*")], shell=True # noqa: S604 ) handler.check_call( - ["svn", "commit", "-m", f"New User: {self.name}", tmpdir] + ["svn", "commit", "-m", f"New User: {self.name}", str(tmpdir)] ) diff --git a/test-data/authz b/test-data/authz index b25f3b9..b862eec 100644 --- a/test-data/authz +++ b/test-data/authz @@ -4,845 +4,29 @@ alumni = AlexeyKopyshev, AndreasBoenisch, AndreasEver, AnkeWoerz, AnneLoesche, A 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 +@administrators = rw @users = r -@restricted = -@alumni = -AlexanderDietz= rw +@restricted = +@alumni = +AlexanderDietz = rw [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 +@administrators = rw @users = r -@restricted = -@alumni = +@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 +@administrators = rw @users = r -@restricted = -@alumni = +@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 +@administrators = rw @users = r -@restricted = -@alumni = +@restricted = +@alumni = +UrmilShah = rw \ No newline at end of file diff --git a/test-data/htpasswd b/test-data/htpasswd index af66e5c..5ea70aa 100644 --- a/test-data/htpasswd +++ b/test-data/htpasswd @@ -1,6 +1,2 @@ -foo:$apr1$SzJRyvJU$U3luHwCA6xHfKowizE.Gl. -FOO:$apr1$LSPDdLqg$tiGbDGgNEXcRA/oyadYSw1 -AndreasEvers:$apr1$n0Oaok6e$wyHcUg6Upm9sE2AoYlVMO/ -FOOBar:$apr1$pZCbClF5$smEDwhMJIVmPsNmMEkRPd1 -FooBar:$apr1$24r9zF2e$9q30fNOqSlvn6itdhZMpc1 +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_authz.py b/tests/test_elab_users_authz.py new file mode 100644 index 0000000..cf25f88 --- /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", [], []), + ("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..aac2324 --- /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[:2] == ["svn", "add"] + assert called.args[2].startswith("/tmp/") # noqa: S108 + assert called.args[2].endswith("/*") + assert called.kargs == {"shell": True} + + 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 -- 2.25.1 From 2c22a4539c71661356933dca6f1d51ee15689105 Mon Sep 17 00:00:00 2001 From: Holger Frey Date: Mon, 30 Aug 2021 17:09:44 +0200 Subject: [PATCH 08/15] added most tests --- elab_users/__init__.py | 35 ++-- test-data/authz | 9 +- tests/test_elab_users.py | 365 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 378 insertions(+), 31 deletions(-) diff --git a/elab_users/__init__.py b/elab_users/__init__.py index e4e08de..e329ef4 100644 --- a/elab_users/__init__.py +++ b/elab_users/__init__.py @@ -36,7 +36,7 @@ def get_config(svn_dir=SVN_REPOS_PATH, authz=AUTHZ_FILE_NAME): return AuthzConfigParser.from_file(authz_path) -def list_users(username, svn_dir=SVN_REPOS_PATH, authz=AUTHZ_FILE_NAME): +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() @@ -52,7 +52,7 @@ def show_group_info(groupname, svn_dir=SVN_REPOS_PATH, authz=AUTHZ_FILE_NAME): groups = config.group_users() if groupname not in groups: sys.exit(f"Group '{groupname}' not found in authz file") - print("Users in group '{groupname}':") + print(f"Users in group '{groupname}':") for name in sorted(groups[groupname]): print(f" {name}") @@ -69,7 +69,7 @@ def add_new_user( 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.lower() for i in svn_dir.iterdir()}: + 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) @@ -104,6 +104,7 @@ def retire_user( 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( @@ -129,7 +130,7 @@ def show_user_info(username, svn_dir=SVN_REPOS_PATH, authz=AUTHZ_FILE_NAME): sys.exit("User {username} not found.") user = config.elab_users[username] - print("User {user.name} is in group '{user.group}':") + print(f"User {user.name} is in group '{user.group}':") # print the write acls for a user if user.group == ADMINS: @@ -172,6 +173,7 @@ def main( authz=AUTHZ_FILE_NAME, htpwd=HTPWD_FILE_NAME, handler=subprocess, + cli_args=None, ): parser = argparse.ArgumentParser(prog="elab-users") parser.add_argument( @@ -182,30 +184,23 @@ def main( parser.add_argument( "name", nargs="?", help="user or group to perform the command on" ) - args = parser.parse_args() + args = parser.parse_args(cli_args) print(args.command) - print(args.username) + print(args.name) if not args.command: list_users(svn_dir, authz) - - if args.command.lower() not in COMMANDS: + elif args.command.lower() not in COMMANDS: show_user_info(args.command, svn_dir, authz) - - if args.command.lower() == "user": + elif args.command.lower() == "user": show_user_info(args.name, svn_dir, authz) - - if args.command.lower() == "groups": + elif args.command.lower() == "groups": show_group_info(args.name, svn_dir, authz) - - if args.command.lower() == "add": + elif args.command.lower() == "add": add_new_user(args.name, USERS, svn_dir, authz, htpwd, handler) - - if args.command.lower() == "restricted": + elif args.command.lower() == "restricted": add_new_user(args.name, RESTRICTED, svn_dir, authz, htpwd, handler) - - if args.command.lower() == "retire": + elif args.command.lower() == "retire": retire_user(args.name, svn_dir, authz, htpwd, handler) - - if args.command.lower() == "password": + elif args.command.lower() == "password": change_password(args.name, svn_dir, authz, htpwd, handler) diff --git a/test-data/authz b/test-data/authz index b862eec..920cdda 100644 --- a/test-data/authz +++ b/test-data/authz @@ -1,6 +1,6 @@ [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 +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 @@ -24,6 +24,13 @@ AlexanderDietz = rw @alumni = UrmilShah = r +[OswaldPrucker:/] +@administrators = rw +@users = r +@restricted = +@alumni = +OswaldPrucker = rw + [UrmilShah:/] @administrators = rw @users = r diff --git a/tests/test_elab_users.py b/tests/test_elab_users.py index 69e728f..bc7a43e 100644 --- a/tests/test_elab_users.py +++ b/tests/test_elab_users.py @@ -23,19 +23,364 @@ 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 -def test_example_unittest(): - """example unittest + with pytest.raises(SystemExit): + add_new_user( + "authz", + "users", + example_authz.parent, + example_authz.name, + example_htpasswd.name, + handler=stub_handler, + ) - will be run by 'make test' and 'make testall' but not 'make coverage' - """ - assert True + +@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_example_functional_test(): - """example unittest +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"), + (["groups", "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() - will be by 'make coverage' and 'make testall' but not 'make test' - """ - assert True + assert "usage: elab-users [-h] [command] [name]" in captured.out -- 2.25.1 From 43ce6de0968e1d1e632c7e8eceb03b4eb430e75b Mon Sep 17 00:00:00 2001 From: Holger Frey Date: Mon, 30 Aug 2021 17:21:45 +0200 Subject: [PATCH 09/15] fixed a bug in a test --- tests/test_elab_users_authz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_elab_users_authz.py b/tests/test_elab_users_authz.py index cf25f88..5bfbadb 100644 --- a/tests/test_elab_users_authz.py +++ b/tests/test_elab_users_authz.py @@ -143,7 +143,7 @@ def test_authz_parser_extract_group_definitions(name, group, example_authz): @pytest.mark.parametrize( "name, read, write", [ - ("OswaldPrucker", [], []), + ("OswaldPrucker", [], ["OswaldPrucker"]), ("AlexanderDietz", [], ["AlexanderDietz"]), ("UrmilShah", ["AndreasEvers"], ["UrmilShah"]), ], -- 2.25.1 From 87ccb771e7c1844c9261e814c3eaa185e593b7ed Mon Sep 17 00:00:00 2001 From: Holger Frey Date: Mon, 30 Aug 2021 17:28:19 +0200 Subject: [PATCH 10/15] made SVN_REPOS_PATH resolve to an absolute path --- elab_users/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elab_users/__init__.py b/elab_users/__init__.py index e329ef4..de24c29 100644 --- a/elab_users/__init__.py +++ b/elab_users/__init__.py @@ -24,7 +24,7 @@ from .constants import ( HTPWD_FILE_NAME, ) -SVN_REPOS_PATH = Path(os.getenv("SVN_REPOS_PATH", default=".")) +SVN_REPOS_PATH = Path(os.getenv("SVN_REPOS_PATH", default=".")).resolve() COMMANDS = ["user", "groups", "add", "restricted", "retire", "password"] -- 2.25.1 From a7a6f7e76ed83d9a23a6b67cc256745beace001a Mon Sep 17 00:00:00 2001 From: Holger Frey Date: Mon, 30 Aug 2021 17:35:23 +0200 Subject: [PATCH 11/15] fixing svn add error --- elab_users/users.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/elab_users/users.py b/elab_users/users.py index 1a83c77..737d721 100644 --- a/elab_users/users.py +++ b/elab_users/users.py @@ -71,6 +71,8 @@ class ElabUser: out_file = tmpdir / filename handler.check_call(["cp", str(in_file), str(out_file)]) # add and commit the changes + print(tmpdir) + print(list(tmpdir.iterdir())) handler.check_call( ["svn", "add", str(tmpdir / "*")], shell=True # noqa: S604 ) -- 2.25.1 From 25d7842c78ec573a4d8d5e9fcf30b73aac43b882 Mon Sep 17 00:00:00 2001 From: Ansible Deploy Date: Mon, 30 Aug 2021 15:44:43 +0000 Subject: [PATCH 12/15] fixed adding elab users --- elab_users/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elab_users/users.py b/elab_users/users.py index 737d721..9ee4f89 100644 --- a/elab_users/users.py +++ b/elab_users/users.py @@ -74,7 +74,7 @@ class ElabUser: print(tmpdir) print(list(tmpdir.iterdir())) handler.check_call( - ["svn", "add", str(tmpdir / "*")], shell=True # noqa: S604 + ["svn", "add", "--force", str(tmpdir) + "/"] # noqa: S604 ) handler.check_call( ["svn", "commit", "-m", f"New User: {self.name}", str(tmpdir)] -- 2.25.1 From c9a9a5b534bffe39ee7de4cfc9ef1363b772559e Mon Sep 17 00:00:00 2001 From: Ansible Deploy Date: Mon, 30 Aug 2021 15:47:41 +0000 Subject: [PATCH 13/15] fixed f-strings --- elab_users/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/elab_users/__init__.py b/elab_users/__init__.py index de24c29..ae0af7c 100644 --- a/elab_users/__init__.py +++ b/elab_users/__init__.py @@ -90,7 +90,7 @@ def retire_user( ): config = get_config(svn_dir, authz) if username not in config.elab_users: - sys.exit("User {username} not found.") + 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}'") @@ -116,7 +116,7 @@ def change_password( ): config = get_config(svn_dir, authz) if username not in config.elab_users: - sys.exit("User {username} not found.") + 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 :") @@ -127,7 +127,7 @@ def change_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("User {username} not found.") + sys.exit(f"User {username} not found.") user = config.elab_users[username] print(f"User {user.name} is in group '{user.group}':") -- 2.25.1 From 61150fdf7e8d1850c58f56118b885abcdd3a558d Mon Sep 17 00:00:00 2001 From: Ansible Deploy Date: Mon, 30 Aug 2021 15:52:40 +0000 Subject: [PATCH 14/15] fixed tests for fixed svn add --- elab_users/users.py | 2 -- tests/test_elab_users_users.py | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/elab_users/users.py b/elab_users/users.py index 9ee4f89..34ca518 100644 --- a/elab_users/users.py +++ b/elab_users/users.py @@ -71,8 +71,6 @@ class ElabUser: out_file = tmpdir / filename handler.check_call(["cp", str(in_file), str(out_file)]) # add and commit the changes - print(tmpdir) - print(list(tmpdir.iterdir())) handler.check_call( ["svn", "add", "--force", str(tmpdir) + "/"] # noqa: S604 ) diff --git a/tests/test_elab_users_users.py b/tests/test_elab_users_users.py index aac2324..2567424 100644 --- a/tests/test_elab_users_users.py +++ b/tests/test_elab_users_users.py @@ -93,10 +93,10 @@ def test_elabuser_create_new_repo(stub_handler): called = stub_handler.stack[-2] assert called.func == "check_call" - assert called.args[:2] == ["svn", "add"] - assert called.args[2].startswith("/tmp/") # noqa: S108 - assert called.args[2].endswith("/*") - assert called.kargs == {"shell": True} + 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" -- 2.25.1 From 0a14f0207c36fed31ca27a08279a64e9ad8a1597 Mon Sep 17 00:00:00 2001 From: Holger Frey Date: Tue, 31 Aug 2021 10:08:38 +0200 Subject: [PATCH 15/15] updated readme to new commands --- README.md | 72 +++++++++++++++++----------------------- elab_users/__init__.py | 4 +-- tests/test_elab_users.py | 2 +- 3 files changed, 34 insertions(+), 44 deletions(-) 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/elab_users/__init__.py b/elab_users/__init__.py index ae0af7c..be1b041 100644 --- a/elab_users/__init__.py +++ b/elab_users/__init__.py @@ -26,7 +26,7 @@ from .constants import ( SVN_REPOS_PATH = Path(os.getenv("SVN_REPOS_PATH", default=".")).resolve() -COMMANDS = ["user", "groups", "add", "restricted", "retire", "password"] +COMMANDS = ["user", "group", "add", "restricted", "retire", "password"] def get_config(svn_dir=SVN_REPOS_PATH, authz=AUTHZ_FILE_NAME): @@ -194,7 +194,7 @@ def main( 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() == "groups": + 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) diff --git a/tests/test_elab_users.py b/tests/test_elab_users.py index bc7a43e..570010d 100644 --- a/tests/test_elab_users.py +++ b/tests/test_elab_users.py @@ -345,7 +345,7 @@ def test_show_user_info_error_unknown_user(example_authz): ([], "Users in group 'restricted':"), (["OswaldPrucker"], "granted to all journals"), (["user", "OswaldPrucker"], "granted to all journals"), - (["groups", "alumni"], "Users in group 'alumni':"), + (["group", "alumni"], "Users in group 'alumni':"), (["add", "JaneDoe"], "url:"), (["restricted", "JaneDoe"], "url:"), (["retire", "AlexanderDietz"], "to alumni"), -- 2.25.1