Commit 50e1f117 authored by Markus Scheller's avatar Markus Scheller

Merge branch 'master' into 172-dokumentation-ueberarbeiten

parents 7c9c3829 23b68036
FLASK_APP=server.py:app
FLASK_ENV=development
[submodule "common"]
path = common
url = git@git.fsmpi.rwth-aachen.de:protokollsystem/common-web.git
import hmac
import hashlib
import ssl
from datetime import datetime
class User:
def __init__(self, username, groups, all_groups, timestamp=None,
obsolete=False, permanent=False):
self.username = username
self.groups = groups
self.all_groups = all_groups
if timestamp is not None:
self.timestamp = timestamp
else:
self.timestamp = datetime.now()
self.obsolete = obsolete
self.permanent = permanent
def summarize(self):
return ":".join((
self.username, ",".join(self.groups), ",".join(self.all_groups),
str(self.timestamp.timestamp()), str(self.obsolete),
str(self.permanent)))
@staticmethod
def from_summary(summary):
parts = summary.split(":", 5)
if len(parts) != 6:
return None
(name, group_str, all_group_str, timestamp_str, obsolete_str,
permanent_str) = parts
timestamp = datetime.fromtimestamp(float(timestamp_str))
obsolete = obsolete_str == "True"
groups = group_str.split(",")
all_groups = group_str.split(",")
permanent = permanent_str == "True"
return User(name, groups, all_groups, timestamp, obsolete, permanent)
@staticmethod
def from_hashstring(secure_string):
summary, hash = secure_string.split("=", 1)
return User.from_summary(summary)
class UserManager:
def __init__(self, backends):
self.backends = backends
def login(self, username, password, permanent=False):
for backend in self.backends:
if backend.authenticate(username, password):
groups = sorted(list(set(backend.groups(username, password))))
all_groups = sorted(list(set(backend.all_groups(
username, password))))
return User(
username, groups, all_groups, obsolete=backend.obsolete,
permanent=permanent)
return None
class SecurityManager:
def __init__(self, key, max_duration=300):
self.maccer = hmac.new(key.encode("utf-8"), digestmod=hashlib.sha512)
self.max_duration = max_duration
def hash_user(self, user):
maccer = self.maccer.copy()
summary = user.summarize()
maccer.update(summary.encode("utf-8"))
return "{}={}".format(summary, maccer.hexdigest())
def check_user(self, string):
parts = string.split("=", 1)
if len(parts) != 2:
# wrong format, expecting summary:hash
return False
summary, hash = map(lambda s: s.encode("utf-8"), parts)
maccer = self.maccer.copy()
maccer.update(summary)
user = User.from_hashstring(string)
if user is None:
return False
session_duration = datetime.now() - user.timestamp
macs_equal = hmac.compare_digest(
maccer.hexdigest().encode("utf-8"), hash)
time_short = int(session_duration.total_seconds()) < self.max_duration
return macs_equal and (time_short or user.permanent)
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, username, password):
yield from list(set(group for group in self.group_map.values()))
try:
import ldap3
class LdapManager:
def __init__(self, host, user_dn, group_dn, port=636, use_ssl=True,
obsolete=False):
self.server = ldap3.Server(host, port=port, use_ssl=use_ssl)
self.user_dn = user_dn
self.group_dn = group_dn
self.obsolete = obsolete
def authenticate(self, username, password):
try:
connection = ldap3.Connection(
self.server, self.user_dn.format(username), password)
return connection.bind()
except ldap3.core.exceptions.LDAPSocketOpenError:
return False
def groups(self, username, password=None):
connection = ldap3.Connection(self.server)
obj_def = ldap3.ObjectDef("posixgroup", connection)
group_reader = ldap3.Reader(connection, obj_def, self.group_dn)
username = username.lower()
for group in group_reader.search():
members = group.memberUid.value
if members is not None and username in members:
yield group.cn.value
def all_groups(self, username, password):
connection = ldap3.Connection(self.server)
obj_def = ldap3.ObjectDef("posixgroup", connection)
group_reader = ldap3.Reader(connection, obj_def, self.group_dn)
for group in group_reader.search():
yield group.cn.value
class ADManager:
def __init__(self, host, domain, user_dn, group_dn,
port=636, use_ssl=True, ca_cert=None, obsolete=False):
tls_config = ldap3.Tls(validate=ssl.CERT_REQUIRED)
if ca_cert is not None:
tls_config = ldap3.Tls(
validate=ssl.CERT_REQUIRED, ca_certs_file=ca_cert)
self.server = ldap3.Server(
host, port=port, use_ssl=use_ssl, tls=tls_config)
self.domain = domain
self.user_dn = user_dn
self.group_dn = group_dn
self.obsolete = obsolete
def prepare_connection(self, username=None, password=None):
if username is not None and password is not None:
ad_user = "{}\\{}".format(self.domain, username)
return ldap3.Connection(self.server, ad_user, password)
return ldap3.Connection(self.server)
def authenticate(self, username, password):
try:
return self.prepare_connection(username, password).bind()
except ldap3.core.exceptions.LDAPSocketOpenError:
return False
def groups(self, username, password):
connection = self.prepare_connection(username, password)
if not connection.bind():
return
obj_def = ldap3.ObjectDef("user", connection)
name_filter = "cn:={}".format(username)
user_reader = ldap3.Reader(
connection, obj_def, self.user_dn, name_filter)
group_def = ldap3.ObjectDef("group", connection)
all_group_reader = ldap3.Reader(
connection, group_def, self.group_dn)
all_groups = {
group.primaryGroupToken.value: group
for group in all_group_reader.search()
}
def _yield_recursive_groups(group_dn):
group_reader = ldap3.Reader(
connection, group_def, group_dn)
for entry in group_reader.search():
yield entry.name.value
for child in entry.memberOf:
yield from _yield_recursive_groups(child)
for result in user_reader.search():
yield from _yield_recursive_groups(
all_groups[result.primaryGroupID.value]
.distinguishedName.value)
for group_dn in result.memberOf:
yield from _yield_recursive_groups(group_dn)
def all_groups(self, username, password):
connection = self.prepare_connection(username, password)
if not connection.bind():
return
obj_def = ldap3.ObjectDef("group", connection)
group_reader = ldap3.Reader(connection, obj_def, self.group_dn)
for result in group_reader.search():
yield result.name.value
except ImportError:
pass
try:
import grp
import pwd
import pam
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):
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, username, password):
for group in grp.getgrall():
yield group.gr_name
except ImportError:
pass
# This snippet is in public domain.
# However, please retain this link in your sources:
# http://flask.pocoo.org/snippets/120/
# Danya Alexeyevsky
import functools
from flask import session, request, redirect as flask_redirect, url_for
import config
cookie = getattr(config, "REDIRECT_BACK_COOKIE", "back")
default_view = getattr(config, "REDIRECT_BACK_DEFAULT", "index")
def anchor(func, cookie=cookie):
@functools.wraps(func)
def result(*args, **kwargs):
session[cookie] = request.url
return func(*args, **kwargs)
return result
def default_url(default, **url_args):
return url_for(default, **url_args)
def url(default=default_view, cookie=cookie, **url_args):
return session.get(cookie, default_url(default, **url_args))
def redirect(default=default_view, cookie=cookie, **url_args):
print(request.url, request.url_rule, default, session.get(cookie))
target = url(default, cookie, **url_args)
if target == request.url:
target = default_url(default, **url_args)
return flask_redirect(target)
......@@ -5,7 +5,7 @@ import quopri
from caldav import DAVClient
from vobject.base import ContentLine
import config
from shared import config
class CalendarException(Exception):
......
Subproject commit e9e79e088d711e5243d30a1f9b9adc28f91c793d
# (local) database
SQLALCHEMY_DATABASE_URI = "postgresql://user:password@host/database" # change this
#SQLALCHEMY_DATABASE_URI = "mysql://user:password@host/database"
#SQLALCHEMY_DATABASE_URI = "sqlite:///path/to/database.db"
SQLALCHEMY_TRACK_MODIFICATIONS = False # do not change
SECRET_KEY = "something random" # change this
SERVER_NAME = "protokolle.example.com"
PREFERRED_URL_SCHEME = "https" # change to http for development
DEBUG = False # do not change
# mailserver (optional)
MAIL_ACTIVE = True
MAIL_FROM = "protokolle@example.com"
MAIL_HOST = "mail.example.com:465"
MAIL_USER = "user" # set to "" for unauthenticated sending
MAIL_PASSWORD = "password" # set to "" for unauthenticated sending
MAIL_USE_TLS = True # should match the port in MAIL_HOST (if present there)
MAIL_USE_STARTTLS = False # Usually, it's either this or SMTPS, not both
# (local) message queue (necessary)
CELERY_BROKER_URL = "redis://localhost:6379/0" # change this if you do not use redis or it is running somewhere else
CELERY_TASK_SERIALIZER = "pickle" # do not change
CELERY_ACCEPT_CONTENT = ["pickle"] # do not change
# Send exceptions to sentry (optional)
# SENTRY_DSN = "https://********:********@sentry.example.com//1"
# CUPS printserver (optional)
PRINTING_ACTIVE = True
PRINTING_SERVER = "printsrv.example.com:631"
PRINTING_USER = "protocols"
PRINTING_PRINTERS = {
"example_printer": ["Duplex=DuplexNoTumble", "option2=value"],
"other_printer": ["list", "of", "options"]
}
# etherpad (optional)
ETHERPAD_ACTIVE = True
ETHERPAD_URL = "https://example.com/etherpad" # without /p/…
EMPTY_ETHERPAD = """Welcome to Etherpad!
This pad text is synchronized as you type, so that everyone viewing this page sees the same text. This allows you to collaborate seamlessly on documents!
Get involved with Etherpad at http://etherpad.org
""" # do not change
# wiki (optional)
WIKI_ACTIVE = True
WIKI_TYPE = "MEDIAWIKI"
WIKI_API_URL = "https://wiki.example.com/wiki/api.php"
WIKI_ANONYMOUS = False
WIKI_USER = "user"
WIKI_PASSWORD = "password"
WIKI_DOMAIN = "domain" # set to None if not necessary
# CalDAV calendar (optional)
CALENDAR_ACTIVE = True
CALENDAR_URL = "https://user:password@calendar.example.com/dav/"
CALENDAR_DEFAULT_DURATION = 3 # default meeting length in hours
CALENDAR_MAX_REQUESTS = 10 # number of retries before giving up (some caldav servers like to randomly reply with errors)
CALENDAR_TIMEZONE_MAP = {
"CET": "Europe/Berlin",
"CEST": "Europe/Berlin",
}
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",
user_dn="uid={},ou=users,dc=example,dc=com",
group_dn="dc=example,dc=com"),
ADManager(
host="ad.example.com",
domain="EXAMPLE",
user_dn="cn=users,dc=example,dc=com",
group_dn="dc=example,dc=com",
ca_cert="/etc/ssl/certs/example-ca.pem"),
StaticUserManager(
users=(
("username", "password", ("group1", "group2")),
("testuser", "abc123", ("group1",)),
)
),
PAMManager(),
]
OBSOLETION_WARNING = """Please migrate your account!""" # not important
# lines of error description
ERROR_CONTEXT_LINES = 3
# pagination
PAGE_LENGTH = 20
PAGE_DIFF = 3
# upcoming meetings within this number of days from today are shown on the index page
MAX_INDEX_DAYS = 14
MAX_PAST_INDEX_DAYS = 2
MAX_PAST_INDEX_DAYS_BEFORE_REMINDER = 14
# mail to contact in case of complex errors
ADMIN_MAIL = "admin@example.com"
# users with this group may see and do everything
ADMIN_GROUP = "admin"
# accept protocols even with some errors
# useful for importing old protocols
# not recommended for regular operation
PARSER_LAZY = False
# minimum similarity (0-100) todos need to have to be considered equal while importing
FUZZY_MIN_SCORE = 90
# choose something nice from fc-list
# Nimbus Sans looks very much like Computer Modern
FONTS = {
"main": {
"extension": ".otf",
"path": "/usr/share/fonts/OTF/",
"regular": "NimbusSans-Regular",
"bold": "NimbusSans-Bold",
"italic": "NimbusSans-Oblique",
"bolditalic": "NimbusSans-BoldOblique"
},
"roman": {
"extension": ".otf",
"path": "/usr/share/fonts/OTF/",
"regular": "NimbusRoman-Regular",
"bold": "NimbusRoman-Bold",
"italic": "NimbusRoman-Italic",
"bolditalic": "NimbusRoman-BoldItalic"
},
"sans": {
"extension": ".otf",
"path": "/usr/share/fonts/OTF/",
"regular": "NimbusSans-Regular",
"bold": "NimbusSans-Bold",
"italic": "NimbusSans-Oblique",
"bolditalic": "NimbusSans-BoldOblique"
},
"mono": {
"extension": ".otf",
"path": "/usr/share/fonts/OTF/",
"regular": "NimbusMonoPS-Regular",
"bold": "NimbusMonoPS-Bold",
"italic": "NimbusMonoPS-Italic",
"bolditalic": "NimbusMonoPS-BoldItalic"
}
}
# local filesystem path to save compiled and uploaded protocols (and attachments)
# create this path!
DOCUMENTS_PATH = "documents"
# keywords indicating private protocol parts
PRIVATE_KEYWORDS = ["private", "internal", "privat", "intern"]
# list of bulletpoints to use in latex
# these are latex-defaults, add more if you like more
# they are cycled as often as necessary to allow (theoretically) infinite nesting depth
LATEX_BULLETPOINTS = [
r"\textbullet",
r"\normalfont \bfseries \textendash",
r"\textasteriskcentered",
r"\textperiodcentered"
]
# optional: path to additional jinja-templates, will be need in combination with LATEX_TEMPLATES
#LATEX_LOCAL_TEMPLATES = "local-templates"
# optional: the template to include at the top of protocol.tex
#LATEX_LOGO_TEMPLATE = "asta-logo.tex"
# optional: custom protocol page geometry
#LATEX_GEOMETRY = "bottom=1.6cm,top=1.6cm,inner=2.5cm,outer=1.0cm,footskip=1.0cm,headsep=0.6cm"
# optional: custom protocol pagestyle
#LATEX_PAGESTYLE = "fancy"
# optional: additional latex packages
#LATEX_ADDITIONAL_PACKAGES = ["[absolute]{textpos}", "{fancyheadings}"]
# optional: include header and footer in asta-style, not just a page number on top
#LATEX_HEADER_FOOTER = True
# optional: define multiple LaTeX-templates to use with a each protocol type individually overiding the general LATEX options
# the LATEX_LOCAL_TEMPLATES parameter is need to provide the path for the templates
# each template must be placed in an individual folder named by its ID in LATEX_TEMPLATES and must contain the provided template files: e.g.
# - the files for the template "yourtemplate" need to be in the folder named "yourtemplate"
# - the templates provides the files: "protokoll2.cls" (class), "protocol.tex" (protocol), "decision.tex" (decision) and "asta-logo.tex"
#LATEX_TEMPLATES = {
# "yourtemplate": {
# "name": "Dein Template",
# "provides": ["class", "protocol", "decision"], # optional: if this option is set the corresponding files must be provided
# "logo": "asta-logo.tex", # optional: replaces the general template to include at the top of protocol.tex set by LATEX_LOGO_TEMPLATE
# "geometry": "bottom=1.6cm,top=1.6cm,inner=2.5cm,outer=1.0cm,footskip=1.0cm,headsep=0.6cm", # optional: replaces the general protocol page geometry set by LATEX_GEOMETRY
# "pagestyle": "fancy", # optional: replaces the general protocol pagestyle set by LATEX_PAGESTYLE
# "additional_packages": ["[absolute]{textpos}", "{fancyheadings}"], # optional: replaces the general latex packages set by LATEX_ADDITIONAL_PACKAGES
# "headerfooter": True # optional: replaces the general LATEX_HEADER_FOOTER option
# }
#}
HTML_LEVEL_OFFSET = 3
def dummy_todomail_provider():
return {"example": ("Name", "mail@example.com")}
# if you want to generate this mapping automatically
# manually creating todomails through the web interface will still be possible for every authenticated user
# list of functions that return dicts mapping todomail-keys to a tuple containing name and mail address
ADDITIONAL_TODOMAIL_PROVIDERS = [
dummy_todomail_provider
]
This diff is collapsed.
from flask import request, flash, abort
from functools import wraps
from hmac import compare_digest
from flask import flash
from models.database import ALL_MODELS
from shared import current_user
from utils import get_csrf_token
import back
ID_KEY = "id"
KEY_NOT_PRESENT_MESSAGE = "Missing {}_id."
OBJECT_DOES_NOT_EXIST_MESSAGE = "There is no {} with id {}."
MISSING_VIEW_RIGHT = "Dir fehlenden die nötigen Zugriffsrechte."
from common import back
def default_redirect():
......@@ -23,29 +15,7 @@ def login_redirect():
return back.redirect("login")
def db_lookup(*models, check_exists=True):
def _decorator(function):
@wraps(function)
def _decorated_function(*args, **kwargs):
for model in models:
key = model.__model_name__
id_key = "{}_{}".format(key, ID_KEY)
if id_key not in kwargs:
flash(KEY_NOT_PRESENT_MESSAGE.format(key), "alert-error")
return default_redirect()
obj_id = kwargs[id_key]
obj = model.query.filter_by(id=obj_id).first()
if check_exists and obj is None:
model_name = model.__class__.__name__
flash(OBJECT_DOES_NOT_EXIST_MESSAGE.format(
model_name, obj_id),
"alert-error")
return default_redirect()
kwargs[key] = obj
kwargs.pop(id_key)
return function(*args, **kwargs)
return _decorated_function
return _decorator
MISSING_VIEW_RIGHT = "Dir fehlenden die nötigen Zugriffsrechte."
def require_right(right, require_exist):
......@@ -92,14 +62,3 @@ def require_publish_right(require_exist=True):
def require_admin_right(require_exist=True):
return require_right("admin", require_exist)
def protect_csrf(function):
@wraps(function)
def _decorated_function(*args, **kwargs):
token = request.args.get("csrf_token")
true_token = get_csrf_token()
if token is None or not compare_digest(token, true_token):
abort(400)
return function(*args, **kwargs)
return _decorated_function
......@@ -4,7 +4,7 @@ from fuzzywuzzy import process
from models.database import OldTodo, Protocol, ProtocolType, TodoMail
from shared import db
import config
from shared import config
def lookup_todo_id(old_candidates, new_who, new_description):
......
......@@ -7,7 +7,7 @@ from uuid import uuid4
from shared import (
db, date_filter_short, escape_tex, DATE_KEY, START_TIME_KEY, END_TIME_KEY,
current_user)
current_user, config)
from utils import get_etherpad_url, split_terms, check_ip_in_networks
from models.errors import DateNotMatchingException
from dateutil import tz
......@@ -17,7 +17,6 @@ import os
from sqlalchemy import event
from sqlalchemy.orm import relationship, backref
import config
from todostates import make_states
......
......@@ -6,7 +6,7 @@ from enum import Enum
from shared import escape_tex
from utils import footnote_hash
import config
from shared import config
INDENT_LETTER = "-"
......
alembic==0.9.8
alembic==0.9.9
amqp==2.2.2
appdirs==1.4.3
APScheduler==3.5.1
argh==0.26.2
bandit==1.4.0
billiard==3.5.0.3
blessings==1.6.1
blinker==1.4
bpython==0.17.1
caldav==0.5.0
celery==4.1.0
certifi==2018.1.18
certifi==2018.4.16
chardet==3.0.4
click==6.7
colorama==0.3.9
coverage==4.5.1
curtsies==0.3.0
enum-compat==0.0.2
eventlet==0.22.1
eventlet==0.23.0
feedgen==0.6.1
flake8==3.5.0
Flask==0.12.2
Flask==1.0.2
Flask-Migrate==2.1.1
Flask-Script==2.0.6
Flask-SocketIO==2.9.3
Flask-SQLAlchemy==2.3.2
Flask-WTF==0.14.2
fuzzywuzzy==0.16.0
gitdb2==2.0.3
GitPython==2.1.9
greenlet==0.4.13
icalendar==4.0.1
idna==2.6
itsdangerous==0.24
Jinja2==2.10
kombu==4.1.0
ldap3==2.4.1
lxml==4.1.1
ldap3==2.5
lxml==4.2.1
Mako==1.0.7
mando==0.6.4
MarkupSafe==1.0
mccabe==0.6.1
nose==1.3.7
packaging==16.8
packaging==17.1
pathtools==0.1.2
psycopg2==2.7.4
pbr==4.0.2
psycopg2-binary==2.7.4
pyasn1==0.4.2
pycodestyle==2.3.1
pyflakes==1.6.0
pyasn1-modules==0.2.1
pycodestyle==2.4.0
Pygments==2.2.0
pyldap==2.4.45
pyldap==3.0.0.post1
pyparsing==2.2.0
python-dateutil==2.7.0
python-dateutil==2.7.3
python-dotenv==0.8.2
python-editor==1.0.3
python-engineio==2.0.2
python-engineio==2.1.0
python-ldap==3.0.0
python-Levenshtein==0.12.0
python-pam==1.8.2
python-socketio==1.8.4
pytz==2018.3
python-pam==1.8.3
pytz==2018.4
PyYAML==3.12
raven==6.6.0
raven==6.7.0
redis==2.10.6
regex==2018.2.8
regex==2018.2.21
requests==2.18.4
six==1.11.0
SQLAlchemy==1.2.3
smmap2==2.0.3
SQLAlchemy==1.2.7
SQLAlchemy-Utils==0.33.3
stevedore==1.28.0
typing==3.6.4
tzlocal==1.5.1
urllib3==1.22
......
......@@ -5,9 +5,9 @@ locale.setlocale(locale.LC_TIME, "de_DE.utf8")
from flask import (
Flask, request, session, flash, redirect,
url_for, abort, render_template, Response, Markup)
import click
from werkzeug.utils import secure_filename
from flask_script import Manager, prompt
from flask_migrate import Migrate, MigrateCommand
from flask_migrate import Migrate
from celery import Celery
from sqlalchemy import or_
from apscheduler.schedulers.background import BackgroundScheduler
......@@ -21,9 +21,8 @@ from datetime import datetime, timedelta
import math
import mimetypes
import config
from shared import (
db, date_filter, datetime_filter, date_filter_long,
config, db, date_filter, datetime_filter, date_filter_long,
date_filter_short, time_filter, time_filter_short, user_manager,
security_manager, current_user, check_login, login_required,
class_filter, needs_date_test, todostate_name_filter,
......@@ -31,9 +30,8 @@ from shared import (
from utils import (
get_first_unused_int, get_etherpad_text, split_terms, optional_int_arg,
fancy_join, footnote_hash, get_git_revision, get_max_page_length_exp,
get_internal_filename, get_csrf_token, get_current_ip)
get_internal_filename, get_current_ip)