From f0211b3841173302c4b06d002014e1d58473d028 Mon Sep 17 00:00:00 2001 From: Leonhard Kuboschek Date: Fri, 10 Mar 2017 13:43:38 +0100 Subject: [PATCH] Switch authentication and permission management Previously, authentication worked by transmitting the password to the legacy OpenJUB API. Furthermore, user data was also received from this API and evaluated using a custom JavaScript bridge. This caused problem, as Jay was able potentially able to intercept user passwords and also depended on two legacy systems, the OpenJUB API and the memory intensive JavaScript evaluation of filters This commit updates the authentication to use the new dreamjub api via OAuth. Furthermore, it updates the permission system to move away from UserProfiles and SuperAdmin models. Instead, this commit makes use of internal Django mechanisms and stores user data directly with the model. Furthermore, we also migrate the evaluation of filters to use a python-based evaluation, which no longer needs a JavaScript runtime on the server side. See also issue #26. --- README.md | 6 + core/templatetags/userflags.py | 15 + core/views.py | 4 +- filters/forest.js | 26 -- filters/forest.py | 79 ---- filters/forest/__init__.py | 0 filters/forest/layouter.py | 242 ++++++++++ filters/forest/logic.py | 415 ++++++++++++++++++ filters/forest/renderer.py | 109 +++++ filters/models.py | 29 +- filters/templatetags/filter.py | 14 +- filters/views.py | 30 +- jay/allauthurls/__init__.py | 0 jay/allauthurls/main.py | 20 + jay/allauthurls/socialaccounts.py | 8 + jay/settings.py | 28 +- jay/urls.py | 3 +- jay/utils.py | 34 +- requirements.txt | 14 +- settings/models.py | 49 ++- settings/urls.py | 10 +- settings/views/systems.py | 1 - static/js/forest/logic.js | 202 --------- templates/auth/login.html | 50 --- templates/base/base.html | 23 +- .../socialaccount/authentication_error.html | 40 ++ templates/socialaccount/login_cancelled.html | 24 + templates/vote/fragments/option_edit.html | 2 +- templates/vote/fragments/vote_list.html | 4 +- templates/vote/fragments/vote_stage.html | 8 +- users/models.py | 91 +--- users/ojub_auth.py | 130 ++---- votes/models.py | 35 +- votes/views.py | 51 +-- 34 files changed, 1122 insertions(+), 674 deletions(-) delete mode 100644 filters/forest.js delete mode 100644 filters/forest.py create mode 100644 filters/forest/__init__.py create mode 100644 filters/forest/layouter.py create mode 100644 filters/forest/logic.py create mode 100644 filters/forest/renderer.py create mode 100644 jay/allauthurls/__init__.py create mode 100644 jay/allauthurls/main.py create mode 100644 jay/allauthurls/socialaccounts.py delete mode 100644 templates/auth/login.html create mode 100644 templates/socialaccount/authentication_error.html create mode 100644 templates/socialaccount/login_cancelled.html diff --git a/README.md b/README.md index 9abc0d5..61b1008 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ Jay is a simple secret voting system for Jacobs University. Just in case you wer see [doc/](doc) for minimal developer documentation +## Setup + +1. Configure database and apply migrations +2. Create a Social Application pointing to `dreamjub` +3. update `DREAMJUB_CLIENT_ID` and `DREAMJUB_CLIENT_SECRET` variables + ## License Jay is © 2015-17 Leonhard Kuboschek, Tom Wiesing & Contributors. Licensed under MIT license. See [LICENSE.md](LICENSE.md) for details. diff --git a/core/templatetags/userflags.py b/core/templatetags/userflags.py index e69de29..f5cfbd3 100644 --- a/core/templatetags/userflags.py +++ b/core/templatetags/userflags.py @@ -0,0 +1,15 @@ +from django import template +from settings.models import VotingSystem +from jay import utils + +register = template.Library() + + +@register.filter() +def getAdminSystems(user): + return VotingSystem.getAdminSystems(user) + + +@register.filter() +def isSuperAdmin(user): + return utils.is_superadmin(user) diff --git a/core/views.py b/core/views.py index 1cfb20e..e9889f8 100644 --- a/core/views.py +++ b/core/views.py @@ -4,6 +4,7 @@ from settings.models import VotingSystem from votes.models import Vote, Status +from jay.utils import get_user_details # Create your views here. @@ -14,7 +15,8 @@ def home(request): systems = VotingSystem.objects.all() if request.user.is_authenticated(): - details = json.loads(request.user.profile.details) + details = get_user_details(request.user) + votes_shown = [v for v in votes if v.filter.matches(details)] ctx["vote_list_title"] = "Your votes" diff --git a/filters/forest.js b/filters/forest.js deleted file mode 100644 index 824add0..0000000 --- a/filters/forest.js +++ /dev/null @@ -1,26 +0,0 @@ - -/** Parses a string into a tree */ -var parse = function(obj){ - return logic.parse(obj); -}; - -/** Simplifies a parsed tree */ -var simplify = function( tree ){ - return logic.simplify(tree); -}; - -/** Checks if an object matches a filter tree */ -var matches = function(tree, obj){ - return logic.matches(tree, obj); -} - -/** Finds objects that match a filter tree */ -var map_match = function(tree, objs){ - var res = []; - - for(var i = 0; i < objs.length; i++){ - res.push(matches(tree, objs[i])); - } - - return res; -} diff --git a/filters/forest.py b/filters/forest.py deleted file mode 100644 index 6c2faa1..0000000 --- a/filters/forest.py +++ /dev/null @@ -1,79 +0,0 @@ -from django.contrib.staticfiles import finders -import os.path -import execjs - -from jay.utils import memoize - - -def init(): - """ Initialises the javascript context """ - - # find the path to the static files. - - files = [ - 'js/forest/jsep.js', - 'js/forest/logic.js', - 'js/forest/layouter.js', - 'js/forest/renderer.js' - ] - - src = '' - - for f in files: - with open(finders.find(f)) as g: - src += g.read() - - with open(os.path.join(os.path.dirname(__file__), 'forest.js')) as g: - src += g.read() - - # and eval them. - return execjs.compile(src) - - -# initialise the context -ctx = init() - - -@memoize -def parse(treestr): - return ctx.call('parse', treestr) - - -@memoize -def simplify(tree): - return ctx.call('simplify', tree) - - -def parse_and_simplify(treestr): - tree = parse(treestr) - return simplify(tree) - - -@memoize -def matches(tree, obj): - return ctx.call('matches', tree, obj) - - -def map_match(tree, objs): - return ctx.call('map_match', tree, objs) - - -@memoize -def layouter(tree, obj): - return ctx.call('layouter', tree, obj) - - -@memoize -def renderer(layout): - return ctx.call('renderer', layout) - - -@memoize -def renderer_box(contentNode, inp, out): - return ctx.call('renderer.box', contentNode, inp, out) - - -def parse_and_render(treestr, obj): - tree = parse(treestr) - layout = layouter(tree, obj) - return renderer(layout) diff --git a/filters/forest/__init__.py b/filters/forest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/filters/forest/layouter.py b/filters/forest/layouter.py new file mode 100644 index 0000000..6875ae0 --- /dev/null +++ b/filters/forest/layouter.py @@ -0,0 +1,242 @@ +from . import logic + + +def layouter(tree, obj): + op = tree["operation"] + + # we have a constant or a filter, there is only one thing to render + if ((op in logic.op_list["OP_TRUE"]) or ( + op in logic.op_list["OP_FALSE"]) or ( + op in logic.op_list["OPS_FILTERS"])): + return layout_const(op, tree, obj) + + # for a unary operation, we add a new node on top + if (op in logic.op_list["OPS_UNARY"]): + right = layouter(tree["right"], obj) + return layout_unary(op, right, tree, obj) + + # for a binary operation, we need to merge two existing parts + if (op in logic.op_list["OPS_BINARY"]): + left = layouter(tree["left"], obj) + right = layouter(tree["right"], obj) + return layout_binary(op, left, right, tree, obj) + + raise Exception("Unexpected operator during rendering") + + +def layout_const(op, tree, obj): + doesMatch = logic.matches(tree, obj) + is_filter = False + + # if it is a filter, include key, value + if (op in logic.op_list["OPS_FILTERS"]): + is_filter = True + + return { + 'size': [1, 1], # height x width + 'mainX': 0, + 'out': logic.matches(tree, obj), + 'grid': [[ + { + 'type': 'node', + 'prop': { + 'class': 'const', + 'op': op, + + 'is_filter': is_filter, + 'key': tree['key'] if 'key' in tree else '', + 'value': tree['value'] if 'value' in tree else '', + + 'input': [], + 'output': doesMatch + } + } + ]] + } + + +def layout_unary(op, right, tree, obj): + # get some properties from the right. + rightSize = right['size'] + rightGrid = right['grid'] + rightOut = right['out'] + rightX = right['mainX'] + + # check the value we should return + doesMatch = logic.matches(tree, obj) + + # these are two new lines to render + nodeline = [] + connline = [] + + # create them with a lot of empty space. + for i in range(rightSize[1]): + if i != rightX: + nodeline.append({ + "type": "empty" + }) + + connline.append({ + "type": "empty" + }) + else: + # push the node on top + nodeline.append({ + 'type': 'node', + 'prop': { + 'op': op, + 'input': [rightOut], + 'output': doesMatch + } + }) + + # push a connection line + connline.append({ + 'type': 'conn', + 'prop': { + 'class': 'tree_connect_ver', + 'active': [rightOut] + } + }) + + # add the connection line + rightGrid.insert(0, connline) + + # and the nodeline + rightGrid.insert(0, nodeline) + + # and assemble the thing to return + return { + 'size': [rightSize[0] + 2, right['size'][1]], + 'mainX': rightX, + 'out': doesMatch, + 'grid': rightGrid + } + + +def make_empty_line(size): + return [{"type": "empty"} for i in range(size)] + + +def layout_binary(op, left, right, tree, obj): + # get some properties from the left. + leftSize = left['size'] + leftGrid = left['grid'] + leftOut = left['out'] + leftX = left['mainX'] + + # get some properties from the right. + rightSize = right['size'] + rightGrid = right['grid'] + rightOut = right['out'] + rightX = right['mainX'] + + # the new size + newWidth = leftSize[1] + rightSize[1] + 1 + newHeight = max(leftSize[0], rightSize[0]) + 2 + + # check the value we should return + doesMatch = logic.matches(tree, obj) + + # these are two new lines to render + nodeline = [] + connline = [] + + # create the new top lines + for i in range(newWidth): + if (i != leftSize[1]): + nodeline.append({ + 'type': 'empty' + }) + + # for the left x, we need to connect towards the right + if (i == leftX): + connline.append({ + 'type': 'conn', + 'prop': { + 'active': [leftOut], + 'class': 'tree_connect_bot_right' + } + }) + elif i < leftX: + connline.append({ + 'type': 'empty' + }) + elif (i <= leftSize[1]): + connline.append({ + 'type': 'conn', + 'prop': { + 'active': [leftOut], + 'class': 'tree_connect_hor' + } + }) + elif (i <= leftSize[1] + rightX): + connline.append({ + 'type': 'conn', + 'prop': { + 'active': [rightOut], + 'class': 'tree_connect_hor' + } + }) + elif (i == leftSize[1] + rightX + 1): + connline.append({ + 'type': 'conn', + 'prop': { + 'active': [rightOut], + 'class': 'tree_connect_bot_left' + } + }) + else: + connline.append({ + 'type': 'empty' + }) + else: + # push the node on top + nodeline.append({ + 'type': 'node', + 'prop': { + 'class': 'binary', + 'op': op, + 'input': [leftOut, rightOut], + 'output': doesMatch + } + }) + + # push the connection line l / r + connline.append({ + 'type': 'conn', + 'prop': { + 'active': [leftOut, rightOut], + 'class': 'tree_connect_top_lr' + } + }) + + # create a new grid + newGrid = [] + + # push the top two lines + newGrid.append(nodeline) + newGrid.append(connline) + + for i in range(newHeight - 2): + newGrid.append( + [] + + + (leftGrid[i] if len(leftGrid) > i else make_empty_line( + leftSize[1])) + + + [{ + "type": "empty" + }] + + + (rightGrid[i] if len(rightGrid) > i else make_empty_line( + rightSize[1])) + ) + + # and assemble the thing to return + return { + 'size': [newHeight, newWidth], + 'mainX': leftSize[1], + 'out': doesMatch, + 'grid': newGrid + } diff --git a/filters/forest/logic.py b/filters/forest/logic.py new file mode 100644 index 0000000..01ec750 --- /dev/null +++ b/filters/forest/logic.py @@ -0,0 +1,415 @@ +import re +import PreJsPy + +OP_TRUE = ['true'] +OP_FALSE = ['false'] + +OP_NOT = ['not', '!'] + +OP_EQUALS = ['equals', '=', '==', '==='] + +OP_LESS = ['less than', '<'] +OP_LESS_EQUAL = ['less than or equal', '<=', '=<', ] + +OP_GREATER = ['greater than', '>'] +OP_GREATER_EQUAL = ['greater than or equal', '>=', '=>'] + +OP_CONTAINS = ['contains', '::'] + +OP_MATCHES = ['matches', 'unicorn', '@'] + +OP_AND = ['and', '&', '&&', '*'] + +OP_OR = ['or', '|', '||', '+'] + +OP_NAND = ['nand', '!&'] + +OP_XOR = ['xor', '^'] + +# ============================================================================= +# OPERATOR GROUPS CONFIG +# ============================================================================= + +# unary operators that expect one binary argument +OPS_UNARY = [] + OP_NOT + +# filters that expect a key and value argument +OPS_FILTERS = [] + OP_EQUALS + OP_LESS + OP_LESS_EQUAL + OP_GREATER + \ + OP_GREATER_EQUAL + OP_CONTAINS + OP_MATCHES + +# binary operators that expect to logical arguments +OPS_BINARY = [] + OP_AND + OP_OR + OP_NAND + OP_XOR + + +# ============================================================================= + + +def popu_jsep(jsep): + jsep.setUnaryOperators(OPS_UNARY) + + OPS_BIN_ALL = dict((o, 1) for o in OPS_BINARY) + OPS_BIN_ALL.update( + dict((o, 2) for o in OPS_FILTERS) + ) + jsep.setBinaryOperators(OPS_BIN_ALL) + + +# create a new parser for parsing all the things +parser = PreJsPy.PreJsPy() +popu_jsep(parser) + + +def parse_binary(tree): + """ + A binary expression is either: + + 1. A literal expression + 1a. the literal true ( ===> true ) + 1b. the literal false ( ===> false ) + 1c. the literal null ( ===> false ) + 2. A unary logical expression + 2a. a not expression + 3. A binary logical connetive + 3a. a logical And + 3b. a logical or + 3c. a logical nand + 3d. a logical xor + 4. A binary filter + 4a. an equals filter + 4b. a less filter + 4c. a less or equals filter + 4d. a greater filter + 4e. a greater than filter + 4f. a contains filter + 4g. a matches filter + 5. A primitive expression ( ==> is the string false-ish ? ) + """ + + # if there is nothing, return + if not tree: + raise Exception("Missing binary expression. ") + + # check for literals + if tree["type"] == "Literal": + if tree["raw"] == OP_TRUE[0]: + return {'operation': OP_TRUE[0]} + elif (tree["raw"] == OP_FALSE[0]): + return {'operation': OP_FALSE[0]} + elif (tree.raw == 'null'): + return {'operation': OP_FALSE[0]} + + # check for a unary expression + if (tree["type"] == 'UnaryExpression'): + # find the index inside the logical not + if tree["operator"] in OP_NOT: + return {'operation': OP_NOT[0], + 'right': parse_binary(tree["argument"])} + else: + raise Exception( + 'Expected a binary expression, but found unknown ' + 'UnaryExpression type in input. ') + + # check for binary logical connectives + if tree["type"] == 'BinaryExpression' \ + or tree["type"] == 'LogicalExpression': + + # check for a logical and + if tree["operator"] in OP_AND: + return {'operation': OP_AND[0], 'left': parse_binary(tree["left"]), + 'right': parse_binary(tree["right"])} + + # check for a logical or + if tree["operator"] in OP_OR: + return {'operation': OP_OR[0], 'left': parse_binary(tree["left"]), + 'right': parse_binary(tree["right"])} + + # check for a logical nand + if tree["operator"] in OP_NAND: + return {'operation': OP_NAND[0], + 'left': parse_binary(tree["left"]), + 'right': parse_binary(tree["right"])} + + # check for a logical xor + if tree["operator"] in OP_XOR: + return {'operation': OP_XOR[0], 'left': parse_binary(tree["left"]), + 'right': parse_binary(tree["right"])} + + # check for binary filter + if tree["type"] == 'BinaryExpression' \ + or tree["type"] == 'LogicalExpression': + # an equals filter + if tree["operator"] in OP_EQUALS: + return {'operation': OP_EQUALS[0], + 'key': parse_primitive(tree["left"]), + 'value': parse_primitive(tree["right"])} + + # a less filter + if tree["operator"] in OP_LESS: + return {'operation': OP_LESS[0], + 'key': parse_primitive(tree["left"]), + 'value': parse_primitive(tree["right"])} + + # a less or equals filter + if tree["operator"] in OP_LESS_EQUAL: + return {'operation': OP_LESS_EQUAL[0], + 'key': parse_primitive(tree["left"]), + 'value': parse_primitive(tree["right"])} + + # a greater filter + if tree["operator"] in OP_GREATER: + return {'operation': OP_GREATER[0], + 'key': parse_primitive(tree["left"]), + 'value': parse_primitive(tree["right"])} + + # a greater or equals filter + if tree["operator"] in OP_GREATER_EQUAL: + return {'operation': OP_GREATER_EQUAL[0], + 'key': parse_primitive(tree["left"]), + 'value': parse_primitive(tree["right"])} + + # a contains filter + if tree["operator"] in OP_CONTAINS: + return {'operation': OP_CONTAINS[0], + 'key': parse_primitive(tree["left"]), + 'value': parse_primitive(tree["right"])} + + # a matches filter + if tree["operator"] in OP_MATCHES: + return {'operation': OP_MATCHES[0], + 'key': parse_primitive(tree["left"]), + 'value': parse_primitive(tree["right"])} + + raise Exception( + 'Expected a binary expression, but found unknown ' + 'BinaryExpression / LogicalExpression in input. ') + + is_primitive = False + + try: + parse_primitive(tree) + is_primitive = True + except: + pass + + if is_primitive: + raise Exception( + "Expected a binary expression, but found a primitive instead. ") + else: + raise Exception( + "Expected a binary expression, but found unknown expression type " + "in input. ") + + +def parse_primitive(tree): + """ + A primitive expression is either: + + 1. A ThisExpression ( ==> this ) + 1. A literal expression ( ==> literal.raw ) + 2. A identifier expression ( ==> identifier.name ) + 3. A non-empty compound expression consisting of primitives + as above ( ==> join as strings ) + + """ + + # if there is nothing, return + if not tree: + raise Exception('Missing binary expression. ') + + # check for a literal expression + if (tree["type"] == 'Literal'): + if isinstance(tree["value"], str): + return tree["value"] + else: + return tree["raw"] + + # check for an identifier + if tree["type"] == "Identifier": + return tree["name"] + + # in case of a compound expression + if tree["type"] == "CompoundExpression": + return " ".join([parse_primitive(e) for e in tree["body"]]) + + raise Exception( + "Expected a primitive expression, but found unknown expression type " + "in input. ") + + +def parse(s): + """ + Parses a string into an expression + """ + + ast = parser.parse(s) + + return parse_binary(ast) + + +def matches_logical(tree, obj): + """ Check if an object matches a logical tree """ + + # operation of the tree + op = tree['operation'] + + # constants + if op == OP_TRUE[0]: + return True + + if op == OP_FALSE[0]: + return False + + # not + if op == OP_NOT[0]: + return not matches_logical(tree["right"], obj) + + # binary operations + if op == OP_AND[0]: + return matches_logical(tree['left'], obj) and matches_logical( + tree['right'], obj) + + if op == OP_NAND[0]: + return not ( + matches_logical(tree['left'], obj) and matches_logical( + tree['right'], + obj)) + + if op == OP_OR[0]: + return matches_logical(tree['left'], obj) or matches_logical( + tree['right'], obj) + + if (op == OP_XOR[0]): + return matches_logical(tree['left'], obj) != matches_logical( + tree['right'], obj) + + return matches_filter(tree, obj) + + +def match_toString(obj): + if obj is bool(obj): + return "true" if obj is True else "false" + return str(obj) + + +def match_toFloat(obj): + return float(obj) + + +def matches_filter(tree, obj): + """ Check if a tree matches a filter + + :param tree: + :param obj: + :return: + """ + + # find the operation + op = tree['operation'] + + # find the key and value to check. + key = tree['key'] + value = tree['value'] + + # if the object does not have the property, we can exit immediatly + if (key not in obj): + return False + + # read the key from the object + obj_value = obj[key] + + # equality: check if the objects are equal as strings. + if op in OP_EQUALS: + return match_toString(obj_value) == match_toString(value) + + # numeric comparisions + if op in OP_LESS: + try: + value = match_toFloat(value) + obj_value = match_toFloat(obj_value) + except: + return False + + return obj_value < value + + if op in OP_LESS_EQUAL: + try: + value = match_toFloat(value) + obj_value = match_toFloat(obj_value) + except: + return False + + return obj_value <= value + + if op in OP_GREATER: + try: + value = match_toFloat(value) + obj_value = match_toFloat(obj_value) + except: + return False + + return obj_value > value + + if (op == OP_GREATER_EQUAL[0]): + try: + value = match_toFloat(value) + obj_value = match_toFloat(obj_value) + except: + return False + + return obj_value >= value + + # check if we match a regular expression + if op in OP_MATCHES: + try: + value = re.compile(value) + except: + return False + + return bool(value.match(match_toString(obj_value))) + + # check if a value is contained in this array + if op in OP_CONTAINS: + return value in obj_value + + # and thats it + return False + + +def matches(tree, obj): + """Checks if a tree matches an object""" + + return matches_logical(tree, obj) + + +op_list = { + 'OP_TRUE': OP_TRUE, + 'OP_FALSE': OP_FALSE, + + 'OP_NOT': OP_NOT, + + 'OP_EQUALS': OP_EQUALS, + + 'OP_LESS': OP_LESS, + 'OP_LESS_EQUAL': OP_LESS_EQUAL, + + 'OP_GREATER': OP_GREATER, + 'OP_GREATER_EQUAL': OP_GREATER_EQUAL, + + 'OP_CONTAINS': OP_CONTAINS, + + 'OP_MATCHES': OP_MATCHES, + + 'OP_AND': OP_AND, + + 'OP_OR': OP_OR, + + 'OP_NAND': OP_NAND, + + 'OP_XOR': OP_XOR, + + 'OPS_UNARY': OPS_UNARY, + 'OPS_FILTERS': OPS_FILTERS, + 'OPS_BINARY': OPS_BINARY +} + +__all__ = ['parse', 'matches', 'op_list'] diff --git a/filters/forest/renderer.py b/filters/forest/renderer.py new file mode 100644 index 0000000..8ccf0d9 --- /dev/null +++ b/filters/forest/renderer.py @@ -0,0 +1,109 @@ +import re + +from django.utils.html import escape + + +def classescape(cls): + return re.sub(r"\s", "_", cls).upper() + + +def renderer(layout): + # the grid which contains the actual data + grid = layout["grid"] + + # dimensions + height = layout["size"][0] + width = layout["size"][1] + + table = "" + + for i in range(height): + tr = "" + + for j in range(width): + node = grid[i][j] + td = "" + tr += td + + # close the table row and add it to the table + tr += "" + table += tr + + # close the table + table += "
" + + # if the node is empty, do nothing. + if node["type"] == "empty": + pass + + # if the node is a connection, draw the right arrows in the + # right direction + elif node["type"] == "conn": + + # for a double connection we need to add two divs + if node["prop"]["class"] == "tree_connect_top_lr": + td += "
" + td += "
" + # for a single connection we need to add one div + else: + td += "
" + else: + if "is_filter" in node["prop"] and node["prop"]["is_filter"]: + key = escape(node["prop"]["key"]) + value = escape(node["prop"]["value"]) + + box = """
+
{}
+
+
{}
+
+ """.format(classescape(node["prop"]["op"]), key, value) + else: + box = """
+
+
+ """.format(classescape(node["prop"]["op"])) + + td += renderBox(box, node["prop"]["input"], + node["prop"]["output"]) + + # close the cell and add it to the row + td += "
" + return table + + +def renderStatusNode(addClass, status): + return "
" + + +def renderBox(contentNode, inStatus, outStatus): + # make a box to contain all the other items + box = "
" + + # render a box for the output + box += renderStatusNode("status_node_center", outStatus) + + # make a central box for the content + box += "
" + contentNode + "
" + + # make two nodes for input / output + if (len(inStatus) == 1): + box += renderStatusNode("status_node_center", inStatus[0]) + elif (len(inStatus) == 2): + box += renderStatusNode("status_node_left", inStatus[0]) + box += renderStatusNode("status_node_right", inStatus[1]) + + # close the box + box += "
" + + # and return it. + return box diff --git a/filters/models.py b/filters/models.py index dca5c8a..b706068 100644 --- a/filters/models.py +++ b/filters/models.py @@ -8,7 +8,8 @@ from settings.models import VotingSystem -import filters.forest as forest +from filters.forest import logic +from jay import utils # Create your models here. @@ -23,7 +24,7 @@ def __str__(self): def clean(self): try: - self.tree = json.dumps(forest.parse_and_simplify(self.value)) + self.tree = json.dumps(logic.parse(self.value)) except Exception as e: self.tree = None @@ -41,23 +42,29 @@ def matches(self, obj): """ try: - return forest.matches(json.loads(self.tree), obj) + return logic.matches(json.loads(self.tree), obj) except Exception as e: import sys sys.stderr.write(e) return False - def map_matches(self, objs): + def count_matches(self, objs): + """ Counts the number of objects matching this filter""" - try: - return forest.map_match(json.loads(self.tree), objs) - except Exception as e: - return False + tree = json.loads(self.tree) + c = 0 + + for obj in objs: + try: + if logic.matches(tree, obj): + c += 1 + except: + pass + + return c def canEdit(self, user): - """ - Checks if a user can edit this UserFilter. - """ + """ Checks if a user can edit this UserFilter. """ return self.system.isAdmin(user) diff --git a/filters/templatetags/filter.py b/filters/templatetags/filter.py index f794437..a02fdac 100644 --- a/filters/templatetags/filter.py +++ b/filters/templatetags/filter.py @@ -1,5 +1,7 @@ from django import template -import filters.forest as forest +from django.utils.safestring import mark_safe + +from filters.forest import logic, layouter, renderer import json register = template.Library() @@ -11,16 +13,16 @@ def render_full(src, inp): obj = json.loads(inp) # parse the source code - tree = forest.parse(src) + tree = logic.parse(src) # make a layout with the given object - layout = forest.layouter(tree, obj) + layout = layouter.layouter(tree, obj) # and finally render it - render = forest.renderer(layout) + render = renderer.renderer(layout) # that is what we return - return render + return mark_safe(render) @register.simple_tag(takes_context=False) @@ -31,4 +33,4 @@ def render_lbox(name, inp, out): inp = list(map(lambda x: x == '1', str(inp))) out = (out == '1') - return forest.renderer_box(box, inp, out) + return mark_safe(renderer.renderBox(box, inp, out)) diff --git a/filters/views.py b/filters/views.py index deec2a2..b6fcf2f 100644 --- a/filters/views.py +++ b/filters/views.py @@ -10,13 +10,14 @@ from filters.models import UserFilter from filters.forms import NewFilterForm, EditFilterForm, FilterTestForm, \ FilterTestUserForm -import filters.forest as forest -import json +from filters.forest import logic from votes.models import VotingSystem -from jay.utils import priviliged +from jay.utils import elevated, is_elevated, get_user_details + +import json FILTER_FOREST_TEMPLATE = "filters/filter_forest.html" FILTER_EDIT_TEMPLATE = "filters/filter_edit.html" @@ -24,10 +25,10 @@ @login_required -@priviliged +@elevated def Forest(request, alert_type=None, alert_head=None, alert_text=None): # if the user does not have enough priviliges, throw an exception - if not request.user.profile.isElevated(): + if not is_elevated(request.user): raise PermissionDenied # build a new context @@ -40,7 +41,7 @@ def Forest(request, alert_type=None, alert_head=None, alert_text=None): {'url': reverse('filters:forest'), 'text': 'Filters', 'active': True}) ctx['breadcrumbs'] = bc - (admin_systems, other_systems) = request.user.profile.getSystems() + (admin_systems, other_systems) = VotingSystem.splitSystemsFor(request.user) # give those to the view ctx['admin_systems'] = admin_systems @@ -79,7 +80,7 @@ def FilterNew(request): # check if the user can edit it. # if not, go back to the overview - if not system.isAdmin(request.user.profile): + if not system.isAdmin(request.user): return Forest(request, alert_head="Creation failed", alert_text="Nice try. You are not allowed to edit " "this VotingSystem. ") @@ -115,7 +116,7 @@ def FilterDelete(request, filter_id): # check if the user can edit it. # if not, go back to the overview - if not system.isAdmin(request.user.profile): + if not system.isAdmin(request.user): return Forest(request, alert_head="Deletion failed", alert_text="Nice try. You don't have permissions to " "delete this filter. ") @@ -139,7 +140,7 @@ def FilterDelete(request, filter_id): @login_required -@priviliged +@elevated def FilterEdit(request, filter_id): # make a context ctx = {} @@ -149,7 +150,7 @@ def FilterEdit(request, filter_id): ctx["filter"] = filter # check if the user can edit it - if not filter.canEdit(request.user.profile): + if not filter.canEdit(request.user): raise PermissionDenied # Set up the breadcrumbs @@ -175,7 +176,7 @@ def FilterEdit(request, filter_id): # check if we have a valid tree manually try: - tree = forest.parse(form.cleaned_data['value']) + tree = logic.parse(form.cleaned_data['value']) if not tree: raise Exception except Exception as e: @@ -207,7 +208,7 @@ def FilterEdit(request, filter_id): @login_required -@priviliged +@elevated def FilterTest(request, filter_id, obj=None): # try and grab the user filter filter = get_object_or_404(UserFilter, id=filter_id) @@ -244,7 +245,7 @@ def FilterTest(request, filter_id, obj=None): @login_required -@priviliged +@elevated def FilterTestUser(request, filter_id): # try and grab the user filter filter = get_object_or_404(UserFilter, id=filter_id) @@ -259,7 +260,8 @@ def FilterTestUser(request, filter_id): form = FilterTestUserForm(request.POST) if form.is_valid(): obj = form.cleaned_data["user"] - obj = User.objects.filter(username=obj)[0].profile.details + obj = json.dumps(get_user_details( + User.objects.filter(username=obj)[0])) except Exception as e: print(e) pass diff --git a/jay/allauthurls/__init__.py b/jay/allauthurls/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/jay/allauthurls/main.py b/jay/allauthurls/main.py new file mode 100644 index 0000000..fb92487 --- /dev/null +++ b/jay/allauthurls/main.py @@ -0,0 +1,20 @@ +from django.conf.urls import url, include +from allauth.compat import importlib + +from allauth.socialaccount import providers + +from allauth import app_settings + +urlpatterns = [url('^', include('allauth.account.urls'))] + +if app_settings.SOCIALACCOUNT_ENABLED: + urlpatterns += [url('^social/', include('jay.allauthurls.socialaccounts'))] + +for provider in providers.registry.get_list(): + try: + prov_mod = importlib.import_module(provider.get_package() + '.urls') + except ImportError: + continue + prov_urlpatterns = getattr(prov_mod, 'urlpatterns', None) + if prov_urlpatterns: + urlpatterns += prov_urlpatterns diff --git a/jay/allauthurls/socialaccounts.py b/jay/allauthurls/socialaccounts.py new file mode 100644 index 0000000..6bf8c79 --- /dev/null +++ b/jay/allauthurls/socialaccounts.py @@ -0,0 +1,8 @@ +from django.conf.urls import url +from allauth.socialaccount import views + +urlpatterns = [ + url('^login/cancelled/$', views.login_cancelled, + name='socialaccount_login_cancelled'), + url('^login/error/$', views.login_error, name='socialaccount_login_error') +] diff --git a/jay/settings.py b/jay/settings.py index 825a9c0..ea1bef2 100644 --- a/jay/settings.py +++ b/jay/settings.py @@ -25,7 +25,15 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.contrib.sites', + 'django_forms_bootstrap', + + 'allauth', + 'allauth.account', + 'allauth.socialaccount', + 'dreamjub.providers.oauth', + 'filters', 'settings', 'users', @@ -65,13 +73,22 @@ WSGI_APPLICATION = 'jay.wsgi.application' # OpenJUB auth -AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend', - 'users.ojub_auth.OjubBackend') +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', + 'allauth.account.auth_backends.AuthenticationBackend', +) + +# the token and secret for oauth +DREAMJUB_CLIENT_URL = 'http://localhost:9000/' +DREAMJUB_CLIENT_ID = '7ZYpfROz1AEUDbsTQJJuy4RNL8LVSRTONOAXcjm4' +DREAMJUB_CLIENT_SECRET = 'EkeeT0GgOGOQRHbgFE4n1RERxVNSiOD1HZ80TRfERiWj3cK1hZ' \ + 'oTodH0kv8tz3gbqk53YMDuUFAsoaJkMEg1OM1RrZyd1xaYUGv5' \ + 'CtmHrpmJavc2JvRDNUAkFJgORpUW' # Default after login redirect # These are named URL routes -LOGIN_URL = "login" -LOGOUT_URL = "logout" +LOGIN_URL = "/accounts/dreamjub/login" +LOGOUT_URL = "/logout" LOGIN_REDIRECT_URL = "home" # Internationalization @@ -91,3 +108,6 @@ ) STATIC_URL = '/static/' +SITE_ID = 1 + +ACCOUNT_EMAIL_VERIFICATION = "none" diff --git a/jay/urls.py b/jay/urls.py index 34ffe79..be53506 100644 --- a/jay/urls.py +++ b/jay/urls.py @@ -49,8 +49,7 @@ name="filter_help"), # Authentication - url(r'^login/', auth_views.login, {'template_name': 'auth/login.html'}, - name="login"), + url(r'^accounts/', include('jay.allauthurls.main'), name='login'), url(r'^logout/', auth_views.logout, {'template_name': 'auth/logout.html', 'next_page': 'home'}, name="logout"), diff --git a/jay/utils.py b/jay/utils.py index 698bda5..41ced99 100644 --- a/jay/utils.py +++ b/jay/utils.py @@ -13,24 +13,48 @@ def helper(*x): return helper +def is_superadmin(user): + """ Checks if a user is a superadmin. """ + return user.is_superuser + + +def is_elevated(user): + """ Checks if a user is an admin for some voting system """ + if not user.is_superuser: + if not user.admin_set.count() > 0: + return False + return True + + def superadmin(handler): - """ Checks if a user is a super admin. """ + """ Requires a user to be a superadmin. """ def helper(request, *args, **kwargs): - if not request.user.profile.isSuperAdmin(): + if not is_superadmin(request.user): raise PermissionDenied return handler(request, *args, **kwargs) return helper -def priviliged(handler): - """ Checks that a user has elevated privileges. """ +def elevated(handler): + """ Requires a user to be elevated user """ def helper(request, *args, **kwargs): - if not request.user.profile.isElevated(): + if not is_elevated(request.user): raise PermissionDenied return handler(request, *args, **kwargs) return helper + + +def get_user_details(user): + """ Gets a dict() object representing user data """ + + try: + data = user.socialaccount_set.get(provider="dreamjub").extra_data + return data + except: + # fallback to an in-active user with just a username flag + return {'username': user.username, 'active': False} diff --git a/requirements.txt b/requirements.txt index 8b63007..b002762 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,10 +8,14 @@ django-forms-bootstrap==3.0.1 Markdown==2.6.5 # For filters -PyExecJS==1.1.0 - -# For OpenJUB auth backend -requests==2.6.0 +pre-js-py==1.1.0 # For testing pep -pep8 >= 1.7.0 \ No newline at end of file +pep8 >= 1.7.0 + +# For authing against dreamjub +django-allauth==0.29.0 +django-allauth-dreamjub==0.1.3 + +# for counting users +requests-oauthlib==0.8.0 diff --git a/settings/models.py b/settings/models.py index cfeea0a..8f8e1bc 100644 --- a/settings/models.py +++ b/settings/models.py @@ -3,6 +3,8 @@ from django.contrib import admin from jay.restricted import is_restricted_word +from jay.utils import is_superadmin +from users.models import Admin class VotingSystem(models.Model): @@ -16,16 +18,47 @@ def clean(self): is_restricted_word('machine_name', self.machine_name) def canEdit(self, user): - """ - Checks if a user can edit this voting system. - """ - return user.isSuperAdmin() + """ Checks if a user can edit this voting system. """ + + return is_superadmin(user) def isAdmin(self, user): - """ - Checks if a user is an administrator for this voting system. - """ - return user.isAdminFor(self) + """ Checks if a user is an administrator for this voting system. """ + if user.is_anonymous: + return False + return self.canEdit(user) or \ + Admin.objects.filter(system=self, user=user).exists() + + @classmethod + def getAdminSystems(cls, user): + """ returns a pair (admin, others) of systems for a given user """ + + # if a user is a super admin all of them + if is_superadmin(user): + return VotingSystem.objects.all() + if user.is_anonymous: + return VotingSystem.objects.none() + + return VotingSystem.objects.filter(admin__user=user) + + @classmethod + def splitSystemsFor(cls, user): + """ returns a pair (admin, others) of systems for a given user """ + + # if a user is a super admin, return (all, none) + if is_superadmin(user): + return VotingSystem.objects.all(), VotingSystem.objects.none() + + # if are anonymous return (none, all) + if user.is_anonymous: + return VotingSystem.objects.none(), VotingSystem.objects.all() + + # else we need two quieries + administered = VotingSystem.objects.filter(admin__user=user) + others = VotingSystem.objects.exclude(admin__user=user) + + # and return both QuerySets + return administered, others admin.site.register(VotingSystem) diff --git a/settings/urls.py b/settings/urls.py index db501ca..a0de000 100644 --- a/settings/urls.py +++ b/settings/urls.py @@ -1,15 +1,15 @@ from django.conf.urls import url from django.views.generic import TemplateView -from settings.views import superadmins, systems +from settings.views import systems urlpatterns = [ # Superadmin management - url(r'^$', superadmins.settings, name="settings"), - url(r'^superadmins/add$', superadmins.superadmin_add, name="add"), - url(r'^superadmins/(?P[\w-]+)/remove$', - superadmins.superadmin_remove, name="remove"), + # url(r'^$', superadmins.settings, name="settings"), + # url(r'^superadmins/add$', superadmins.superadmin_add, name="add"), + # url(r'^superadmins/(?P[\w-]+)/remove$', + # superadmins.superadmin_remove, name="remove"), # System management url(r'^systems$', systems.systems, name='systems'), diff --git a/settings/views/systems.py b/settings/views/systems.py index 85f5f6c..5c1b580 100644 --- a/settings/views/systems.py +++ b/settings/views/systems.py @@ -52,7 +52,6 @@ def system_edit(request, system_id): except Exception as e: ctx['alert_head'] = 'Saving failed' ctx['alert_text'] = 'Invalid data submitted' - print(e) return render(request, SETTINGS_SYSTEMS_EDIT_TEMPLATE, ctx) diff --git a/static/js/forest/logic.js b/static/js/forest/logic.js index 104b0cc..5a45508 100644 --- a/static/js/forest/logic.js +++ b/static/js/forest/logic.js @@ -331,207 +331,6 @@ return parse_binary(ast); }; - // simplifies a boolean expression. - var simplify = function (obj) { - - var left, left_op; - var right, right_op; - - if (obj['operation'] == OP_AND[0]) { - // simplifiy on the left and on the right - left = simplify(obj['left']); - right = simplify(obj['right']); - - // find left and right operations - left_op = left['operation']; - right_op = right['operation']; - - // if the right operation is a constant - if (right_op == OP_TRUE[0]){ - return left; - } - - if (right_op == OP_FALSE[0]){ - return {'operation': OP_FALSE[0]}; - } - - if (left_op == OP_TRUE[0]){ - return right; - } - - if (left_op == OP_FALSE[0]){ - return {'operation': OP_FALSE[0]}; - } - - // else return the cleaned operation - return {'operation': OP_AND[0], 'left': left, 'right': right} - } - - if(obj['operation'] == OP_NAND[0]){ - // simplify on the left and on the right - left = simplify(obj['left']); - right = simplify(obj['right']); - - // find left and right operations - left_op = left['operation']; - right_op = right['operation']; - - // if the right operation is a constant - if(right_op == OP_TRUE[0]){ - return simplify({ - operation: OP_NOT[0], - 'right': left - }); - } - - if(right_op == OP_FALSE[0]){ - return {'operation': OP_TRUE[0]}; - } - - - // if the left operation is a constant - if (left_op == OP_TRUE[0]){ - return simplify({ - operation: OP_NOT[0], - 'right': right - }); - } - - if(left_op == OP_FALSE[0]){ - return {'operation': OP_TRUE[0]}; - } - - // else return the cleaned operation - return {'operation': OP_NAND[0], 'left': left, 'right': right} - } - - - if(obj['operation'] == OP_OR[0]){ - // simplify on the left and on the right - left = simplify(obj['left']); - right = simplify(obj['right']); - - // find left and right operations - left_op = left['operation']; - right_op = right['operation']; - - // if the right operation is a constant - if(right_op == OP_TRUE[0]){ - return {'operation': OP_TRUE[0]}; - } - - if(right_op == OP_FALSE[0]){ - return left; - } - - - // if the left operation is a constant - if(left_op == OP_TRUE[0]){ - return {'operation': OP_TRUE[0]}; - } - - if(left_op == OP_FALSE[0]){ - return right; - } - - - // else return the cleaned operation - return {'operation': OP_OR[0], 'left': left, 'right': right}; - } - - if(obj['operation'] == OP_XOR[0]){ - - // simplify on the left and on the right - left = simplify(obj['left']); - right = simplify(obj['right']); - - // find left and right operations - left_op = left['operation']; - right_op = right['operation']; - - // if the right operation is a constant - if(right_op == OP_TRUE[0]){ - return simplify({ - 'operation': OP_NOT[0], - 'right': left - }); - } - - if(right_op == OP_FALSE[0]){ - return left; - } - - - // if the left operation is a constant - if (left_op == OP_TRUE[0]) { - return simplify({ - 'operation': OP_NOT[0], - 'right': right - }); - } - - if (left_op == OP_FALSE[0]) { - return right; - } - - // else return the cleaned operation - return {'operation': OP_XOR[0], 'left': left, 'right': right}; - } - - if(obj['operation'] == OP_NOT[0]) { - // simplify the sub operation - right = simplify(obj['right']); - - // find the operation on the right - right_op = right['operation']; - - // remove true / false - if(right_op == OP_TRUE[0]) { - return {'operation': OP_FALSE[0]}; - } - - if(right_op == OP_FALSE[0]) { - return {'operation': OP_TRUE[0]}; - } - - // remove double nots - if(right_op == OP_NOT[0]){ - return simplify(right['right']); - } - - // ! nand => and - if( right_op == OP_NAND[0]){ - return { - 'operation': OP_AND[0], - 'left': right['left'], - 'right': right['right'] - }; - }; - - // ! and => nand - if(right_op == OP_AND[0]){ - return { - 'operation': OP_NAND[0], - 'left': right['left'], - 'right': right['right'] - }; - } - - // ! or(a, b) = and(!a, !b) - if (right_op == OP_OR[0]){ - return { - 'operation': OP_AND[0], - 'left': simplify({operation:OP_NOT[0], right: right['left']}), - 'right': simplify({operation:OP_NOT[0], right: right['right']}), - } - } - - return {'operation': OP_NOT[0], 'right': simplify(obj['right'])} - } - // otherwise it is a filter, so return as is. - return obj - }; - var matches_logical = function (tree, obj) { // operation of the tree @@ -685,7 +484,6 @@ // exports the entire thing in a namespace var logic = { 'parse': parse, - 'simplify': simplify, 'matches': matches, 'op_list': { diff --git a/templates/auth/login.html b/templates/auth/login.html deleted file mode 100644 index aadffe3..0000000 --- a/templates/auth/login.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "base/base.html" %} - -{% block page_title %}Login{% endblock %} - -{% block content %} - -{% if form.errors %} - -{% endif %} - -{% if next %} - {% if user.is_authenticated %} - - {% else %} - - {% endif %} -{% endif %} - -
- -
-
- -
- -
-
- -

