From e86392ea49e5ed50f076c8a877456a980f6098d4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Schneider?= <jschneider@entrouvert.com>
Date: Wed, 22 Oct 2014 16:04:33 +0200
Subject: [PATCH] add blacklists (by nameIDs and mac addresses) support

Closes #5571
Closes #5572
---
 etc/inc/captiveportal_saml.inc                     |  18 +++
 usr/local/univnautes/sp/rc.sh                      |   9 ++
 usr/local/univnautes/sp/sp/auth.py                 |  29 +++-
 .../sp/sp/management/commands/configxml.py         |  10 +-
 usr/local/univnautes/sp/sp/pfconfigxml.py          |  13 ++
 .../sp/sp/static/univnautes/univnautes.js          |   2 +-
 usr/local/univnautes/sp/update-blacklists.sh       |  24 +++
 .../www/services_captiveportal_saml_blacklist.php  | 162 +++++++++++++++++++++
 usr/local/www/services_captiveportal_zones.php     |   3 +-
 9 files changed, 264 insertions(+), 6 deletions(-)
 create mode 100644 etc/inc/captiveportal_saml.inc
 create mode 100755 usr/local/univnautes/sp/update-blacklists.sh
 create mode 100755 usr/local/www/services_captiveportal_saml_blacklist.php

diff --git a/etc/inc/captiveportal_saml.inc b/etc/inc/captiveportal_saml.inc
new file mode 100644
index 0000000..ad8b9f6
--- /dev/null
+++ b/etc/inc/captiveportal_saml.inc
@@ -0,0 +1,18 @@
+<?php
+
+function display_saml_tabs($file_name) {
+    $tab_array = array();
+    $tab_array[] = array(gettext("Service provider"), false, "services_captiveportal_saml_sp.php");
+    # $tab_array[] = array(gettext("Identity provider"), false, "services_captiveportal_saml_idp.php");
+    $tab_array[] = array(gettext("Federations"), false, "services_captiveportal_saml_federation.php");
+    $tab_array[] = array(gettext("Whitelists"), false, "services_captiveportal_saml_whitelist.php");
+    $tab_array[] = array(gettext("Blacklists"), false, "services_captiveportal_saml_blacklist.php");
+    for ($i = 0; $i < sizeof($tab_array); $i++) {
+        if ($tab_array[$i][2] == $file_name) {
+            $tab_array[$i][1] = true;
+        }
+    }
+    display_top_tabs($tab_array);
+}
+
+?>
diff --git a/usr/local/univnautes/sp/rc.sh b/usr/local/univnautes/sp/rc.sh
index a304a9c..b5afa70 100755
--- a/usr/local/univnautes/sp/rc.sh
+++ b/usr/local/univnautes/sp/rc.sh
@@ -32,6 +32,14 @@ function syncwl() {
     ) &
 }
 
