]> WPIA git - motion.git/commitdiff
add: implement proxy handling
authorINOPIAE <m.maengel@inopiae.de>
Fri, 14 Jun 2019 13:38:12 +0000 (15:38 +0200)
committerINOPIAE <m.maengel@inopiae.de>
Mon, 5 Oct 2020 19:19:30 +0000 (21:19 +0200)
Only implements the setting and revoking of proxy.
The proxy voting is done in a later patch.

README.md
config.py.example
motion.py
sql/from_3.sql [new file with mode: 0644]
sql/sample_data.sql
sql/schema.sql
templates/base.html
templates/index.html
templates/proxy.html [new file with mode: 0644]
tests/test_motion.py

index e06c09e865173400a41156b66c1d2ea491c9761c..18df67fdae1672541b8e4705a7810fa2e90677bc 100644 (file)
--- a/README.md
+++ b/README.md
@@ -39,6 +39,7 @@ The following user rights can be granted:
 - cancel: user is able to cancel a running motion
 - finish: user is able to close a running motion
 - audit: user is able to see given votes of a finished motion
 - cancel: user is able to cancel a running motion
 - finish: user is able to close a running motion
 - audit: user is able to see given votes of a finished motion
+- proxyadmin: user is able to grant proxy rights for users
 
 To grant right use the following (here with vote right as example):
 - on all groups add "vote:*"
 
 To grant right use the following (here with vote right as example):
 - on all groups add "vote:*"
index 5bbdb4d9c77b133ac9a4975b156e4e9ee3746194..83cb5fb83a499aa7a98ca328062b4d291aa8aa88 100644 (file)
@@ -13,6 +13,8 @@ DEFAULT_HOST="hostname for migration from db version < 2"
 
 GROUP_PREFIX={'hostname': {'group1': 'g1', 'group2': 'g2'}} # motion types and their id-prefixes
 
 
 GROUP_PREFIX={'hostname': {'group1': 'g1', 'group2': 'g2'}} # motion types and their id-prefixes
 
+MAX_PROXY=2  # user is allowed to hold up to MAX_PROXY votes
+
 DURATION={'hostname':[3, 7, 14]} # duration period for motions
 
 #DEBUGUSER={'hostname':'username/create:* vote:*'} # remove # at beginning of line to use local debuguser
 DURATION={'hostname':[3, 7, 14]} # duration period for motions
 
 #DEBUGUSER={'hostname':'username/create:* vote:*'} # remove # at beginning of line to use local debuguser
index 80ac5a36029eb728e0bf7031fb0404caf0397c8e..0503b829722ea6831a5a7ac0fa5b071ea33cccaf 100644 (file)
--- a/motion.py
+++ b/motion.py
@@ -53,6 +53,8 @@ prefix = ConfigProxy("GROUP_PREFIX")
 times = ConfigProxy("DURATION")
 debuguser = ConfigProxy("DEBUGUSER")
 
 times = ConfigProxy("DURATION")
 debuguser = ConfigProxy("DEBUGUSER")
 
+max_proxy=app.config.get("MAX_PROXY")
+
 @app.before_request
 def lookup_user():
     global prefix
 @app.before_request
 def lookup_user():
     global prefix
@@ -88,6 +90,17 @@ def lookup_user():
             db.prepare("INSERT INTO voter(\"email\") VALUES($1)")(user)
             rv = db.prepare("SELECT id FROM voter WHERE email=$1")(user)
         g.voter = rv[0].get("id");
             db.prepare("INSERT INTO voter(\"email\") VALUES($1)")(user)
             rv = db.prepare("SELECT id FROM voter WHERE email=$1")(user)
         g.voter = rv[0].get("id");
+        rv = db.prepare("SELECT email FROM voter, proxy WHERE proxy.proxy_id = voter.id AND proxy.revoked IS NULL AND proxy.voter_id = $1 ")(g.voter)
+        if len(rv) != 0:
+            g.proxies_given = rv[0].get("email")
+        rv = db.prepare("SELECT email FROM voter, proxy WHERE proxy.voter_id = voter.id AND proxy.revoked IS NULL AND proxy.proxy_id = $1 ")(g.voter)
+        if len(rv) != 0:
+            sep = ""
+            g.proxies_received = ""
+            for x in range(0, len(rv)):
+                g.proxies_received += sep + rv[x].get("email")
+                sep =", "
+
     g.user = user
     g.roles = {}
 
     g.user = user
     g.roles = {}
 