If you lost your password, reset it in CampusNet.

- -{% endblock %} diff --git a/templates/base/base.html b/templates/base/base.html index dfa528a..6762619 100644 --- a/templates/base/base.html +++ b/templates/base/base.html @@ -1,5 +1,6 @@ {% load staticfiles %} {% load bootstrap_tags %} +{% load userflags %} @@ -43,18 +44,20 @@ {{ user.username }} {% else %} - Login + Login {% endif %} diff --git a/templates/socialaccount/authentication_error.html b/templates/socialaccount/authentication_error.html new file mode 100644 index 0000000..6f7eac2 --- /dev/null +++ b/templates/socialaccount/authentication_error.html @@ -0,0 +1,40 @@ +{% extends "socialaccount/base.html" %} + +{% load i18n %} + +{% block head_title %}{% trans "Social Network Login Failure" %}{% endblock %} + +{% block content %} +

{% trans "Social Network Login Failure" %}

+ +

{% trans "An error occurred while attempting to login via your social network account." %}

+{% endblock %} + + +{% extends "base/base.html" %} + +{% load staticfiles %} + +{% block page_title %}Login Failure{% endblock %} + +{% block content %} + +
+
+
+

Login Failure

+

+ We are very sorry to say, but an unexpected error occured while trying to login. + Please tell the smart people the following details: + +

