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
]
#!/usr/bin/env python3
import os
from common import auth
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def split_line(line, max_length=76):
words = list(filter(None, map(str.strip, line.split(" "))))
lines = []
current_line = []
current_line_length = 0
for word in words:
if (current_line and current_line_length + len(word) + 1 > max_length):
lines.append(" ".join(current_line))
current_line = []
current_line_length = 0
current_line.append(word)
current_line_length += len(word) + 1
if current_line:
lines.append(" ".join(current_line))
return lines
def prefix_lines(lines, prefix="# "):
return map(lambda line: "".join((prefix, line)), lines)
class ConfigEntry:
def __init__(self, name, default, required=True, internal=True,
immutable=False, no_sane_default=False,
description=None, value=None):
self.name = name
self.default = default
self.required = required
self.internal = internal
self.immutable = immutable
self.no_sane_default = no_sane_default
self.description = description
self.value = value
def extract(self, config):
if self.immutable:
self.value = self.default
return
if not hasattr(config, self.name) and self.required:
raise ValueError(
"Missing required config entry {}!".format(self.name))
self.value = getattr(config, self.name, self.default)
if self.no_sane_default and self.value == self.default:
raise ValueError(
"You have not configured {}, which is required!".format(
self.name))
def get_example(self):
if self.immutable:
return []
lines = []
if self.description is not None:
lines.extend(prefix_lines(split_line(self.description)))
entry_line = "{} = {}".format(self.name, repr(self.default))
if not self.required:
entry_line = "# {}".format(entry_line)
lines.append(entry_line)
return lines
class ConfigSection:
def __init__(self, name, entries, check, description=None,
deactivatable=False, active=None, imports=None):
self.name = name
self.entries = entries
self.check = check
self.description = description
self.deactivatable = deactivatable
self.active = active
if isinstance(imports, str):
imports = [imports]
self.imports = imports
def extract(self, config):
if self.deactivatable:
self.active = getattr(config, "{}_ACTIVE".format(self.name), False)
if not self.active:
return
for entry in self.entries:
entry.extract(config)
def get_example(self):
lines = []
if self.imports:
lines.extend(self.imports)
header_line = (
"# [{}]".format(self.name) +
(" (optional)" if self.deactivatable else ""))
lines.append(header_line)
if self.description is not None:
lines.extend(prefix_lines(split_line(self.description)))
lines.append("")
if self.deactivatable:
lines.extend(prefix_lines(split_line("deactivate with")))
lines.append("# {}_ACTIVE = False".format(self.name))
lines.append("")
for entry in self.entries:
lines.extend(entry.get_example())
lines.append("")
lines.append("")
return "\n".join(lines)
def fill_modules(self, config, public_config):
if self.deactivatable:
setattr(config, "{}_ACTIVE".format(self.name), self.active)
setattr(public_config, "{}_ACTIVE".format(self.name), self.active)
if not self.active:
return
for entry in self.entries:
setattr(config, entry.name, entry.value)
if not entry.internal:
setattr(public_config, entry.name, entry.value)
def check_choice(option, value, choices):
if value not in choices:
raise ValueError(
"{} is not allowed choices! Should be one of {}, is {}"