@@ -101,6 +114,7 @@ def lookup_user():
                 g.roles[a[0]] = [group for group in prefix.per_host]
             else:
                 g.roles[a[0]].append(val)
                 g.roles[a[0]] = [group for group in prefix.per_host]
             else:
                 g.roles[a[0]].append(val)
+
     return None
 
 @app.context_processor
     return None
 
 @app.context_processor
@@ -126,6 +140,17 @@ def get_allowed_cats(action):
 def may(action, motion):
     return motion in get_allowed_cats(action)
 
 def may(action, motion):
     return motion in get_allowed_cats(action)
 
+def may_admin(action):
+    return action in g.roles
+
+def get_voters():
+    rv = get_db().prepare("SELECT email FROM voter")
+    return rv
+
+def get_all_proxies():
+    rv = get_db().prepare("SELECT p.id as id, v1.email as voter_email, v1.id as voterid, v2.email as proxy_email, v2.id as proxyid FROM voter AS v1, voter AS v2, proxy AS p WHERE v2.id = p.proxy_id AND v1.id = p.voter_id AND p.revoked is NULL ORDER BY voter_email, proxy_email")
+    return rv
+
 @app.teardown_appcontext
 def close_connection(exception):
     db = getattr(g, '_database', None)
 @app.teardown_appcontext
 def close_connection(exception):
     db = getattr(g, '_database', None)
@@ -175,8 +200,14 @@ def init_db():
                 db.prepare("ALTER TABLE \"motion\" ALTER COLUMN \"host\" SET NOT NULL")()
                 db.prepare("UPDATE \"schema_version\" SET \"version\"=3")()
 
                 db.prepare("ALTER TABLE \"motion\" ALTER COLUMN \"host\" SET NOT NULL")()
                 db.prepare("UPDATE \"schema_version\" SET \"version\"=3")()
 
+        if ver < 4:
+            with app.open_resource('sql/from_3.sql', mode='r') as f:
+                db.execute(f.read())
+                db.prepare("UPDATE \"schema_version\" SET \"version\"=4")()
+
 init_db()
 
 init_db()
 
+
 @app.route("/")
 def main():
     start=int(request.args.get("start", "-1"));
 @app.route("/")
 def main():
     start=int(request.args.get("start", "-1"));
@@ -200,7 +231,7 @@ def main():
         else:
             prev = -1
     return render_template('index.html', motions=rv[:10], more=rv[10]["id"] if len(rv) == 11 else None, times=times.per_host, prev=prev,
         else:
             prev = -1
     return render_template('index.html', motions=rv[:10], more=rv[10]["id"] if len(rv) == 11 else None, times=times.per_host, prev=prev,
-                           categories=get_allowed_cats("create"), singlemotion=False)
+                           categories=get_allowed_cats("create"), singlemotion=False, may_proxyadmin=may_admin("proxyadmin"))
 
 def rel_redirect(loc):
     r = redirect(loc)
 
 def rel_redirect(loc):
     r = redirect(loc)
@@ -288,7 +319,7 @@ def show_motion(motion):
     votes = None
     if may("audit", rv[0].get("type")) and not rv[0].get("running") and not rv[0].get("canceled"):
         votes = get_db().prepare("SELECT vote.result, voter.email FROM vote INNER JOIN voter ON voter.id = vote.voter_id WHERE vote.motion_id=$1")(rv[0].get("id"));
     votes = None
     if may("audit", rv[0].get("type")) and not rv[0].get("running") and not rv[0].get("canceled"):
         votes = get_db().prepare("SELECT vote.result, voter.email FROM vote INNER JOIN voter ON voter.id = vote.voter_id WHERE vote.motion_id=$1")(rv[0].get("id"));