+function syncbl() {
+    echo "sync blacklist in progress (backgrounded)" | logger -p local4.info -t sp/syncblacklists
+    (
+      cd /usr/local/univnautes/sp/
+      ./update-blacklists.sh | logger -p local4.info -t sp/update-blacklists
+    ) &
+}
+
 function syncdata() {
     echo "sync metadatas + geoinfos in progress (backgrounded)" | logger -p local4.info -t sp/syncdata
     (
@@ -56,6 +64,7 @@ function start() {
     echo "started (manage.py runfcgi)" | logger -p local4.info -t sp/start
     syncdata
     syncwl
+    syncbl
     cronstart
 }
 
diff --git a/usr/local/univnautes/sp/sp/auth.py b/usr/local/univnautes/sp/sp/auth.py
index 207adff..7767010 100644
--- a/usr/local/univnautes/sp/sp/auth.py
+++ b/usr/local/univnautes/sp/sp/auth.py
@@ -1,5 +1,6 @@
 # -*- encoding: utf-8 -*-
 
+import re
 import subprocess
 import syslog
 import xml.etree.ElementTree
@@ -8,6 +9,9 @@ from django.conf import settings
 from django.contrib import messages
 
 from authentic2.authsaml2 import signals
+from sp import pfconfigxml
+
+COMMENT=re.compile('^\s*($|#)')
 
 def user_login_cb(sender, request, attributes={}, **kwargs):
     if request and request.user.is_anonymous():
@@ -35,10 +39,31 @@ def user_login_cb(sender, request, attributes={}, **kwargs):
     # log needs eduPersonTargetedID + transientID + idp
     username = eduPersonTargetedID + '|' + eduPersonTargetedID_NameQualifier
 
-    # TODO : blacklist
-
     ip = request.META['REMOTE_ADDR']
 
+    # user blacklist
+    try:
+        nameid_bl = pfconfigxml.get_blacklists('nameid')
+        for line in nameid_bl.splitlines(True):
+            if COMMENT.match(line):
+                continue
+            userbl = line.strip()
+            # if the line starts with ~, it's a regex
+            if userbl[0] == '~' and re.match(userbl[1::], username):
+                request.session['pfsenseid'] = 'BLACKLISTED'
+                messages.error(request, u"Connexion refusée (règle dans une liste noire).")
+                syslog.openlog("logportalauth", syslog.LOG_PID)
+                syslog.syslog(syslog.LOG_LOCAL4 | syslog.LOG_INFO , "BLACKLISTED-REGEX: %s,,%s" % (username, ip))
+                return False
+            elif userbl.strip() == username:
+                request.session['pfsenseid'] = 'BLACKLISTED'
+                messages.error(request, u"Connexion refusée (présent dans une liste noire).")
+                syslog.openlog("logportalauth", syslog.LOG_PID)
+                syslog.syslog(syslog.LOG_LOCAL4 | syslog.LOG_INFO , "BLACKLISTED: %s,,%s" % (username, ip))
+                return False
+    except:
+        pass
+
     # univnautes idp returns univnautesPrivileges attribute (a list)
     multiple = 0
     privileges = attributes.get((u'univnautesPrivileges', u'urn:oasis:names:tc:SAML:2.0:attrname-format:basic'), [])
diff --git a/usr/local/univnautes/sp/sp/management/commands/configxml.py b/usr/local/univnautes/sp/sp/management/commands/configxml.py
index 4290543..d693dfa 100644
--- a/usr/local/univnautes/sp/sp/management/commands/configxml.py
+++ b/usr/local/univnautes/sp/sp/management/commands/configxml.py
@@ -18,12 +18,18 @@
 
 from django.core.management.base import BaseCommand, CommandError
 import os
+import sys
 from sp import pfconfigxml
 
-def get(key):
+def get(key, args):
     if key == 'cpnames':
         for n in pfconfigxml.get_saml_cps():
             print n['name']
+    elif key == 'blacklist':
+        if len(args) > 2:
+            print pfconfigxml.get_blacklists(args[2])
+        else:
+            print >> sys.stderr, 'you need to choose a blacklist (nameid or macadresses)'
 
 class Command(BaseCommand):
     help = 'get infos from config.xml'
@@ -35,5 +41,5 @@ class Command(BaseCommand):
             print "syntax: configxml get|set key"
             return
         if action == 'get':
-            return get(key)
+            return get(key, args)
 
diff --git a/usr/local/univnautes/sp/sp/pfconfigxml.py b/usr/local/univnautes/sp/sp/pfconfigxml.py
index 498b7aa..45f870a 100644
--- a/usr/local/univnautes/sp/sp/pfconfigxml.py
+++ b/usr/local/univnautes/sp/sp/pfconfigxml.py
@@ -240,3 +240,16 @@ def get_whitelists():
             })
     return whitelists
 
+
+def get_blacklists(list_type):
+    """
+    <blacklists>
+        <macaddresses>base64</macaddresses>
+	<nameid>base64</nameid>
+    </blacklists>
+    """
+    xml_blacklist = root().find('univnautes/blacklists/%s' % list_type)
+    if xml_blacklist is not None and xml_blacklist.text:
+        return xml_blacklist.text.decode('base64')
+    return ''
+
diff --git a/usr/local/univnautes/sp/sp/static/univnautes/univnautes.js b/usr/local/univnautes/sp/sp/static/univnautes/univnautes.js
index fb1d273..006418d 100644
--- a/usr/local/univnautes/sp/sp/static/univnautes/univnautes.js
+++ b/usr/local/univnautes/sp/sp/static/univnautes/univnautes.js
@@ -98,7 +98,7 @@ function univnautes(bounds) {
                                     })
                              });
                 marker.entityid = entityid;
