# HG changeset patch # User Paul Boddie # Date 1454197526 -3600 # Node ID fc1efc973849fbb1047fb43910529b15782998c7 # Parent 6e8f43c29e71ef2d990c6a2eaf358c92ffff8237 Introduced the access control list scheduling function plus argument support. Moved scheduling function lookup into the scheduling module as part of the revised function invocation parsing activity, employing a new text processing module in imiptools. Added tests of the access control list functionality. diff -r 6e8f43c29e71 -r fc1efc973849 imiptools/handlers/resource.py --- a/imiptools/handlers/resource.py Sat Jan 30 17:24:39 2016 +0100 +++ b/imiptools/handlers/resource.py Sun Jan 31 00:45:26 2016 +0100 @@ -22,8 +22,7 @@ from imiptools.data import get_address, to_part, uri_dict from imiptools.handlers import Handler from imiptools.handlers.common import CommonFreebusy, CommonEvent -from imiptools.handlers.scheduling import apply_scheduling_functions, \ - scheduling_functions +from imiptools.handlers.scheduling import apply_scheduling_functions class ResourceHandler(CommonEvent, Handler): @@ -168,10 +167,6 @@ functions = self.get_preferences().get("scheduling_function", "schedule_in_freebusy").split("\n") - # Obtain the actual scheduling functions. - - functions = map(scheduling_functions.get, functions) - return apply_scheduling_functions(functions, self) class Event(ResourceHandler): diff -r 6e8f43c29e71 -r fc1efc973849 imiptools/handlers/scheduling/__init__.py --- a/imiptools/handlers/scheduling/__init__.py Sat Jan 30 17:24:39 2016 +0100 +++ b/imiptools/handlers/scheduling/__init__.py Sun Jan 31 00:45:26 2016 +0100 @@ -19,7 +19,9 @@ this program. If not, see . """ +from imiptools.text import parse_line from imiptools.handlers.scheduling.manifest import scheduling_functions +import re def apply_scheduling_functions(functions, handler): @@ -28,9 +30,13 @@ 'handler'. """ + # Obtain the actual scheduling functions with arguments. + + functions = get_scheduling_function_calls(functions) + response = "ACCEPTED" - for fn in functions: + for fn, args in functions: # NOTE: Should signal an error for incorrectly configured resources. @@ -41,7 +47,7 @@ # declines or gives a null response. else: - result = fn(handler) + result = fn(handler, args) # Return a negative result immediately. @@ -56,4 +62,24 @@ return response +def get_scheduling_function_calls(lines): + + """ + Parse the given 'lines', returning a list of (function, arguments) tuples, + with each function being a genuine function object and with the arguments + being a list of strings. + + Each of the 'lines' should employ the function name and argument strings + separated by whitespace, with any whitespace inside arguments quoted using + single or double quotes. + """ + + functions = [] + + for line in lines: + parts = parse_line(line) + functions.append((scheduling_functions.get(parts[0]), parts[1:])) + + return functions + # vim: tabstop=4 expandtab shiftwidth=4 diff -r 6e8f43c29e71 -r fc1efc973849 imiptools/handlers/scheduling/access.py --- a/imiptools/handlers/scheduling/access.py Sat Jan 30 17:24:39 2016 +0100 +++ b/imiptools/handlers/scheduling/access.py Sun Jan 31 00:45:26 2016 +0100 @@ -19,9 +19,97 @@ this program. If not, see . """ -from imiptools.data import get_address +from imiptools.data import get_address, get_addresses +from imiptools.text import parse_line + +def access_control_list(handler, args): + + """ + Attempt to schedule the current object of the given 'handler' using an + access control list provided in the given 'args', applying it to the + organiser. + """ + + # Obtain either a file from the user's preferences directory... + + if not args: + acl = handler.get_preferences().get("acl") + lines = acl.strip().split("\n") + + # Or obtain the contents of a specific file. + + else: + try: + f = open(args[0]) + except IOError: + return None + try: + lines = f.readlines() + finally: + f.close() + + # Use the current object's identities with the ACL rules. + + organiser = get_address(handler.obj.get_value("ORGANIZER")) + attendees = get_addresses(handler.obj.get_values("ATTENDEE")) + + response = None + + for line in lines: + parts = parse_line(line.strip()) + + # Skip empty lines. + + if not parts: + continue -def same_domain_only(handler): + # Accept either a single word with an action or a rule. + # NOTE: Should signal an error with the format. + + if len(parts) == 1: + action = parts[0] + elif len(parts) >= 3: + action, role, identities = parts[0], parts[1], map(get_address, parts[2:]) + else: + return None + + if action.lower() == "accept": + result = "ACCEPTED" + elif action.lower() in ["decline", "reject"]: + result = "DECLINED" + else: + return None + + # With only an action, prepare a default response in case none of + # the rules match. + + if len(parts) == 1: + response = result + continue + + # Where no default has been set, use an implicit default based on + # the action appearing in a rule. + + elif not response: + response = result == "ACCEPTED" and "DECLINED" or "ACCEPTED" + + # Interpret a rule, attempting to match identities to properties. + + if role.lower() in ["organiser", "organizer"]: + match = organiser in identities + elif role.lower() in ["attendee", "attendees"]: + match = set(attendees).intersection(identities) + else: + return None + + # Use the result of any match. + + if match: + response = result + + return response + +def same_domain_only(handler, args): """ Attempt to schedule the current object of the given 'handler' if the @@ -39,6 +127,7 @@ # Registry of scheduling functions. scheduling_functions = { + "access_control_list" : access_control_list, "same_domain_only" : same_domain_only, } diff -r 6e8f43c29e71 -r fc1efc973849 imiptools/handlers/scheduling/freebusy.py --- a/imiptools/handlers/scheduling/freebusy.py Sat Jan 30 17:24:39 2016 +0100 +++ b/imiptools/handlers/scheduling/freebusy.py Sun Jan 31 00:45:26 2016 +0100 @@ -25,7 +25,7 @@ periods_from, remove_event_periods, \ remove_periods -def schedule_in_freebusy(handler, freebusy=None): +def schedule_in_freebusy(handler, args, freebusy=None): """ Attempt to schedule the current object of the given 'handler' in the @@ -53,7 +53,7 @@ return scheduled and "ACCEPTED" or "DECLINED" -def schedule_corrected_in_freebusy(handler): +def schedule_corrected_in_freebusy(handler, args): """ Attempt to schedule the current object of the given 'handler', correcting @@ -88,7 +88,7 @@ return scheduled == "ACCEPTED" and (corrected and "COUNTER" or "ACCEPTED") or "DECLINED" -def schedule_next_available_in_freebusy(handler): +def schedule_next_available_in_freebusy(handler, args): """ Attempt to schedule the current object of the given 'handler', correcting @@ -202,7 +202,7 @@ # Check one last time, reverting the change if not scheduled. - scheduled = schedule_in_freebusy(handler, busy) + scheduled = schedule_in_freebusy(handler, args, busy) if scheduled == "DECLINED": handler.set_object(obj) diff -r 6e8f43c29e71 -r fc1efc973849 imiptools/text.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/imiptools/text.py Sun Jan 31 00:45:26 2016 +0100 @@ -0,0 +1,56 @@ +#!/usr/bin/env python + +""" +Parsing of textual content. + +Copyright (C) 2016 Paul Boddie + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +import re + +# Parsing of lines to obtain functions and arguments. + +line_pattern_str = r"(?:" \ + r"(?:'(.*?)')" \ + r"|" \ + r'(?:"(.*?)")' \ + r"|" \ + r"([^\s]+)" \ + r")+" \ + r"(?:\s+|$)" +line_pattern = re.compile(line_pattern_str) + +def parse_line(text): + + """ + Parse the given 'text', returning a list of words separated by whitespace in + the input, where whitespace may occur inside words if quoted using single or + double quotes. + """ + + parts = [] + + # Match the components of each part. + + for match in line_pattern.finditer(text): + + # Combine the components by traversing the matching groups. + + parts.append(reduce(lambda a, b: (a or "") + (b or ""), match.groups())) + + return parts + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 6e8f43c29e71 -r fc1efc973849 tests/templates/event-request-sauna-acl.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/templates/event-request-sauna-acl.txt Sun Jan 31 00:45:26 2016 +0100 @@ -0,0 +1,35 @@ +Content-Type: multipart/alternative; boundary="===============0047278175==" +MIME-Version: 1.0 +From: paul.boddie@example.com +To: resource-room-sauna@example.com +Subject: Invitation! + +--===============0047278175== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit + +This message contains an event. +--===============0047278175== +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Type: text/calendar; charset="us-ascii"; method="REQUEST" + +BEGIN:VCALENDAR +PRODID:-//imip-agent/test//EN +METHOD:REQUEST +VERSION:2.0 +BEGIN:VEVENT +ORGANIZER:mailto:paul.boddie@example.com +ATTENDEE;ROLE=CHAIR:mailto:paul.boddie@example.com +ATTENDEE;RSVP=TRUE:mailto:resource-room-sauna@example.com +ATTENDEE;RSVP=TRUE:mailto:simon.skunk@example.com +DTSTAMP:20141125T004600Z +DTSTART;TZID=Europe/Oslo:20141126T170000 +DTEND;TZID=Europe/Oslo:20141126T174500 +SUMMARY:Meeting at 5pm +UID:event20@example.com +END:VEVENT +END:VCALENDAR + +--===============0047278175==-- diff -r 6e8f43c29e71 -r fc1efc973849 tests/test_resource_invitation_constraints_multiple.sh --- a/tests/test_resource_invitation_constraints_multiple.sh Sat Jan 30 17:24:39 2016 +0100 +++ b/tests/test_resource_invitation_constraints_multiple.sh Sun Jan 31 00:45:26 2016 +0100 @@ -3,12 +3,11 @@ . "`dirname \"$0\"`/common.sh" USER="mailto:resource-room-sauna@example.com" -SENDER="mailto:paul.boddie@example.net" +SENDER="mailto:paul.boddie@example.com" +OUTSIDESENDER="mailto:paul.boddie@example.net" FBFILE="$STORE/$USER/freebusy" -FBOFFERFILE="$STORE/$USER/freebusy-offers" FBSENDERFILE="$STORE/$SENDER/freebusy" -FBSENDEROTHERFILE="$STORE/$SENDER/freebusy-other/$USER" -FBSENDERREQUESTS="$STORE/$SENDER/requests" +FBOUTSIDESENDERFILE="$STORE/$OUTSIDESENDER/freebusy" mkdir -p "$PREFS/$USER" echo 'Europe/Oslo' > "$PREFS/$USER/TZID" @@ -33,7 +32,7 @@ "$OUTGOING_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-outsider.txt" 2>> $ERROR - grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBSENDERFILE" \ + grep -q "^20141126T150000Z${TAB}20141126T154500Z" "$FBOUTSIDESENDERFILE" \ && echo "Success" \ || echo "Failed" @@ -87,3 +86,137 @@ && grep -q 'FREEBUSY;FBTYPE=BUSY:20141126T150000Z/20141126T154500Z' out3.tmp \ && echo "Success" \ || echo "Failed" + +# Try a different scheduling function. + +ACL="$PWD/acl.tmp" + +cat > "$PREFS/$USER/scheduling_function" < "$ACL" <> $ERROR + + grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBSENDERFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the request to the resource. + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-acl.txt" 2>> $ERROR \ +| tee out4r.tmp \ +| "$SHOWMAIL" \ +> out4.tmp + + grep -q 'METHOD:REPLY' out4.tmp \ +&& grep -q 'ATTENDEE.*;PARTSTAT=DECLINED' out4.tmp \ +&& echo "Success" \ +|| echo "Failed" + + ! [ -e "$FBFILE" ] \ +|| ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Try with an unreasonable ACL. + +cat > "$ACL" <> $ERROR + + grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBSENDERFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the request to the resource. + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-acl.txt" 2>> $ERROR \ +| tee out5r.tmp \ +| "$SHOWMAIL" \ +> out5.tmp + + grep -q 'METHOD:REPLY' out5.tmp \ +&& grep -q 'ATTENDEE.*;PARTSTAT=DECLINED' out5.tmp \ +&& echo "Success" \ +|| echo "Failed" + + ! [ -e "$FBFILE" ] \ +|| ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Try with a reasonable ACL. + +cat > "$ACL" <> $ERROR + + grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBSENDERFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Present the request to the resource. + + "$RESOURCE_SCRIPT" $ARGS < "$TEMPLATES/event-request-sauna-acl.txt" 2>> $ERROR \ +| tee out6r.tmp \ +| "$SHOWMAIL" \ +> out6.tmp + + grep -q 'METHOD:REPLY' out6.tmp \ +&& grep -q 'ATTENDEE.*;PARTSTAT=DECLINED' out6.tmp \ +&& echo "Success" \ +|| echo "Failed" + + ! [ -e "$FBFILE" ] \ +|| ! grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBFILE" \ +&& echo "Success" \ +|| echo "Failed" + +# Modify the ACL, using the implicit preference setting to hold the list. + +cat > "$PREFS/$USER/scheduling_function" < "$PREFS/$USER/acl" <> $ERROR \ +| tee out7r.tmp \ +| "$SHOWMAIL" \ +> out7.tmp + + grep -q 'METHOD:REPLY' out7.tmp \ +&& grep -q 'ATTENDEE.*;PARTSTAT=ACCEPTED' out7.tmp \ +&& echo "Success" \ +|| echo "Failed" + + ! [ -e "$FBFILE" ] \ +|| grep -q "^20141126T160000Z${TAB}20141126T164500Z" "$FBFILE" \ +&& echo "Success" \ +|| echo "Failed" + +rm "$ACL"