-    return render_template('single_motion.html', motion=rv[0], may_vote=may("vote", rv[0].get("type")), may_cancel=may("cancel", rv[0].get("type")), may_finish=may("finish", rv[0].get("type")), votes=votes, singlemotion=True)
+    return render_template('single_motion.html', motion=rv[0], may_vote=may("vote", rv[0].get("type")), may_cancel=may("cancel", rv[0].get("type")), may_finish=may("finish", rv[0].get("type")), votes=votes, singlemotion=True, may_proxyadmin=may_admin("proxyadmin"))
 
 @app.route("/motion/<string:motion>/vote", methods=['POST'])
 @validate_motion_access('vote')
 
 @app.route("/motion/<string:motion>/vote", methods=['POST'])
 @validate_motion_access('vote')
@@ -302,3 +333,51 @@ def vote(motion, id):
     else:
         db.prepare("UPDATE vote SET result=$3, entered=CURRENT_TIMESTAMP WHERE motion_id=$1 AND voter_id = $2")(id, g.voter, v)
     return motion_edited(id)
     else:
         db.prepare("UPDATE vote SET result=$3, entered=CURRENT_TIMESTAMP WHERE motion_id=$1 AND voter_id = $2")(id, g.voter, v)
     return motion_edited(id)
+
+@app.route("/proxy")
+def proxy():
+    if not may_admin("proxyadmin"):
+        return "Forbidden", 403
+    return render_template('proxy.html', voters=get_voters(), proxies=get_all_proxies(), may_proxyadmin=may_admin("proxyadmin"))
+
+@app.route("/proxy/add", methods=['POST'])
+def add_proxy():
+    if not may_admin("proxyadmin"):
+        return "Forbidden", 403
+    voter=request.form.get("voter", "")
+    proxy=request.form.get("proxy", "")
+    if voter == proxy :
+        return "Error, voter equals proxy.", 400
+    rv = get_db().prepare("SELECT id FROM voter WHERE email=$1")(voter);
+    if len(rv) == 0:
+        return "Error, voter not found.", 400
+    voterid = rv[0].get("id")
+    rv = get_db().prepare("SELECT id FROM voter WHERE email=$1")(proxy);
+    if len(rv) == 0:
+        return "Error, proxy not found.", 400
+    proxyid = rv[0].get("id")
+    rv = get_db().prepare("SELECT id FROM proxy WHERE voter_id=$1 AND revoked is NULL")(voterid);
+    if len(rv) != 0:
+        return "Error, proxy allready given.", 400
+    rv = get_db().prepare("SELECT COUNT(id) as c FROM proxy WHERE proxy_id=$1 AND revoked is NULL GROUP BY proxy_id")(proxyid);
+    if len(rv) != 0:
+        if rv[0].get("c") >= max_proxy:
+            return "Error, Max proxy for '" + proxy + "' reached.", 400
+    rv = get_db().prepare("INSERT INTO proxy(voter_id, proxy_id, granted_by) VALUES ($1,$2,$3)")(voterid, proxyid, g.voter)
+    return rel_redirect("/proxy")
+
+@app.route("/proxy/revoke", methods=['POST'])
+def revoke_proxy():
+    if not may_admin("proxyadmin"):
+        return "Forbidden", 403
+    id=request.form.get("id", "")
+    rv = get_db().prepare("UPDATE proxy SET revoked=CURRENT_TIMESTAMP, revoked_by=$1 WHERE id=$2")(g.voter, int(id))
+    return rel_redirect("/proxy")
+
+@app.route("/proxy/revokeall", methods=['POST'])
+def revoke_proxy_all():
+    if not may_admin("proxyadmin"):
+        return "Forbidden", 403
+    rv = get_db().prepare("UPDATE proxy SET revoked=CURRENT_TIMESTAMP, revoked_by=$1 WHERE revoked IS NULL")(g.voter)
+    return rel_redirect("/proxy")
+
diff --git a/sql/from_3.sql b/sql/from_3.sql
new file mode 100644 (file)
index 0000000..7299f75
--- /dev/null
@@ -0,0 +1,11 @@
+DROP TABLE IF EXISTS proxy;
+CREATE TABLE proxy (id serial NOT NULL,
+                   voter_id INTEGER NOT NULL,
+                   proxy_id INTEGER NOT NULL,
+                   granted timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                   granted_by int NOT NULL,
+                   revoked timestamp NULL DEFAULT NULL,
+                   revoked_by int NULL DEFAULT NULL,
+                   PRIMARY KEY(id));
+CREATE INDEX proxy_voter ON proxy (voter_id);
+CREATE INDEX proxy_proxy ON proxy (proxy_id);
index fd762be112631233ab930418e6cdee68f6b7e262..ea020f8698733b2da6bd1e8503fd94d45c65bf05 100644 (file)
@@ -1,4 +1,4 @@
--- sample data for scheme version 3
+-- sample data for scheme version 4
 INSERT INTO voter (id,email) VALUES (1, 'User A');
 INSERT INTO voter (id,email) VALUES (2, 'User B');
 INSERT INTO voter (id,email) VALUES (3, 'User C');
 INSERT INTO voter (id,email) VALUES (1, 'User A');
 INSERT INTO voter (id,email) VALUES (2, 'User B');
 INSERT INTO voter (id,email) VALUES (3, 'User C');