+ Error: {{ error | safe }} + Exception: {{ exception | safe }} +
+

+

Try again

+
+
+
+ +{% endblock %} diff --git a/templates/socialaccount/login_cancelled.html b/templates/socialaccount/login_cancelled.html new file mode 100644 index 0000000..041e22d --- /dev/null +++ b/templates/socialaccount/login_cancelled.html @@ -0,0 +1,24 @@ +{% extends "base/base.html" %} + +{% load staticfiles %} + +{% block page_title %}Login Cancelled{% endblock %} + +{% block content %} + +
+
+
+

Login Cancelled

+

+ You decided to cancel logging in to Jay using Dreamjub. + Logging into the voting system is required to vote or perform other actions. + The login is used only to check if you are eligible for a vote, and to make sure you vote only once. +

+

Try again

+
+ +
+
+ +{% endblock %} diff --git a/templates/vote/fragments/option_edit.html b/templates/vote/fragments/option_edit.html index f130c4e..50c68d7 100644 --- a/templates/vote/fragments/option_edit.html +++ b/templates/vote/fragments/option_edit.html @@ -73,7 +73,7 @@

Option {{o.number}}

- +
diff --git a/templates/vote/fragments/vote_list.html b/templates/vote/fragments/vote_list.html index f73dc3b..6c7cf1f 100644 --- a/templates/vote/fragments/vote_list.html +++ b/templates/vote/fragments/vote_list.html @@ -12,13 +12,13 @@  {{ vote.name }} - {% if vote|can_delete:request.user.profile %} + {% if vote|can_delete:request.user %} {% endif %} - {% if vote|can_edit:request.user.profile %} + {% if vote|can_edit:request.user %} diff --git a/templates/vote/fragments/vote_stage.html b/templates/vote/fragments/vote_stage.html index bbd57a0..2f0a076 100644 --- a/templates/vote/fragments/vote_stage.html +++ b/templates/vote/fragments/vote_stage.html @@ -7,7 +7,7 @@
- + @@ -15,7 +15,8 @@
- Re-enter your password to stage the vote. This will prevent any further changes to be made. Staging the vote may take a few seconds. + Enter the name of this vote {{ vote.machine_name | escape }} to stage it.
+ This will prevent further edits from being made. Staging a vote may take a few seconds.
@@ -38,7 +39,8 @@
- Re-enter your password to update eligibility count. This may take a few seconds. + Enter the name of this vote, {{ vote.machine_name | escape }}, to update eligibility count. + This may take a few seconds.
diff --git a/users/models.py b/users/models.py index b8f9d93..f7e15ac 100644 --- a/users/models.py +++ b/users/models.py @@ -1,19 +1,13 @@ from django.db import models from django.contrib.auth.models import User -from django.core.exceptions import ValidationError - from django.contrib import admin -from settings.models import VotingSystem - -import json - # Create your models here. class Admin(models.Model): user = models.ForeignKey(User) - system = models.ForeignKey(VotingSystem) + system = models.ForeignKey("settings.VotingSystem") class Meta(): unique_together = (("system", "user")) @@ -22,87 +16,4 @@ def __str__(self): return u'[%s] %s' % (self.system.machine_name, self.user) -class SuperAdmin(models.Model): - user = models.ForeignKey(User) - - class Meta(): - unique_together = (("user",),) - - def __str__(self): - return u'%s' % (self.user) - - -class UserProfile(models.Model): - user = models.OneToOneField(User, related_name="profile") - details = models.TextField() - - def __str__(self): - return u'[Profile] %s' % (self.user.username) - - def clean(self): - # make sure that the details are a valid json object - try: - json.loads(self.details) - except: - raise ValidationError({ - 'details': ValidationError( - 'Details needs to be a valid JSON object', code='invalid') - }) - - def isSuperAdmin(self): - """ - Returns if this user is a SuperAdmin. - """ - return self.user.superadmin_set.count() > 0 - - def isAdminFor(self, system): - """ - Checks if this user can administer a certain voting system. - """ - return system in self.getAdministratedSystems() - - def getAdministratedSystems(self): - """ - Returns all voting systems this user can administer. - """ - # if we are a superadmin we can manage all systems - if self.isSuperAdmin(): - return VotingSystem.objects.all() - - # else return only the systems we are an admin for. - else: - return list( - map(lambda x: x.system, Admin.objects.filter(user=self.user))) - - def isElevated(self): - """ - Checks if this user is an elevated user. - - (i. e. if they are a superadmin or admin for some voting system) - """ - - # thy are a superadmin - if self.isSuperAdmin(): - return True - - # they administer some voting system - return self.user.admin_set.count() > 0 - - def getSystems(self): - """ - Gets the editable filters for this user. - """ - - # get all the voting systems for this user - admin_systems = self.getAdministratedSystems() - - # and all the other ones also - other_systems = list(filter(lambda a: a not in admin_systems, - VotingSystem.objects.all())) - - return (admin_systems, other_systems) - - admin.site.register(Admin) -admin.site.register(SuperAdmin) -admin.site.register(UserProfile) diff --git a/users/ojub_auth.py b/users/ojub_auth.py index af7a211..24113c3 100644 --- a/users/ojub_auth.py +++ b/users/ojub_auth.py @@ -1,113 +1,37 @@ from django.conf import settings -from django.contrib.auth.models import User -from users.models import UserProfile +from requests_oauthlib import OAuth2Session +from oauthlib.oauth2 import BackendApplicationClient -import requests +OPENJUB_BASE = "http://localhost:9000/" -OPENJUB_BASE = "https://api.jacobs.university/" +def get_all(): + client = BackendApplicationClient(client_id=settings.DREAMJUB_CLIENT_ID) + dreamjub = OAuth2Session(client=client) + dreamjub.fetch_token( + token_url=settings.DREAMJUB_CLIENT_URL + 'login/o/token/', + client_id=settings.DREAMJUB_CLIENT_ID, + client_secret=settings.DREAMJUB_CLIENT_SECRET) -class OjubBackend(object): - """ - Authenticates credentials against the OpenJUB database. + # iterate over the pages (while there is a next) + results = [] + next = settings.DREAMJUB_CLIENT_URL + 'api/v1/users/' - The URL for the server is configured by OPENJUB_BASE in the settings. + while next: + res = dreamjub.get(next) + if not res.ok: + raise Exception( + 'Unable to retrieve current list of students, ' + 'please try again later. ') - This class does not fill in user profiles, this has to be handled - in other places - """ + res = res.json() + results += res['results'] + next = res['next'] if 'next' in res else None - def authenticate(self, username=None, password=None): - r = requests.post(OPENJUB_BASE + "auth/signin", - data={'username': username, 'password': password}) + # replace http with https at most one + if next is not None and next.startswith('http://') and \ + settings.DREAMJUB_CLIENT_URL.startswith('https://'): + next = next.replace('http://', 'https://', 1) - if r.status_code != requests.codes.ok: - return None - - resp = r.json() - - uname = resp['user'] - token = resp['token'] - - details = requests.get(OPENJUB_BASE + "user/me", - params={'token': token}) - - if details.status_code != requests.codes.ok: - print("Could not get user details") - return None - - try: - user = User.objects.get(username=uname) - except User.DoesNotExist: - user = User(username=uname) - - user.set_unusable_password() - - # TODO Don't hardcode this - if user.username in ["lkuboschek", "twiesing", "jinzhang", - "rdeliallis"]: - user.is_staff = True - user.is_superuser = True - - data = details.json() - - user.first_name = data['firstName'] - user.last_name = data['lastName'] - user.email = data['email'] - - user.save() - - # Make a user profile if there isn't one already - try: - profile = UserProfile.objects.get(user=user) - except UserProfile.DoesNotExist: - profile = UserProfile(user=user) - - profile.details = details.text - profile.save() - - return user - - def get_user(self, user_id): - try: - return User.objects.get(pk=user_id) - except User.DoesNotExist: - return None - - -def get_all(username, password): - r = requests.post(OPENJUB_BASE + "auth/signin", - data={'username': username, 'password': password}) - - if r.status_code != requests.codes.ok: - return None - - resp = r.json() - - uname = resp['user'] - token = resp['token'] - - users = [] - - TIMEOUT = 60 - - request = requests.get(OPENJUB_BASE + "query", - params={'token': token, 'limit': 20000}, - timeout=TIMEOUT) - - while True: - if request.status_code != requests.codes.ok: - return None - else: - # read json - resjson = request.json() - - # load all the users - users += resjson["data"] - - # if there was no data or no next field, continue - if len(resjson["data"]) == 0 or not resjson["next"]: - return users - else: - request = requests.get(resjson["next"], timeout=TIMEOUT) + return results diff --git a/votes/models.py b/votes/models.py index 07c5af9..c4a2af1 100644 --- a/votes/models.py +++ b/votes/models.py @@ -1,5 +1,3 @@ -import datetime - from django.utils import timezone from django.db import models, transaction @@ -44,18 +42,16 @@ def clean(self): is_restricted_word('machine_name', self.machine_name) def canEdit(self, user): - """ - Checks if a user can edit this vote. - """ - return user.isAdminFor( - self.system) and self.status.stage != Status.PUBLIC + """ Checks if a user can edit this vote. """ + + return self.system.isAdmin(user) \ + and self.status.stage != Status.PUBLIC def canDelete(self, user): - """ - Check if a user can delete this vote. - """ - return user.isAdminFor( - self.system) and self.status.stage == Status.INIT + """ Checks if a user can delete this vote. """ + + return self.system.isAdmin(user) \ + and self.status.stage == Status.INIT def canBeModified(self): """ @@ -63,27 +59,24 @@ def canBeModified(self): """ return self.status.stage == Status.INIT - def update_eligibility(self, username, password): + def update_eligibility(self): + + # TODO: Retrieve the API key things PassiveVote.objects.get_or_create(vote=self, defaults={ 'num_voters': 0, 'num_eligible': 0}) - if self.filter is not None: + if self.filter is None: raise Exception("Missing filter. ") # this will take really long - everyone = get_all(username, password) + everyone = get_all() if not everyone: raise Exception("Invalid password or something went wrong. ") - check = self.filter.map_matches(everyone) - c = 0 - - for b in check: - if b: - c += 1 + c = self.filter.count_matches(everyone) # get or create the passive vote object (pv, _) = PassiveVote.objects.get_or_create(vote=self, defaults={ diff --git a/votes/views.py b/votes/views.py index fb8971e..e3140d1 100644 --- a/votes/views.py +++ b/votes/views.py @@ -21,14 +21,15 @@ from votes.models import Vote, Option, Status, ActiveVote, PassiveVote from filters.models import UserFilter -from users.models import UserProfile, Admin +from users.models import Admin from settings.models import VotingSystem from django.contrib.auth.models import User +from jay import utils from votes.forms import EditVoteForm, EditVoteFilterForm, \ - EditVoteOptionsForm, GetVoteOptionForm, EditVoteOptionForm, PasswordForm, \ - EditScheduleForm, AdminSelectForm + EditVoteOptionsForm, GetVoteOptionForm, EditVoteOptionForm, \ + PasswordForm, EditScheduleForm, AdminSelectForm VOTE_ERROR_TEMPLATE = "vote/vote_msg.html" VOTE_RESULT_TEMPLATE = "vote/vote_result.html" @@ -52,7 +53,7 @@ def system_home(request, system_name): all_votes = Vote.objects.filter(system=vs) - if request.user.is_authenticated() and vs.isAdmin(request.user.profile): + if request.user.is_authenticated() and vs.isAdmin(request.user): ctx['votes'] = all_votes ctx['results'] = Vote.objects.filter(system=vs, status__stage__in=[Status.PUBLIC, @@ -81,7 +82,7 @@ def admin(request, system_name, alert_type=None, alert_head=None, vs = get_object_or_404(VotingSystem, machine_name=system_name) # raise an error if the user trying to access is not an admin - if not vs.isAdmin(request.user.profile): + if not vs.isAdmin(request.user): raise PermissionDenied ctx['vs'] = vs @@ -112,7 +113,7 @@ def admin_add(request, system_name): vs = get_object_or_404(VotingSystem, machine_name=system_name) # raise an error if the user trying to access is not an admin - if not vs.isAdmin(request.user.profile): + if not vs.isAdmin(request.user): raise PermissionDenied try: @@ -151,7 +152,7 @@ def admin_remove(request, system_name): vs = get_object_or_404(VotingSystem, machine_name=system_name) # raise an error if the user trying to access is not an admin - if not vs.isAdmin(request.user.profile): + if not request.user.isAdmin(request.user): raise PermissionDenied try: @@ -228,6 +229,7 @@ def get_vote_props(ctx, vote): def vote_edit_context(request, system_name, vote_name): + from jay import utils """ Returns context and basic parameters for vote editing. """ @@ -237,14 +239,14 @@ def vote_edit_context(request, system_name, vote_name): vote.touch() # raise an error if the user trying to access is not an admin - if not system.isAdmin(request.user.profile): + if not system.isAdmin(request.user): raise PermissionDenied # make a context ctx = {} # get all the systems this user can edit - (admin_systems, other_systems) = request.user.profile.getSystems() + (admin_systems, other_systems) = VotingSystem.splitSystemsFor(request.user) # add the vote to the system ctx['vote'] = vote @@ -281,7 +283,7 @@ def vote_add(request, system_name): vs = get_object_or_404(VotingSystem, machine_name=system_name) # raise an error if the user trying to access is not an admin - if not vs.isAdmin(request.user.profile): + if not vs.isAdmin(request.user): raise PermissionDenied v = Vote() @@ -311,7 +313,7 @@ def vote_add(request, system_name): def vote_delete(request, system_name, vote_name): (system, vote, ctx) = vote_edit_context(request, system_name, vote_name) - if vote.canDelete(request.user.profile): + if vote.canDelete(request.user): vote.delete() return redirect('votes:system', system_name=system_name) @@ -440,18 +442,18 @@ def vote_stage(request, system_name, vote_name): if not form.is_valid(): raise Exception - # read username + password - username = request.user.username - password = form.cleaned_data['password'] + # make sure that the name of the vote has been submitted + if form.cleaned_data['password'] != vote_name: + raise Exception except: - ctx['alert_head'] = 'Saving failed' + ctx['alert_head'] = 'Staging vote failed' ctx['alert_text'] = 'Invalid data submitted' return render(request, VOTE_EDIT_TEMPLATE, ctx) # set the vote status to public try: - vote.update_eligibility(username, password) + vote.update_eligibility() vote.status.stage = Status.STAGED vote.status.save() @@ -499,7 +501,6 @@ def vote_time(request, system_name, vote_name): public_time = form.cleaned_data["public_time"] except Exception as e: - print(e, form.errors) ctx['alert_head'] = 'Saving failed' ctx['alert_text'] = 'Invalid data submitted' return render(request, VOTE_EDIT_TEMPLATE, ctx) @@ -547,18 +548,18 @@ def vote_update(request, system_name, vote_name): if not form.is_valid(): raise Exception - # read username + password - username = request.user.username - password = form.cleaned_data['password'] + # make sure that the name of the vote has been submitted + if form.cleaned_data['password'] != vote_name: + raise Exception except: ctx['alert_head'] = 'Saving failed' ctx['alert_text'] = 'Invalid data submitted' return render(request, VOTE_EDIT_TEMPLATE, ctx) - # set the vote status to public + # re-count if eligible try: - vote.update_eligibility(username, password) + vote.update_eligibility() except Exception as e: ctx['alert_head'] = 'Updating eligibility failed. ' ctx['alert_text'] = str(e) @@ -1013,7 +1014,7 @@ def results(request, system_name, vote_name): if vote.status.stage != Status.PUBLIC: if vote.status.stage == Status.CLOSE and \ request.user.is_authenticated(): - if vote.system.isAdmin(request.user.profile): + if vote.system.isAdmin(request.user): ctx['alert_type'] = 'info' ctx['alert_head'] = 'Non-public' ctx['alert_text'] = 'The results are not public yet. You ' \ @@ -1060,7 +1061,7 @@ def get(self, request, system_name, vote_name): # TODO Check status of vote try: - user_details = json.loads(request.user.profile.details) + user_details = utils.get_user_details(request.user) if not filter: ctx['alert_head'] = "No filter given." @@ -1157,7 +1158,7 @@ def post(self, request, system_name, vote_name): return self.render_error_response(ctx) try: - user_details = json.loads(request.user.profile.details) + user_details = utils.get_user_details(request.user) if not filter: ctx['alert_head'] = "No filter given."