Commit a0ca4d6d authored by Robin Sonnabend's avatar Robin Sonnabend

Parse, check and generate the configuration

The configuration is defined in configproxy.py. Add new keys there.
Use ./configproxy.py check to check the config validity.
    ./configproxy.py create to create an example config.
parent 4d789866
......@@ -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 4c0c16f069bb39d593b53eae0c2b92b1f05eba90
Subproject commit 2a0f881728f1000f27fb00321bc37fb869007fa9
# (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 common.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", # pass multiple hosts for replication
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
]
# [Database]
# Settings for SQLAlchemy
# Database connection string. See
# http://docs.sqlalchemy.org/en/latest/core/engines.html for details. SQLite
# does not work with Alembic migrations.
SQLALCHEMY_DATABASE_URI = 'engine://user:password@host/database'
# [Security]
# Secret keys and random strings
# Secret random key used for session security.
SECRET_KEY = b'\x86\xb1;\xfd\x8a\x05f\xf5/\xb22"\x07_\xc3\x021\x0f\xa2\xf6\xd56<i\xc8\xd4\xe0\xa4R\xf9D.\xa3B`\x8b#\xddYk\xfc\xcc\xe3\xe4\xce\xa1 \x81([\xc5e\xe7\xb0#\\x\x87\xa6-~\x15\x8c>\x19\xcc\xf1\x8f\xb0d\xc7\xdc\x0f\xcf\xe9\xc6\x03D\xfc~!7\xa8\xa9\x15\xe7\xa2\xde9\x18\x93N`\x0f\xdb\xb1\x0b\x08\xd6\xaa\xbb\x7f\x8f\xc5\x91\xf77\x07\x01\xce,\xcd\xfb\xfd\x1c\xd7N\xd3\x13\xbdR\xb8\x05\xd3\x98\xe8\xdcQ'
# Secret random key used for user sessions.
SECURITY_KEY = b"\xad\xf5\xfav\xbc\xb3u\xfc\xc0\xd3\xf4\xa1\x8bu\x81\x85\t%P\xba\xe5\xb8\xacv\x92\xa8\x16U]\xc5d\xdb\xf9\t\x95\xcc\x91\x88T\xedK]%\xd1\xe6gis\xf3N\xc1\xac\x98\x10q\xd1*\xcd\xce.\xec\xe4\x0f\x9b\xdd\xd1c\xa1\x01\xba\xdf\x0f%\x88\xdd\x03H\xd8\xc84F\xe6\xeb\xb1\x8a\x08\xd3<S?\xff\x9c\x19\xf4\xa5J\x84\xf4jy\xf1[\xf9\xe5\xce:>\xa9DS\xe9\xe3\x8f\x8d!\x8f\xa3\xf6p+\\\x86\xc9,:~E'"
# [URL Root]
# Where is the website hosted
# Domain on which this website is hosted
SERVER_NAME = 'protokolle.example.com'
# Protocol used by this website. Either 'http' or 'https'.
# PREFERRED_URL_SCHEME = 'https'
# [Debug]
# Debug mode. Do not set in production.
# Activate debug mode
# DEBUG = False
# [Celery]
# Settings for the task scheduler.
# Connection to the celery broker. See
# http://docs.celeryproject.org/en/latest/userguide/configuration.html
CELERY_BROKER_URL = 'redis://localhost:6379/0'
# [Sentry]
# Connection information for sentry exception reporting.
# Connection string for sentry. See
# https://docs.sentry.io/quickstart/#configure-the-dsn
# SENTRY_DSN = None
from common import auth
# [Authentication]
# User Authentication backend settings.
# Time in seconds for which non-permanent sessions remain valid
# AUTH_MAX_DURATION = 300
# Active Authentication backends
AUTH_BACKENDS = [StaticUserManager([('user', 'password', ('group',))])]
# [Error Report Context]
# Compiling error context settings
# Number of lines before and after an error to include in the error
# description
# ERROR_CONTEXT_LINES = 3
# [Pagination]
# Pagination settings, used for list pages
# Default number of entries per page
# PAGE_LENGTH = 20
# Number of pages before and after the current one with a direct link.
# PAGE_DIFF = 3
# [Index Page]
# Settings for what to show on the index page
# Next days to list upcoming meetings on
# MAX_INDEX_DAYS = 14
# Past days to list unfinished meetings on
# MAX_PAST_INDEX_DAYS = 2
# If a meeting is unfinished this many days after its date, a reminder is
# sent by mail
# MAX_PAST_INDEX_DAYS_BEFORE_REMINDER = 14
# [Admin data]
# Settings for who is an admin and how to contact them
# Mail address to tell users to contact in case of errors
ADMIN_MAIL = 'admin@example.com'
# Users with this group are admins and are allowed to do and see everything.
ADMIN_GROUP = 'admin'
# [Parser]
# Settings for the protocol syntax parser
# Do not enforce some parser policies.
# PARSER_LAZY = False
# Todos with at least this equality score are considered equal whe importing
# old protocols.
# FUZZY_MIN_SCORE = 90
# Keywords indicating private protocol parts
# PRIVATE_KEYWORDS = ['private', 'internal', 'privat', 'intern']
# [Rendering]
# Settings for rendering protocols to pdf, html, etc.
# fonts for xelatex
FONTS = {'main': {'extension': '.otf', 'path': '/usr/share/fonts/gsfonts/', 'regular': 'NimbusSans-Regular', 'bold': 'NimbusSans-Bold', 'italic': 'NimbusSans-Italic', 'bolditalic': 'NimbusSans-BoldItalic'}, 'roman': {'extension': '.otf', 'path': '/usr/share/fonts/gsfonts/', 'regular': 'NimbusRoman-Regular', 'bold': 'NimbusRoman-Bold', 'italic': 'NimbusRoman-Italic', 'bolditalic': 'NimbusRoman-BoldItalic'}, 'sans': {'extension': '.otf', 'path': '/usr/share/fonts/gsfonts/', 'regular': 'NimbusSans-Regular', 'bold': 'NimbusSans-Bold', 'italic': 'NimbusSans-Italic', 'bolditalic': 'NimbusSans-BoldItalic'}, 'mono': {'extension': '.otf', 'path': '/usr/share/fonts/gsfonts/', 'regular': 'NimbusMonoPS-Regular', 'bold': 'NimbusMonoPS-Bold', 'italic': 'NimbusMonoPS-Italic', 'bolditalic': 'NimbusMonoPS-BoldItalic'}}
# Path to the directory to save protocols in. Write access is necessary.
# DOCUMENTS_PATH = 'documents'
# list of bulletpoints to use in latex
# LATEX_BULLETPOINTS = ['\\textbullet', '\\normalfont \\bfseries \\textendash', '\\textasteriskcentered', '\\textperiodcentered']
# Header level at which to start with HTML headlines
# HTML_LEVEL_OFFSET = 3
# path to additional jinja2 templates
# LATEX_LOCAL_TEMPLATES = None
# template to include at the top of protocols
# LATEX_LOGO_TEMPLATE = None
# custom latex page geometry
# LATEX_GEOMETRY = None
# custom latex pagestyle, e.g. 'fancy'
# LATEX_PAGESTYLE = None
# Include a header and footer in protocols
# LATEX_HEADER_FOOTER = False
# 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', and the
# templates provides the files: 'protokoll2.cls' (class), 'protocol.tex'
# (protocol), 'decision.tex' (decision) and 'asta-logo.tex'
# LATEX_TEMPLATES = None
# [MAIL] (optional)
# Mail server connection
# deactivate with
# MAIL_ACTIVE = False
# Mail sender address
MAIL_FROM = 'protokolle@example.com'
# SMTP Mail server address with port
MAIL_HOST = 'mail.example.com:465'
# Mail server login user. Empty for no authentication.
# MAIL_USER = ''
# Mail server login password. Empty for no authentication.
# MAIL_PASSWORD = ''
# Use SMPTS (not STARTTLS). Should match port.
# MAIL_USE_TLS = True
# Use SMTP with STARTTLS. Should match port.
# MAIL_USE_STARTTLS = False
# [PRINTING] (optional)
# CUPS printing settings
# deactivate with
# PRINTING_ACTIVE = False
# CUPS printserver connection
PRINTING_SERVER = 'printsrv.example.com:631'
# CUPS user for print jobs
# PRINTING_USER = 'protocols'
# printers with corresponding options
PRINTING_PRINTERS = {'example_printer': ['Duplex=DuplexNoTumble', 'option2=value'], 'other_printer': ['list', 'of', 'options']}
# [ETHERPAD] (optional)
# Etherpad settings
# deactivate with
# ETHERPAD_ACTIVE = False
# URL of the etherpad installation. Do not include the '/p'!
ETHERPAD_URL = 'https://example.com/etherpad'
# The content a new etherpad contains.
# EMPTY_ETHERPAD = 'Welcome to Etherpad!\n\nThis 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!\n\nGet involved with Etherpad at http://etherpad.org\n\n'
# [WIKI] (optional)
# Mediawiki or Dokuwiki settings
# deactivate with
# WIKI_ACTIVE = False
# 'MEDIAWIKI' or 'DOKUWIKI'
WIKI_TYPE = 'MEDIAWIKI'
# API Endpoint for Mediawiki,
# 'https://user:password@wiki.example.com/lib/exe/xmlrpc.php' for Dokuwiki
WIKI_API_URL = 'https://wiki.example.com/wiki/api.php'
# Skip login (only for Mediawiki)
# WIKI_ANONYMOUS = False
# Login user (only for Mediawiki)
# WIKI_USER = None
# Login password (only for Mediawiki)
# WIKI_PASSWORD = None
# Login domain (only for Mediawiki)
# WIKI_DOMAIN = None
# [CALENDAR] (optional)
# CalDAV settings
# deactivate with
# CALENDAR_ACTIVE = False
# CalDAV server URL
CALENDAR_URL = 'https://user:password@calendar.example.com/dav/'
# Default meeting length in hours
# CALENDAR_DEFAULT_DURATION = 3
# Number of retries before giving a connection attempt up. Some CalDAV
# servers like to randomly reply with errors.
# CALENDAR_MAX_REQUESTS = 10
# Timezone abbreviation map. Add as needed.
# CALENDAR_TIMEZONE_MAP = {'CET': 'Europe/Berlin', 'CEST': 'Europe/Berline'}
#!/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 {}".format(
option, choices, value))
def check_database(
SQLALCHEMY_DATABASE_URI,
SQLALCHEMY_TRACK_MODIFICATIONS):
from sqlalchemy_utils import database_exists
from sqlalchemy import exc
try:
if not database_exists(SQLALCHEMY_DATABASE_URI):
raise ValueError(
"The database '{}' does not exist! "
"Please configure it correctly or create it!".format(
SQLALCHEMY_DATABASE_URI))
except (exc.NoSuchModuleError, exc.OperationalError) as error:
raise ValueError(
"The database uri '{}' does not state a valid "
"database: {}".format(SQLALCHEMY_DATABASE_URI, error))
def check_security(SECRET_KEY, SECURITY_KEY, SESSION_PROTECTION):
MIN_KEY_LENGTH = 20
if len(SECRET_KEY) < MIN_KEY_LENGTH:
raise ValueError(
"Insufficient length of SECRET_KEY, should be at least {}!".format(
MIN_KEY_LENGTH))
if len(SECURITY_KEY) < MIN_KEY_LENGTH:
raise ValueError(
"Insufficient length of SECURITY_KEY, should be at "
"least {}!".format(
MIN_KEY_LENGTH))
check_choice("SESSION_PROTECTION", SESSION_PROTECTION, ["strong"])
def check_server_name(SERVER_NAME, PREFERRED_URL_SCHEME):
# todo: check ip address and server name
check_choice(
"PREFERRED_URL_SCHEME", PREFERRED_URL_SCHEME,
["http", "https"])
def check_debug(DEBUG):
if DEBUG:
logger.warning("DEBUG mode is activated!")
def check_celery(