index cd47201bc863ba6eb894c6a3e9d7d041bf017e1f..be4b3d2a0ac4cb94c2ea272dcbbeb130e0225ac0 100644 (file)
@@ -27,6 +27,18 @@ CREATE TABLE vote (motion_id INTEGER NOT NULL,
                  entered timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
                  PRIMARY KEY(motion_id, voter_id));
 
                  entered timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
                  PRIMARY KEY(motion_id, voter_id));
 
+DROP TABLE IF EXISTS proxy;
+CREATE TABLE proxy (id serial NOT NULL,
+                   voter_id INTEGER NOT NULL,
+                   proxy_id INTEGER NOT NULL,
+                   granted timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+                   granted_by int NOT NULL,
+                   revoked timestamp NULL DEFAULT NULL,
+                   revoked_by int NULL DEFAULT NULL,
+                   PRIMARY KEY(id));
+CREATE INDEX proxy_voter ON proxy (voter_id);
+CREATE INDEX proxy_proxy ON proxy (proxy_id);
+
 DROP TABLE IF EXISTS schema_version;
 CREATE TABLE schema_version (version INTEGER NOT NULL);
 DROP TABLE IF EXISTS schema_version;
 CREATE TABLE schema_version (version INTEGER NOT NULL);
-INSERT INTO schema_version(version) VALUES(3);
+INSERT INTO schema_version(version) VALUES(4);
index 9d66ff4a749a06a5546c8844f5813e62603fab41..db44681aa7b6bbabcb29da3e9a8f60d003b47235 100644 (file)
@@ -2,7 +2,7 @@
 <html>
 <head>
 <title>{% block title %}Motion list{% endblock %}</title>
 <html>
 <head>
 <title>{% block title %}Motion list{% endblock %}</title>