-                marker.bindPopup('<p>' + name + '</p><p align="center"><a href="/authsaml2/sso?entity_id=' + entityid + '"><button class="connexion">Connexion</button></a></p>');
+              marker.bindPopup('<p>' + name + '</p><p align="center"><a href="/authsaml2/sso?entity_id=' + entityid + '"><button class="connexion">Connexion</button></a></p>');
                 new_markers.push(marker);
             }
         }
diff --git a/usr/local/univnautes/sp/update-blacklists.sh b/usr/local/univnautes/sp/update-blacklists.sh
new file mode 100755
index 0000000..dbe3828
--- /dev/null
+++ b/usr/local/univnautes/sp/update-blacklists.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+log() {
+    logger -p local4.info -t macblacklist -- "$*"
+}
+
+log "update MAC blacklist"
+cd /usr/local/univnautes/sp
+for ctx in $(./manage.py configxml get cpnames); do
+    /sbin/ipfw -x $ctx -fq delete 666
+    for MAC in $(./manage.py configxml get blacklist macaddresses | sed 's/#.*$//'); do
+        # sanitize
+        MAC=`echo $MAC | tr -dc 0-9a-fA-F:`
+        if test -n "$MAC"; then
+            if /sbin/ipfw -x $ctx -fq add 666 deny MAC $MAC any; then
+                log "added: $MAC in $ctx"
+            else
+                log "ERROR can't add: $MAC (incorrect MAC address ?)"
+            fi
+        fi
+    done
+done
+
+exit 0
diff --git a/usr/local/www/services_captiveportal_saml_blacklist.php b/usr/local/www/services_captiveportal_saml_blacklist.php
new file mode 100755
index 0000000..8426b7f
--- /dev/null
+++ b/usr/local/www/services_captiveportal_saml_blacklist.php
@@ -0,0 +1,162 @@
+<?php
+/*
+	services_captiveportal_saml_sp.php
+	part of m0n0wall (http://m0n0.ch/wall)
+
+	Copyright (C) 2003-2006 Manuel Kasper <mk@neon1.net>.
+	Copyright (C) 2014 Entr'ouvert <info@entrouvert.com>.
+	All rights reserved.
+
+	Redistribution and use in source and binary forms, with or without
+	modification, are permitted provided that the following conditions are met:
+
+	1. Redistributions of source code must retain the above copyright notice,
+	   this list of conditions and the following disclaimer.
+
+	2. Redistributions in binary form must reproduce the above copyright
+	   notice, this list of conditions and the following disclaimer in the
+	   documentation and/or other materials provided with the distribution.
+
+	THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
+	INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+	AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+	AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
+	OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+	SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+	INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+	CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+	ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+	POSSIBILITY OF SUCH DAMAGE.
+*/
+/*
+	pfSense_MODULE:	captiveportal
+*/
+
+##|+PRIV
+##|*IDENT=page-services-captiveportal-saml-sp
+##|*NAME=Services: Captive portal SAML SP page
+##|*DESCR=Allow access to the 'Services: Captive portal SAML SP' page.
+##|*MATCH=services_captiveportal_saml_sp.php*
+##|-PRIV
+
+require_once("guiconfig.inc");
+require_once("functions.inc");
+require_once("filter.inc");
+require_once("shaper.inc");
+require_once("captiveportal.inc");
+require_once("captiveportal_saml.inc");
+
+if (!is_array($config['captiveportal']))
+	$config['captiveportal'] = array();
+$a_cp =& $config['captiveportal'];
+
+if (!is_array($config['univnautes']))
+	$config['univnautes'] = array();
+$a_un =& $config['univnautes'];
+
+if (!is_array($a_un['blacklists']))
+    $a_un['blacklists'] = array(
+        'macaddresses' => array(),
+        'nameid' => array()
+    );
+
+$a_blacklist =& $a_un['blacklists'];
+
+$pconfig['macaddresses'] = base64_decode($a_blacklist['macaddresses']);
+$pconfig['nameid'] = base64_decode($a_blacklist['nameid']);
+
+$pgtitle = array(gettext("Services"),gettext("Captive portal"), "SAML 2.0 Blacklists");
+$shortcut_section = "captiveportal";
+
+if ($_POST) {
+
+	unset($input_errors);
+	$pconfig = $_POST;
+
+	/* input validation */
+    $reqdfields = array();
+	$reqdfieldsn = array();
+
+	do_input_validation($_POST, $reqdfields, $reqdfieldsn, &$input_errors);
+
+    /* if this is an AJAX caller then handle via JSON */
+    if (isAjax() && is_array($input_errors)) {
+        input_errors2Ajax($input_errors);
+        exit;
+    }
+
+	if (!$input_errors) {
+		$a_blacklist['macaddresses'] = base64_encode($pconfig['macaddresses']);
+		$a_blacklist['nameid'] = base64_encode($pconfig['nameid']);
+		/* write config.xml */
+		write_config();
+
+		/* reload whitelists */
+		mwexec_bg("/usr/local/univnautes/sp/rc.sh syncbl");
+
+		/* back to the page */
+		pfSenseHeader("services_captiveportal_saml_blacklist.php");
+	}
+}
+include("head.inc");
+?>
+<body link="#0000CC" vlink="#0000CC" alink="#0000CC">
+<?php include("fbegin.inc"); ?>
+<?php if ($input_errors) print_input_errors($input_errors); ?>
+<?php if ($savemsg) print_info_box($savemsg); ?>
+
+<form action="services_captiveportal_saml_blacklist.php" method="post" enctype="multipart/form-data" name="iform" id="iform">
+
+<table width="100%" border="0" cellpadding="0" cellspacing="0">
+  <tr><td class="tabnavtbl"><?php display_saml_tabs(basename(__FILE__)); ?></td></tr>
+  <tr>
+  <td class="tabcont">
+  <table width="100%" border="0" cellpadding="6" cellspacing="0">
+
+	<tr>
+		<td colspan="2" class="list" height="12"></td>
+	</tr>
+
+
+	<tr>
+		<td valign="top" class="vncellreq"><?=gettext("Mac addresses"); ?></td>
+		<td class="vtable">
+        <textarea name="macaddresses" cols="65" rows="4" id="macaddresses" class="formpre"><?=htmlspecialchars($pconfig['macaddresses']);?></textarea>
+			<br>
+			<?=gettext("List of mac address to ban, one per line."); ?>
+		</td>
+	</tr>
+
+	<tr>
+		<td colspan="2" class="list" height="12"></td>
+	</tr>
+<tr>
+    <td valign="top" class="vncellreq"><?=gettext("SAML NameIDs"); ?></td>
+		<td class="vtable">
+            <textarea name="nameid" cols="65" rows="4" id="nameid" class="formpre"><?=htmlspecialchars($pconfig['nameid']);?></textarea>
+			<br>
+			<?=gettext("List of NameID (SAML identifier) to ban, one per line."); ?>
+		</td>
+	</tr>
+
+	<tr>
+	  <td width="22%" valign="top">&nbsp;</td>
+	  <td width="78%">
+		<?php echo "<input name='zone' id='zone' type='hidden' value='" . htmlspecialchars($cpzone) . "'/>"; ?>
+		<input name="Submit" type="submit" class="formbtn" value="<?=gettext("Save"); ?>" onClick="enable_change(true)">
+		<a href="services_captiveportal_zones.php"><input name="Cancel" type="button" class="formbtn" value="<?=gettext("Cancel"); ?>" onClick="enable_change(true)"></a>
+	  </td>
+	</tr>
+  </table>
+  </td>
+  </tr>
+  </table>
+</form>
+<script language="JavaScript">
+<!--
+enable_change(false);
+//-->
+</script>
+<?php include("fend.inc"); ?>
+</body>
+</html>
diff --git a/usr/local/www/services_captiveportal_zones.php b/usr/local/www/services_captiveportal_zones.php
index bc7ed96..3ebb75e 100644
--- a/usr/local/www/services_captiveportal_zones.php
+++ b/usr/local/www/services_captiveportal_zones.php
@@ -115,7 +115,8 @@ include("head.inc");
 <p>
 <a href="services_captiveportal_saml_sp.php">Service provider</a> |
 <a href="services_captiveportal_saml_federation.php">Federations</a> |
-<a href="services_captiveportal_saml_whitelist.php">Whitelist</a>
+<a href="services_captiveportal_saml_whitelist.php">Whitelist</a> |
+<a href="services_captiveportal_saml_blacklist.php">Blacklist</a>
 </p>
 </div>
 
-- 
2.1.1

