diff --git a/migrations/versions/0068d7a0fac0_.py b/migrations/versions/0068d7a0fac0_.py new file mode 100644 index 0000000000000000000000000000000000000000..d539392599b3cf5ea8b8d644c2d92cddff0ef565 --- /dev/null +++ b/migrations/versions/0068d7a0fac0_.py @@ -0,0 +1,30 @@ +"""empty message + +Revision ID: 0068d7a0fac0 +Revises: 4bdc217932c3 +Create Date: 2017-03-04 02:27:14.915941 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0068d7a0fac0' +down_revision = '4bdc217932c3' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('protocoltypes', sa.Column('allowed_networks', sa.String(), nullable=True)) + op.add_column('protocoltypes', sa.Column('restrict_networks', sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('protocoltypes', 'restrict_networks') + op.drop_column('protocoltypes', 'allowed_networks') + # ### end Alembic commands ### diff --git a/models/database.py b/models/database.py index 102a63d6bedba47e5cbfe546fc1b30eb567eb774..0788d1ec112b93cf6c3cd0651e9128713e478c8f 100644 --- a/models/database.py +++ b/models/database.py @@ -6,7 +6,7 @@ from io import StringIO, BytesIO from enum import Enum from shared import db, date_filter, date_filter_short, escape_tex, DATE_KEY, START_TIME_KEY, END_TIME_KEY -from utils import random_string, url_manager, get_etherpad_url, split_terms +from utils import random_string, url_manager, get_etherpad_url, split_terms, check_ip_in_networks from models.errors import DateNotMatchingException import os @@ -36,6 +36,8 @@ class ProtocolType(db.Model): wiki_only_public = db.Column(db.Boolean) printer = db.Column(db.String) calendar = db.Column(db.String) + restrict_networks = db.Column(db.Boolean) + allowed_networks = db.Column(db.String) protocols = relationship("Protocol", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="Protocol.id") default_tops = relationship("DefaultTOP", backref=backref("protocoltype"), cascade="all, delete-orphan", order_by="DefaultTOP.number") @@ -45,8 +47,9 @@ class ProtocolType(db.Model): def __init__(self, name, short_name, organization, usual_time, is_public, modify_group, private_group, public_group, - private_mail, public_mail, - use_wiki, wiki_category, wiki_only_public, printer, calendar): + private_mail, public_mail, use_wiki, wiki_category, + wiki_only_public, printer, calendar, + restrict_networks, allowed_networks): self.name = name self.short_name = short_name self.organization = organization @@ -62,18 +65,22 @@ class ProtocolType(db.Model): self.wiki_only_public = wiki_only_public self.printer = printer self.calendar = calendar + self.restrict_networks = restrict_networks + self.allowed_networks = allowed_networks def __repr__(self): return ("<ProtocolType(id={}, short_name={}, name={}, " "organization={}, is_public={}, modify_group={}, " "private_group={}, public_group={}, use_wiki={}, " "wiki_category='{}', wiki_only_public={}, printer={}, " - "usual_time={}, calendar='{}')>".format( + "usual_time={}, calendar='{}', restrict_networks={}, " + "allowed_networks='{}')>".format( self.id, self.short_name, self.name, self.organization, self.is_public, self.modify_group, self.private_group, self.public_group, self.use_wiki, self.wiki_category, self.wiki_only_public, self.printer, - self.usual_time, self.calendar)) + self.usual_time, self.calendar, self.restrict_networks, + self.allowed_networks)) def get_latest_protocol(self): candidates = sorted([protocol for protocol in self.protocols if protocol.is_done()], key=lambda p: p.date, reverse=True) @@ -81,12 +88,18 @@ class ProtocolType(db.Model): return None return candidates[0] - @hybrid_method def has_public_view_right(self, user): + return (self.has_public_anonymous_view_right() + or (user is not None and self.has_public_authenticated_view_right(user))) + + def has_public_anonymous_view_right(self): return (self.is_public - or (user is not None and - ((self.public_group != "" and self.public_group in user.groups) - or (self.private_group != "" and self.private_group in user.groups)))) + and (not self.restrict_networks + or check_ip_in_networks(self.allowed_networks))) + + def has_public_authenticated_view_right(self, user): + return ((self.public_group != "" and self.public_group in user.groups) + or (self.private_group != "" and self.private_group in user.groups)) def has_private_view_right(self, user): return (user is not None and self.private_group != "" and self.private_group in user.groups) diff --git a/requirements.txt b/requirements.txt index 6cd3d29ea3a7eb551c7058bf79611ba5409c0206..28192423c36fbaa9fb8fa36998e84a8bbd692b6f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,6 @@ MarkupSafe==0.23 nose==1.3.7 packaging==16.8 pathtools==0.1.2 -pkg-resources==0.0.0 psycopg2==2.6.2 Pygments==2.2.0 pyldap==2.4.28 diff --git a/server.py b/server.py index 52c8ef156068c515b584e68e5e3f9c547eebe1a3..76722cc93bbd7156fc650809c67e3090ad0544e7 100755 --- a/server.py +++ b/server.py @@ -9,9 +9,9 @@ from flask_migrate import Migrate, MigrateCommand #from flask_socketio import SocketIO from celery import Celery from sqlalchemy import or_, and_ -#from apscheduler.schedulers.background import BackgroundScheduler -#from apscheduler.triggers.cron import CronTrigger -#from apscheduler.triggers.interval import IntervalTrigger +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.cron import CronTrigger +from apscheduler.triggers.interval import IntervalTrigger import atexit from io import StringIO, BytesIO import os @@ -45,17 +45,6 @@ celery = make_celery(app, config) # return socketio #socketio = make_socketio(app, config) -#def make_scheduler(app, config, function): -# scheduler = BackgroundScheduler() -# scheduler.start() -# scheduler.add_job( -# func=function, -# trigger=CronTrigger(hour='*', minute=23), -# id="scheduler", -# name="Do an action regularly", -# replace_existing=True) -# atexit.register(scheduler.shutdown) - app.jinja_env.trim_blocks = True app.jinja_env.lstrip_blocks = True app.jinja_env.filters["datify"] = date_filter @@ -1227,11 +1216,25 @@ def logout(): return redirect(url_for(".index")) def make_scheduler(): - from uwsgidecorators import timer as uwsgitimer, signal as uwsgisignal, cron as uwsgicron - @uwsgicron(30, -1, -1, -1, -1, target="mule") - def uwsgi_timer(signum): - if signum == 0: - check_and_send_reminders() + try: + from uwsgidecorators import timer as uwsgitimer, signal as uwsgisignal, cron as uwsgicron + print("using uwsgi for cron-like tasks") + @uwsgicron(30, -1, -1, -1, -1, target="mule") + def uwsgi_timer(signum): + if signum == 0: + check_and_send_reminders() + except ModuleNotFoundError: + print("uwsgi not found, falling back to apscheduler for cron-like tasks") + def make_scheduler(app, config, function): + scheduler = BackgroundScheduler() + scheduler.start() + scheduler.add_job( + func=function, + trigger=CronTrigger(hour='*', minute=30), + id="scheduler", + name="Do an action regularly", + replace_existing=True) + atexit.register(scheduler.shutdown) def check_and_send_reminders(): print("check and send reminders") diff --git a/utils.py b/utils.py index 80ce03fa438078a932bdfdbfc98d53334c80a9df..2457d376e202950f25f42ac5e628bc05b58e0282 100644 --- a/utils.py +++ b/utils.py @@ -11,6 +11,7 @@ from email.mime.application import MIMEApplication from datetime import datetime, date, timedelta import requests from io import BytesIO +import ipaddress import config @@ -170,3 +171,17 @@ def add_line_numbers(text): line )) return "\n".join(lines) + +def check_ip_in_networks(networks_string): + address = ipaddress.ip_address(request.remote_addr) + if address == ipaddress.ip_address("127.0.0.1") and "X-Real-Ip" in request.headers: + address = ipaddress.ip_address(request.headers["X-Real-Ip"]) + print(address) + try: + for network_string in networks_string.split(","): + network = ipaddress.ip_network(network_string.strip()) + if address in network: + return True + return False + except ValueError: + return False diff --git a/views/forms.py b/views/forms.py index 3049f5c4768b41ce496bb6d0670c229ccbfd18d9..27698ed7ab9cf4189e216b710451831e74bf2037 100644 --- a/views/forms.py +++ b/views/forms.py @@ -1,7 +1,9 @@ from flask_wtf import FlaskForm -from wtforms import StringField, PasswordField, BooleanField, DateField, HiddenField, IntegerField, SelectField, FileField, DateTimeField, TextAreaField +from wtforms import StringField, PasswordField, BooleanField, DateField, HiddenField, IntegerField, SelectField, FileField, DateTimeField, TextAreaField, Field, widgets from wtforms.validators import InputRequired, Optional +import ipaddress + from models.database import TodoState from validators import CheckTodoDateByState from calendarpush import Client as CalendarClient @@ -59,6 +61,36 @@ def coerce_todostate(key): key = TodoState[key_part] return key +class IPNetworkField(Field): + widget = widgets.TextInput() + + def __init__(self, label=None, validators=None, **kwargs): + super().__init__(label, validators, **kwargs) + + def _value(self): + if self.raw_data: + print("raw_data in _value", self.raw_data) + return " ".join(self.raw_data) + else: + return self.data and str(self.data) or "" + + def process_formdata(self, valuelist): + print("valuelist in process_formdata", valuelist) + if valuelist: + data_str = valuelist[0] + result_parts = [] + try: + for part in data_str.split(","): + part = part.strip() + if len(part) > 0: + network = ipaddress.ip_network(part) + result_parts.append(network) + except ValueError as exc: + print(exc) + self.data = None + raise ValueError(self.gettext("Not a valid IP Network: {}".format(str(exc)))) + self.data = ",".join(map(str, result_parts)) + class LoginForm(FlaskForm): username = StringField("Benutzer", validators=[InputRequired("Bitte gib deinen Benutzernamen ein.")]) password = PasswordField("Passwort", validators=[InputRequired("Bitte gib dein Passwort ein.")]) @@ -79,6 +111,8 @@ class ProtocolTypeForm(FlaskForm): wiki_only_public = BooleanField("Wiki ist öffentlich") printer = SelectField("Drucker", choices=[]) calendar = SelectField("Kalender", choices=[]) + restrict_networks = BooleanField("Netzwerke einschränken") + allowed_networks = IPNetworkField("Erlaubte Netzwerke") def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/views/tables.py b/views/tables.py index f60995ecf2bb82765765ce033672a43b70875cbd..2922b03f0e051c7cf8b98559caa0753a90e5a97f 100644 --- a/views/tables.py +++ b/views/tables.py @@ -125,9 +125,10 @@ class ProtocolTypeTable(SingleValueTable): calendar_headers = ["Kalender"] if not config.CALENDAR_ACTIVE: calendar_headers = [] + network_headers = ["Netzwerke einschränken", "Erlaubte Netzwerke"] action_headers = ["Aktion"] return (general_headers + mail_headers + printing_headers - + wiki_headers + calendar_headers + action_headers) + + wiki_headers + calendar_headers + network_headers + action_headers) def row(self): general_part = [ @@ -159,8 +160,13 @@ class ProtocolTypeTable(SingleValueTable): calendar_part = [self.value.calendar if self.value.calendar is not None else ""] if not config.CALENDAR_ACTIVE: calendar_part = [] + network_part = [ + Table.bool(self.value.restrict_networks), + self.value.allowed_networks + ] action_part = [Table.link(url_for("delete_type", type_id=self.value.id), "Löschen", confirm="Bist du dir sicher, dass du den Protokolltype {} löschen möchtest?".format(self.value.name))] - return general_part + mail_part + printing_part + wiki_part + calendar_part + action_part + return (general_part + mail_part + printing_part + wiki_part + + calendar_part + network_part + action_part) class DefaultTOPsTable(Table): def __init__(self, tops, protocoltype=None):