-<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
+<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
 <style type="text/css">
 .form-inline .motion {
   width: 100%;
 <style type="text/css">
 .form-inline .motion {
   width: 100%;
@@ -54,12 +54,41 @@ form {
 </style>
 </head>
 <body>
 </style>
 </head>
 <body>
+<nav class="navbar navbar-expand-lg navbar-light bg-light">
+  <a class="navbar-brand" href="../">{{'Motion list'}}</a>
+  <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
+    <span class="navbar-toggler-icon"></span>
+  </button>
+   <div class="collapse navbar-collapse" id="navbarNavDropdown">
+    <ul class="navbar-nav">
+      <li class="nav-item">
+        <a class="nav-link" href="/">{{'Home'}}</a>
+      </li>
+{%- if may_proxyadmin %}
+      <li class="nav-item">
+        <a class="nav-link" href="/proxy">{{'Proxy management'}}</a>
+      </li>
+{%- endif %}
+      <li class="nav-item">
+        <a class="nav-link">{{'User'}}: {{g.user}}
+{%- if g.proxies_given %}
+         <br/>proxy granted to: {{g.proxies_given}}
+{%- endif %}
+{%- if g.proxies_received %}
+         <br/>holds proxy of: {{g.proxies_received}}
+{%- endif %}
+        </a>
+      </li>
+    </ul>
+  </div>
+</nav>
+
 {%- block body %}
 {% endblock %}
 <!-- Footer -->
   <footer class="page-footer">
     <div class="footer-copyright text-center py-3">
 {%- block body %}
 {% endblock %}
 <!-- Footer -->
   <footer class="page-footer">
     <div class="footer-copyright text-center py-3">
-      <p>� {{footer.version_year}} Copyright: <a href="{{footer.copyright_link}}">{{footer.copyright_name}}</a> 
+      <p>� {{footer.version_year}} Copyright: <a href="{{footer.copyright_link}}">{{footer.copyright_name}}</a> 
       | <a href="{{footer.imprint_link}}">Imprint</a> 
       | <a href="{{footer.dataprotection_link}}">Data protection</a></p>
     </div>
       | <a href="{{footer.imprint_link}}">Imprint</a> 
       | <a href="{{footer.dataprotection_link}}">Data protection</a></p>
     </div>
index ad4b643d7bf1b456749199289b2f30a4e6d22bd1..16adecc2a9127fda861e405ce655b348eccc9ec7 100644 (file)
@@ -2,11 +2,6 @@
 {% block body %}
 <div class="container">
 {%- if categories|length != 0 %}
 {% block body %}
 <div class="container">
 {%- if categories|length != 0 %}
-<div class="card">
-<div class="card-body">
-User: {{g.user}}
-</div>
-</div>
 <form action="/motion" method="POST" class="form-inline">
 <div class="motion card">
   <div class="motion-title card-heading alert-light from-group">
 <form action="/motion" method="POST" class="form-inline">
 <div class="motion card">
   <div class="motion-title card-heading alert-light from-group">
diff --git a/templates/proxy.html b/templates/proxy.html
new file mode 100644 (file)
index 0000000..d81214c
--- /dev/null
@@ -0,0 +1,61 @@
+{% extends "base.html" %}
+{% block body %}
+<div class="container">
+<form action="/proxy/add" method="POST">
+  <table>
+  <tr>
+    <td>Voter</td>
+    <td>Proxy</td>
+    <td></td>
+  </tr>
+  <tr>
+    <td>
+      <select class="float form-control" name="voter">
+    {%- for voter in voters %}
+        <option>{{voter.email}}</option>
+    {%- endfor %}
+      </select>
+    </td>
+    <td>
+      <select class="float form-control" name="proxy">
+        {%- for voter in voters %}
+          <option>{{voter.email}}</option>
+        {%- endfor %}
+      </select>
+    </td>
+    <td>
+      <button type="submit" class="btn btn-primary">Add</button>
+    </td>
+  </tr>
+  </table>
+</form>
+{%- if proxies %}
+<form action="/proxy/revoke" method="POST">
+<div class="motion card" id="votes">
+  <div class="card-heading text-white bg-info">
+    Granted Proxies
+  </div>
+  <div class="card-body">
+    <table>
+      <thead>
+        <th>Voter</th>
+        <th>Proxy</th>
+        <th></th>
+      </thead>
+      {%- for row in proxies %}
+      <tr>
+        <td>{{row.voter_email}}</td>
+        <td>{{row.proxy_email}}</td>
+        <td><button type="submit" class="btn btn-danger" name="id" value="{{row.id}}">Revoke</button></td>
+      </tr>
+      {%- endfor %}
+    </table>
+  </div>
+</div>
+</form>
+{%- endif %}
+<form action="/proxy/revokeall" method="POST">
+  <button type="submit" class="btn btn-danger">Revoke ALL</button>
+</form>
+</div>
+{%- endblock %}
\ No newline at end of file
index 1bc65858461318e377cf8660479502895cf80cff..6551a31ed7e516e538b83fec1f8416ecd19e98de 100644 (file)
@@ -9,7 +9,8 @@ app.config.update(
     DEBUGUSER = {},
     GROUP_PREFIX = {'127.0.0.1:5000': {'group1': 'g1', 'group2': 'g2'}},
     DURATION = {'127.0.0.1:5000':[3, 7, 14]},
     DEBUGUSER = {},
     GROUP_PREFIX = {'127.0.0.1:5000': {'group1': 'g1', 'group2': 'g2'}},
     DURATION = {'127.0.0.1:5000':[3, 7, 14]},
-    SERVER_NAME = '127.0.0.1:5000'
+    SERVER_NAME = '127.0.0.1:5000',
+    MAX_PROXY=2
 )
 
 app.config['TESTING'] = True
 )
 
 app.config['TESTING'] = True
@@ -54,6 +55,20 @@ class BasicTest(TestCase):
             environ_base={'USER_ROLES': user}
         )
 
             environ_base={'USER_ROLES': user}
         )
 
