diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 37e09c5d80234cbb5edacbab5bc92d550772a692..1989e049c4c3c37f7c9ae3f892294716633f36e5 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -5,7 +5,7 @@ * Install python3 and virtualenv * Create a virtualenv and install the requirements -``` +```sh virtualenv -p python3 venv source venv/bin/activate pip install -r requirements.txt @@ -14,7 +14,7 @@ pip install -r requirements.txt * Create a database (with sqlite, postgres, …) * Create a config file -``` +```sh cp config.py.example config.py ``` @@ -25,17 +25,49 @@ cp config.py.example config.py * Fill your database -``` +```sh ./server.py db upgrade ``` ## Running the program -``` +Run (in two terminals, one for the server and one for celery): + +```sh source venv/bin/activate ./server.py runserver ``` +```sh +source venv/bin/activate +./start_celery.sh +``` The website will run on `localhost:5000`. +## Data model + +The data model is defined in `models/database.py`. +Each type should inherit from `DatabaseModel` and have a method `get_parent()`, which is responsible for the right management and some magic. + +## Server endpoints + +The actual websites are defined in `server.py`. They begin with the decorator `@app.route((route)` and return a string, usually through `render_template(template, **parameters)`. +There can be more decorators inbetween `app.route` and the function. +A simple website might look like this: +```python +@app.route("/documentation") +@login_required +def documentation(): + todostates = list(TodoState) + name_to_state = TodoState.get_name_to_state() + return render_template("documentation.html", todostates=todostates, name_to_state=name_to_state) +``` + +### Decorators +* `app.route(route)`: Defines for which URLs this function will be called. + - The route may contain variables: `"/type/edit/<int:protocoltype:id>"`. These will be passed to the function. + - Additionally, allowed HTTP methods may be defined: `@app.route("/type/new", methods=["GET", "POST"])`. The default is `GET`, but endpoints for forms require `POST` as well. +* `login_required`: Anonymous users will be redirected to the login page. +* `group_required(group)`: Users without this group will see an error message. +* `db_lookup(DataModel)`: Looks up an element of this type. The route needs to have an argument "{model_name}_id". diff --git a/auth.py b/auth.py index 58d16a52ca1e9d2b7117d20fc312fb03f7d621de..0323a2fd3d1253fa22e80baeae21ff634cac0eb0 100644 --- a/auth.py +++ b/auth.py @@ -3,6 +3,7 @@ import ssl import ldap3 from ldap3.utils.dn import parse_dn from datetime import datetime +import grp, pwd, pam class User: def __init__(self, username, groups, timestamp=None, obsolete=False, permanent=False): @@ -42,7 +43,7 @@ class UserManager: def login(self, username, password, permanent=False): for backend in self.backends: if backend.authenticate(username, password): - groups = backend.groups(username, password) + groups = sorted(list(set(backend.groups(username, password)))) return User(username, groups, obsolete=backend.obsolete, permanent=permanent) return None @@ -135,6 +136,50 @@ class ADManager: for result in reader.search(): yield result.name.value + +class StaticUserManager: + def __init__(self, users, obsolete=False): + self.passwords = { + username: password + for (username, password, groups) in users + } + self.group_map = { + username: groups + for (username, password, groups) in users + } + self.obsolete = obsolete + + def authenticate(self, username, password): + return (username in self.passwords + and self.passwords[username] == password) + + def groups(self, username, password=None): + if username in self.group_map: + yield from self.group_map[username] + + def all_groups(self): + yield from list(set(group for group in groups.values())) + + +class PAMManager: + def __init__(self, obsolete=False): + self.pam = pam.pam() + self.obsolete = obsolete + + def authenticate(self, username, password): + return self.pam.authenticate(username, password) + + def groups(self, username, password=None): + print(username) + yield grp.getgrgid(pwd.getpwnam(username).pw_gid).gr_name + for group in grp.getgrall(): + if username in group.gr_mem: + yield group.gr_name + + def all_groups(self): + for group in grp.getgrall(): + yield group.gr_name + class SecurityManager: def __init__(self, key, max_duration=300): self.maccer = hmac.new(key.encode("utf-8"), digestmod=hashlib.sha512) diff --git a/config.py.example b/config.py.example index 181aed80df8292e0b7ca85efbeaed493548475d2..ee7824736c7246d08ac2f3248fa6fbbe917d3c30 100644 --- a/config.py.example +++ b/config.py.example @@ -26,10 +26,10 @@ CELERY_ACCEPT_CONTENT = ["pickle"] # do not change PRINTING_ACTIVE = True PRINTING_SERVER = "printsrv.example.com:631" PRINTING_USER = "protocols" -PRINTING_PRINTERS = [ +PRINTING_PRINTERS = { "example_printer": ["Duplex=DuplexNoTumble", "option2=value"], "other_printer": ["list", "of", "options"] -] +} # etherpad (optional) ETHERPAD_ACTIVE = True @@ -62,6 +62,7 @@ SESSION_PROTECTION = "strong" # do not change # authentication SECURITY_KEY = "some other random string" # change this AUTH_MAX_DURATION = 300 +from auth import LdapManager, ADManager, StaticUserManager AUTH_BACKENDS = [ LdapManager( host="ldap.example.com", @@ -72,7 +73,13 @@ AUTH_BACKENDS = [ domain="EXAMPLE", user_dn="cn=users,dc=example,dc=com", group_dn="dc=example,dc=com", - ca_cert="/etc/ssl/certs/example-ca.pem") + ca_cert="/etc/ssl/certs/example-ca.pem"), + StaticUserManager( + users=( + ("username", "password", ("group1", "group2")), + ("testuser", "abc123", ("group1")), + ) + ) ] OBSOLETION_WARNING = """Please migrate your account!""" # not important diff --git a/models/database.py b/models/database.py index 737086e861193920dbb4e9c6c52482549df83f8c..a559fd02e3302651ab9f296ed3bf2ee4d4fed64c 100644 --- a/models/database.py +++ b/models/database.py @@ -701,6 +701,8 @@ class Error(DatabaseModel): return self.protocol def get_short_description(self): + if not self.description: + return "" lines = self.description.splitlines() if len(lines) <= 4: return "\n".join(lines) diff --git a/requirements.txt b/requirements.txt index 0451180e4d287f15e0d721b6187783ea862f0fa4..a2f2947eb39ae44aeb79b476503995bde6c73081 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ MarkupSafe==0.23 nose==1.3.7 packaging==16.8 pathtools==0.1.2 -psycopg2==2.6.2 +psycopg2==2.7.4 pyasn1==0.2.3 Pygments==2.2.0 pyldap==2.4.28 @@ -41,6 +41,7 @@ python-dateutil==2.6.0 python-editor==1.0.3 python-engineio==1.2.2 python-Levenshtein==0.12.0 +python-pam==1.8.2 python-socketio==1.7.1 pytz==2016.10 PyYAML==3.12 diff --git a/tasks.py b/tasks.py index 6dc8366529c90ebe8582bea84a9017ef6b010601..b5c4818af2e6dcfc72cb4b93b0fa3a5b4e5c0e1f 100644 --- a/tasks.py +++ b/tasks.py @@ -637,9 +637,9 @@ def print_file_async(filename, protocol_id): for option in config.PRINTING_PRINTERS[protocol.protocoltype.printer]: command.extend(["-o", '"{}"'.format(option) if " " in option else option]) command.append(filename) - subprocess.check_call(command, universal_newlines=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - except subprocess.SubprocessError: - error = protocol.create_error("Printing", "Printing {} failed.".format(protocol.get_identifier()), "") + subprocess.check_output(command, universal_newlines=True, stderr=subprocess.STDOUT) + except subprocess.SubprocessError as exception: + error = protocol.create_error("Printing", "Printing {} failed.".format(protocol.get_identifier()), exception.stdout) db.session.add(error) db.session.commit() diff --git a/templates/protocol-template.txt b/templates/protocol-template.txt index 57e2c13f8283d1b413d0cdec928e84f24e4908ba..ff2efce96b41e8241c7d48584769a8473535b933 100644 --- a/templates/protocol-template.txt +++ b/templates/protocol-template.txt @@ -1,8 +1,13 @@ #Datum;{{protocol.date|datify_short}} #Beginn;{{protocol.protocoltype.usual_time|timify_short}} #Ende; +{% for meta in protocol.metas %} +#{{meta.name}};{{meta.value}} +{% endfor %} {% for defaultmeta in protocol.protocoltype.metas %} + {% if not defaultmeta.prior %} #{{defaultmeta.key}};{{defaultmeta.value}} + {% endif %} {% endfor %} {% macro render_top(top, use_description=False) %}