+    def addProxy(self, user, voter, proxy):
+        return self.app.post(
+            '/proxy/add',
+            environ_base={'USER_ROLES': user},
+            data=dict(voter=voter, proxy=proxy)
+        )
+
+    def revokeProxy(self, user, id):
+        return self.app.post(
+            '/proxy/revoke',
+            environ_base={'USER_ROLES': user},
+            data=dict(id=id)
+        )
+
     def buildResultText(self, motiontext, yes, no, abstain):
         return '<p>'+motiontext+'</p></p>\n    <p>\nYes <span class=\"badge badge-pill badge-secondary\">'+str(yes)+'</span><br>'\
             + '\nNo <span class=\"badge badge-pill badge-secondary\">'+str(no)+'</span><br>'\
     def buildResultText(self, motiontext, yes, no, abstain):
         return '<p>'+motiontext+'</p></p>\n    <p>\nYes <span class=\"badge badge-pill badge-secondary\">'+str(yes)+'</span><br>'\
             + '\nNo <span class=\"badge badge-pill badge-secondary\">'+str(no)+'</span><br>'\
@@ -126,6 +141,8 @@ class GeneralTests(BasicTest):
             + '\nNo <span class=\"badge badge-pill badge-secondary\">1</span><br>'\
             + '\nAbstain <span class=\"badge badge-pill badge-secondary\">0</span><br>\n    </p>\n  </div>\n</div>\n</div>'
         self.assertIn(str.encode(testtext), result.data)
             + '\nNo <span class=\"badge badge-pill badge-secondary\">1</span><br>'\
             + '\nAbstain <span class=\"badge badge-pill badge-secondary\">0</span><br>\n    </p>\n  </div>\n</div>\n</div>'
         self.assertIn(str.encode(testtext), result.data)
+        testtext= 'Proxy management'
+        self.assertNotIn(str.encode(testtext), result.data)
 
         # start with second motion
         result = self.app.get('/', environ_base={'USER_ROLES': user}, query_string=dict(start=2))
 
         # start with second motion
         result = self.app.get('/', environ_base={'USER_ROLES': user}, query_string=dict(start=2))
@@ -169,7 +186,26 @@ class GeneralTests(BasicTest):
         self.assertEqual(result.status_code, 404)
         self.assertIn(str.encode('Error, Not found'), result.data)
 
         self.assertEqual(result.status_code, 404)
         self.assertIn(str.encode('Error, Not found'), result.data)
 
+    def test_no_proxy(self):
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        self.assertEqual(result.status_code, 403)
+        self.assertIn(str.encode('Forbidden'), result.data)
 
 
+    def test_no_proxy_add(self):
+        result = self.app.post('proxy/add', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        self.assertEqual(result.status_code, 403)
+        self.assertIn(str.encode('Forbidden'), result.data)
+
+    def test_no_proxy_revoke(self):
+        result = self.app.post('proxy/revoke', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        self.assertEqual(result.status_code, 403)
+        self.assertIn(str.encode('Forbidden'), result.data)
+
+    def test_no_proxy_revokeAll(self):
+        result = self.app.post('proxy/revokeall', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        self.assertEqual(result.status_code, 403)
+        self.assertIn(str.encode('Forbidden'), result.data)
+        
 class VoterTests(BasicTest):
 
     def setUp(self):
 class VoterTests(BasicTest):
 
     def setUp(self):
@@ -517,6 +553,216 @@ class AuditMotionTests(BasicTest):
             + '\n    <div>User C: no</div>\n  </div>\n</div>\n<a href="/?start=2#motion-2" class="btn btn-primary">Back</a>'
         self.assertIn(str.encode(testtext), result.data)
 
             + '\n    <div>User C: no</div>\n  </div>\n</div>\n<a href="/?start=2#motion-2" class="btn btn-primary">Back</a>'
         self.assertIn(str.encode(testtext), result.data)
 
+class ProxyManagementTests(BasicTest):
+
+    def setUp(self):
+        self.init_test()
+        global user
+        user='testuser/proxyadmin:*'
+        self.db_sampledata()
+
+    def tearDown(self):
+        pass
+
+    def test_see_proxy(self):
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= 'div class="container">\n<form action="/proxy/add" method="POST">'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'proxy granted to:'
+        self.assertNotIn(str.encode(testtext), result.data)
+        testtext= 'holds proxy of:'
+        self.assertNotIn(str.encode(testtext), result.data)
+        testtext= '<select class="float form-control" name="voter">\n        '\
+            + '<option>User A</option>\n        <option>User B</option>\n        '\
+            + '<option>User C</option>\n        '\
+            + '<option>testuser</option>\n      '\
+            + '</select>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= '<select class="float form-control" name="proxy">\n          '\
+            + '<option>User A</option>\n          '\
+            + '<option>User B</option>\n          '\
+            + '<option>User C</option>\n          '\
+            + '<option>testuser</option>\n      '\
+            + '</select>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n    '\
+            + '</table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= '<a class="nav-link" href="/proxy">Proxy management</a>'
+        self.assertIn(str.encode(testtext), result.data)
+
+    def test_add_proxy(self):
+        voter=''
+        proxy=''
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, voter equals proxy.'), response.data)
+
+        voter='User A'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, proxy not found.'), response.data)
+
+        voter='User Z'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, voter not found.'), response.data)
+
+        voter=''
+        proxy='User B'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, voter not found.'), response.data)
+
+        voter='User B'
+        proxy='User B'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, voter equals proxy.'), response.data)
+
+        voter='User A'
+        proxy='User B'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<form action="/proxy/revoke" method="POST">'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        '\
+            + '<th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
+            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      '\
+            + '</tr>\n    </table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, proxy allready given.'), response.data)
+
+        voter='User A'
+        proxy='User C'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, proxy allready given.'), response.data)
+
+        voter='User C'
+        proxy='User B'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        '\
+            + '<th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
+            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
+            + '</tr>\n    </table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'proxy granted to:'
+        self.assertNotIn(str.encode(testtext), result.data)
+        testtext= 'holds proxy of:'
+        self.assertNotIn(str.encode(testtext), result.data)
+
+        voter='testuser'
+        proxy='User B'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 400)
+        self.assertIn(str.encode('Error, Max proxy for \'User B\' reached.'), response.data)
+        
+        voter='testuser'
+        proxy='User A'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
+            + '<tr>\n        <td>testuser</td>\n        <td>User A</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="3">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
+            + '</tr>\n    </table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'proxy granted to: User A\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'holds proxy of:'
+        self.assertNotIn(str.encode(testtext), result.data)
+
+        voter='User B'
+        proxy='testuser'
+        response = self.addProxy(user, voter, proxy)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
+            + '<tr>\n        <td>testuser</td>\n        <td>User A</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="3">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User B</td>\n        <td>testuser</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="4">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
+            + '</tr>\n    </table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'proxy granted to: User A\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'holds proxy of: User B\n'
+        self.assertIn(str.encode(testtext), result.data)
+
+        response = self.revokeProxy(user, 4)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
+            + '<tr>\n        <td>testuser</td>\n        <td>User A</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="3">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
+            + '</tr>\n    </table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'proxy granted to: User A\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'holds proxy of:'
+        self.assertNotIn(str.encode(testtext), result.data)
+
+        response = self.revokeProxy(user, 3)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n      '\
+            + '<tr>\n        <td>User A</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="1">Revoke</button></td>\n      </tr>\n      '\
+            + '<tr>\n        <td>User C</td>\n        <td>User B</td>\n        '\
+            + '<td><button type="submit" class="btn btn-danger" name="id" value="2">Revoke</button></td>\n      '\
+            + '</tr>\n    </table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+        testtext= 'proxy granted to:'
+        self.assertNotIn(str.encode(testtext), result.data)
+        testtext= 'holds proxy of:'
+        self.assertNotIn(str.encode(testtext), result.data)
+
+        result = self.app.post('proxy/revokeall', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        self.assertEqual(response.status_code, 302)
+        result = self.app.get('proxy', environ_base={'USER_ROLES': user}, follow_redirects=True)
+        testtext= '<table>\n      '\
+            + '<thead>\n        '\
+            + '<th>Voter</th>\n        <th>Proxy</th>\n        <th></th>\n      </thead>\n    '\
+            + '</table>\n'
+        self.assertIn(str.encode(testtext), result.data)
+
 
 if __name__ == "__main__":
     unittest.main()
 
 if __name__ == "__main__":
     unittest.main()